脚本如何实现文件分片上传功能

wen 实用脚本 1

本文目录导读:

脚本如何实现文件分片上传功能

  1. 核心原理
  2. 前端实现
  3. 后端实现 (Node.js)
  4. 使用示例
  5. 优化建议

核心原理

文件分片上传将大文件分割成多个小片段,分别上传,然后在服务端合并。

前端实现

1 基础分片上传

class FileUploader {
    constructor(options = {}) {
        this.chunkSize = options.chunkSize || 1 * 1024 * 1024; // 默认1MB
        this.threads = options.threads || 3; // 并发数
        this.file = null;
        this.chunks = [];
        this.uploadedChunks = new Set();
    }
    // 分片处理
    createChunks(file) {
        this.file = file;
        this.chunks = [];
        let start = 0;
        let index = 0;
        while (start < file.size) {
            const end = Math.min(start + this.chunkSize, file.size);
            const chunk = file.slice(start, end);
            this.chunks.push({
                file: chunk,
                index: index,
                start: start,
                end: end,
                size: chunk.size,
                blob: chunk,
                name: `${file.name}.part.${index}`,
                hash: this.generateHash(chunk, index)
            });
            start = end;
            index++;
        }
    }
    // 生成分片哈希(用于验证)
    generateHash(chunk, index) {
        return `${this.file.name}_${index}_${chunk.size}_${Date.now()}`;
    }
    // 上传单个分片
    async uploadChunk(chunk) {
        const formData = new FormData();
        formData.append('file', chunk.file);
        formData.append('name', chunk.name);
        formData.append('totalChunks', this.chunks.length);
        formData.append('chunkIndex', chunk.index);
        formData.append('fileName', this.file.name);
        formData.append('fileSize', this.file.size);
        formData.append('hash', chunk.hash);
        try {
            const response = await fetch('/upload/chunk', {
                method: 'POST',
                body: formData
            });
            const result = await response.json();
            if (result.success) {
                this.uploadedChunks.add(chunk.index);
                this.updateProgress();
                return true;
            }
            return false;
        } catch (error) {
            console.error(`Chunk ${chunk.index} upload failed:`, error);
            return false;
        }
    }
    // 并发上传所有分片
    async upload() {
        const uploadPromises = [];
        const chunks = [...this.chunks];
        // 限制并发数
        const queue = [];
        while (chunks.length > 0) {
            const chunk = chunks.shift();
            const promise = this.uploadChunk(chunk).finally(() => {
                queue.splice(queue.indexOf(promise), 1);
            });
            queue.push(promise);
            if (queue.length >= this.threads) {
                await Promise.race(queue);
            }
        }
        // 等待所有上传完成
        await Promise.all(queue);
        // 通知服务器合并文件
        return this.mergeFile();
    }
    // 通知服务器合并文件
    async mergeFile() {
        const response = await fetch('/upload/merge', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                fileName: this.file.name,
                totalChunks: this.chunks.length,
                fileSize: this.file.size
            })
        });
        return response.json();
    }
    // 更新进度
    updateProgress() {
        const progress = (this.uploadedChunks.size / this.chunks.length) * 100;
        this.onProgress && this.onProgress(progress);
    }
    // 断点续传(检查已上传的分片)
    async checkUploadedChunks(fileName) {
        const response = await fetch(`/upload/status?fileName=${fileName}`);
        const data = await response.json();
        if (data.exists) {
            data.uploadedChunks.forEach(chunk => {
                this.uploadedChunks.add(chunk);
            });
        }
    }
}
// 使用示例
const uploader = new FileUploader({
    chunkSize: 2 * 1024 * 1024, // 2MB
    threads: 5
});
uploader.onProgress = (progress) => {
    console.log(`上传进度: ${progress.toFixed(2)}%`);
};
// 监听文件选择
document.getElementById('fileInput').addEventListener('change', async (e) => {
    const file = e.target.files[0];
    // 检查已上传的分片(断点续传)
    await uploader.checkUploadedChunks(file.name);
    // 创建分片并上传
    uploader.createChunks(file);
    const result = await uploader.upload();
    console.log('上传结果:', result);
});

2 带进度条和暂停恢复功能

class AdvancedFileUploader {
    constructor(options) {
        this.chunkSize = options.chunkSize || 2 * 1024 * 1024;
        this.maxRetries = options.maxRetries || 3;
        this.paused = false;
        this.aborted = false;
    }
    // 上传带重试机制
    async uploadChunkWithRetry(chunk, retryCount = 0) {
        if (this.paused || this.aborted) {
            return false;
        }
        try {
            const result = await this.uploadChunk(chunk);
            return result;
        } catch (error) {
            if (retryCount < this.maxRetries && !this.aborted) {
                await this.delay(Math.pow(2, retryCount) * 1000); // 指数退避
                return this.uploadChunkWithRetry(chunk, retryCount + 1);
            }
            throw error;
        }
    }
    // 暂停上传
    pause() {
        this.paused = true;
    }
    // 恢复上传
    resume() {
        this.paused = false;
        this.upload();
    }
    // 取消上传
    abort() {
        this.aborted = true;
        // 通知服务器取消
        fetch('/upload/abort', {
            method: 'POST',
            body: JSON.stringify({ fileName: this.file.name })
        });
    }
    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

后端实现 (Node.js)

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs').promises;
const app = express();
const upload = multer({ dest: 'uploads/' });
// 存储分片
app.post('/upload/chunk', upload.single('file'), async (req, res) => {
    try {
        const { fileName, chunkIndex, totalChunks, hash } = req.body;
        const chunkDir = path.join('uploads', fileName);
        // 创建临时目录
        await fs.mkdir(chunkDir, { recursive: true });
        // 移动分片到指定目录
        const chunkPath = path.join(chunkDir, `chunk_${chunkIndex}`);
        await fs.rename(req.file.path, chunkPath);
        res.json({ success: true, message: 'Chunk uploaded' });
    } catch (error) {
        res.status(500).json({ success: false, error: error.message });
    }
});
// 合并分片
app.post('/upload/merge', async (req, res) => {
    try {
        const { fileName, totalChunks, fileSize } = req.body;
        const chunkDir = path.join('uploads', fileName);
        const finalPath = path.join('uploads', 'completed', fileName);
        // 确保目标目录存在
        await fs.mkdir(path.dirname(finalPath), { recursive: true });
        // 合并所有分片
        const writeStream = fs.createWriteStream(finalPath);
        for (let i = 0; i < totalChunks; i++) {
            const chunkPath = path.join(chunkDir, `chunk_${i}`);
            const chunkData = await fs.readFile(chunkPath);
            writeStream.write(chunkData);
        }
        writeStream.end();
        // 等待写入完成
        await new Promise((resolve, reject) => {
            writeStream.on('finish', resolve);
            writeStream.on('error', reject);
        });
        // 验证文件大小
        const stats = await fs.stat(finalPath);
        if (stats.size !== parseInt(fileSize)) {
            throw new Error('File size mismatch');
        }
        // 清理临时分片
        await fs.rm(chunkDir, { recursive: true, force: true });
        res.json({ 
            success: true, 
            message: 'File merged successfully',
            path: finalPath 
        });
    } catch (error) {
        res.status(500).json({ success: false, error: error.message });
    }
});
// 检查上传状态(断点续传)
app.get('/upload/status', async (req, res) => {
    try {
        const { fileName } = req.query;
        const chunkDir = path.join('uploads', fileName);
        try {
            const files = await fs.readdir(chunkDir);
            const uploadedChunks = files
                .filter(f => f.startsWith('chunk_'))
                .map(f => parseInt(f.split('_')[1]));
            res.json({ 
                exists: true, 
                uploadedChunks 
            });
        } catch (error) {
            res.json({ exists: false, uploadedChunks: [] });
        }
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});
// 取消上传
app.post('/upload/abort', async (req, res) => {
    try {
        const { fileName } = req.body;
        const chunkDir = path.join('uploads', fileName);
        // 清理临时文件
        await fs.rm(chunkDir, { recursive: true, force: true });
        res.json({ success: true });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

使用示例

<!DOCTYPE html>
<html>
<head>
    <style>
        .upload-container {
            max-width: 600px;
            margin: 20px auto;
            padding: 20px;
        }
        .progress-bar {
            width: 100%;
            height: 20px;
            background: #f0f0f0;
            border-radius: 10px;
            overflow: hidden;
            margin: 10px 0;
        }
        .progress {
            height: 100%;
            background: linear-gradient(90deg, #4CAF50, #45a049);
            transition: width 0.3s ease;
        }
        .controls {
            display: flex;
            gap: 10px;
            margin-top: 10px;
        }
        .btn {
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        .btn-primary {
            background: #4CAF50;
            color: white;
        }
        .btn-warning {
            background: #ff9800;
            color: white;
        }
        .btn-danger {
            background: #f44336;
            color: white;
        }
    </style>
</head>
<body>
    <div class="upload-container">
        <h2>文件分片上传</h2>
        <input type="file" id="fileInput" />
        <div class="progress-bar">
            <div class="progress" id="progress" style="width: 0%"></div>
        </div>
        <div id="status">准备上传...</div>
        <div class="controls">
            <button class="btn btn-primary" onclick="startUpload()">开始上传</button>
            <button class="btn btn-warning" onclick="pauseUpload()">暂停</button>
            <button class="btn btn-primary" onclick="resumeUpload()">继续</button>
            <button class="btn btn-danger" onclick="cancelUpload()">取消</button>
        </div>
    </div>
    <script>
        let uploader = null;
        document.getElementById('fileInput').addEventListener('change', async (e) => {
            const file = e.target.files[0];
            if (!file) return;
            uploader = new FileUploader({
                chunkSize: 1 * 1024 * 1024, // 1MB
                threads: 3
            });
            uploader.onProgress = (progress) => {
                document.getElementById('progress').style.width = progress + '%';
                document.getElementById('status').textContent = 
                    `上传进度: ${progress.toFixed(2)}%`;
            };
            document.getElementById('status').textContent = '准备上传...';
        });
        async function startUpload() {
            if (!uploader) return;
            const file = document.getElementById('fileInput').files[0];
            uploader.createChunks(file);
            document.getElementById('status').textContent = '开始上传...';
            const result = await uploader.upload();
            if (result.success) {
                document.getElementById('status').textContent = '上传完成!';
            }
        }
        function pauseUpload() {
            if (uploader && uploader.pause) {
                uploader.pause();
                document.getElementById('status').textContent = '已暂停';
            }
        }
        function resumeUpload() {
            if (uploader && uploader.resume) {
                uploader.resume();
                document.getElementById('status').textContent = '继续上传...';
            }
        }
        function cancelUpload() {
            if (uploader && uploader.abort) {
                uploader.abort();
                document.getElementById('status').textContent = '已取消';
                document.getElementById('progress').style.width = '0%';
            }
        }
    </script>
</body>
</html>

优化建议

1 性能优化

  • 动态调整分片大小:根据网络状况调整
  • 并发控制:合理设置并发数
  • 缓存机制:缓存已上传的分片

2 错误处理

  • 重试机制:失败自动重试
  • 数据校验:MD5/SHA256校验
  • 日志记录:详细错误日志

3 安全性

  • 文件类型验证:检查MIME类型
  • 大小限制:防止恶意超大文件
  • 访问控制:身份验证

这个实现支持大文件断点续传、并发上传、进度显示等功能,可以根据实际需求调整参数。

抱歉,评论功能暂时关闭!