大文件上传解决方案

各位同行大佬们好,作为一个在广东摸爬滚打多年的前端"老油条",最近接了个让我差点秃顶的项目——20G大文件上传系统,还要兼容IE9!这感觉就像让我用竹篮子去打水还要不漏一样刺激…

需求分析:客户这是要我老命啊

  • 20G大文件传输:我寻思着这不是上传,这是在往浏览器里塞一头大象啊
  • 文件夹保留层级:客户说文件夹里有1000个分类文件,这哪是文件夹,这是个文件博物馆!
  • 加密传输存储:SM4、AES齐上阵,比瑞士银行的保险箱还严实
  • 断点续传:关了浏览器、重启电脑都不能丢进度,这要求比我的记忆力靠谱多了
  • 非打包下载:几万个文件直接下载,这网速得比我的工资涨得还快才行
  • 兼容IE9:Windows7+IE9的组合,让我梦回2012年,青春啊!

最绝的是预算100元以内还要求7×24小时支持,这价格连我家的路由器月租都不够啊兄弟们!

前端解决方案:与IE9的世纪和解

既然客户爸爸说了要用原生JS,那咱们就用H5的File API+IndexedDB来整活:




    穷逼版大文件上传
    
    
        /* 祖传CSS,兼容IE9 */
        .upload-area {
            border: 2px dashed #ccc;
            padding: 20px;
            text-align: center;
            margin: 20px;
            background: #f9f9f9;
        }
        .progress-container {
            width: 100%;
            background-color: #f5f5f5;
            margin: 10px 0;
            height: 20px;
            position: relative;
        }
        .progress-bar {
            height: 100%;
            background-color: #4CAF50;
            width: 0%;
            transition: width 0.3s;
        }
        .progress-text {
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            color: #333;
            font-size: 12px;
        }
        .file-item {
            margin: 10px 0;
            padding: 10px;
            border: 1px solid #eee;
            background: #fff;
        }
        .file-name {
            font-weight: bold;
        }
        .file-path {
            color: #666;
            font-size: 0.9em;
            margin-left: 10px;
        }
        .file-size {
            color: #888;
            font-size: 0.9em;
            margin-left: 10px;
        }
        .file-controls {
            margin-top: 5px;
        }
        button {
            padding: 5px 10px;
            margin-right: 5px;
            background: #f0f0f0;
            border: 1px solid #ddd;
            cursor: pointer;
        }
        button:hover {
            background: #e0e0e0;
        }
    


    大文件上传(兼容IE9版)
    
    
    
    
    
        拖放文件或文件夹到此处
        或
        
        选择文件/文件夹
    
    
    
        等待上传文件...
    
    
    
        开始上传
        暂停
        继续
    
    
    
        加密方式:
        
            SM4国密
            AES-256
        
        
    

    
        // 上传队列
        var uploadQueue = [];
        var currentUpload = null;
        var chunkSize = 5 * 1024 * 1024; // 5MB分块
        
        // 初始化
        function init() {
            // 文件选择处理
            document.getElementById('fileInput').addEventListener('change', function(e) {
                handleFiles(e.target.files);
            });
            
            // 拖放处理
            var dropArea = document.getElementById('dropArea');
            dropArea.addEventListener('dragover', function(e) {
                e.preventDefault();
                dropArea.style.borderColor = '#4CAF50';
                dropArea.style.background = '#f0fff0';
            });
            
            dropArea.addEventListener('dragleave', function() {
                dropArea.style.borderColor = '#ccc';
                dropArea.style.background = '#f9f9f9';
            });
            
            dropArea.addEventListener('drop', function(e) {
                e.preventDefault();
                dropArea.style.borderColor = '#ccc';
                dropArea.style.background = '#f9f9f9';
                handleFiles(e.dataTransfer.files);
            });
            
            // 加载未完成的传输
            loadPendingTransfers();
        }
        
        // 处理文件选择
        function handleFiles(files) {
            var queueContainer = document.getElementById('uploadQueue');
            queueContainer.innerHTML = '';
            
            for (var i = 0; i < files.length; i++) {
                var file = files[i];
                addFileToQueue(file);
            }
        }
        
        // 添加文件到队列
        function addFileToQueue(file) {
            var fileItem = {
                id: generateFileId(file),
                name: file.name,
                path: file.webkitRelativePath || '',
                size: file.size,
                progress: 0,
                status: 'pending',
                file: file
            };
            
            uploadQueue.push(fileItem);
            renderQueue();
        }
        
        // 渲染队列
        function renderQueue() {
            var queueContainer = document.getElementById('uploadQueue');
            queueContainer.innerHTML = '';
            
            if (uploadQueue.length === 0) {
                queueContainer.innerHTML = '<p>等待上传文件...</p>';
                return;
            }
            
            for (var i = 0; i < uploadQueue.length; i++) {
                var item = uploadQueue[i];
                var itemElement = document.createElement('div');
                itemElement.className = 'file-item';
                itemElement.innerHTML = `
                    <div>
                        <span class="file-name">${item.name}</span>
                        <span class="file-path">${item.path}</span>
                        <span class="file-size">${formatFileSize(item.size)}</span>
                    </div>
                    <div class="progress-container">
                        <div class="progress-bar" style="width:${item.progress}%"></div>
                        <span class="progress-text">${item.progress}%</span>
                    </div>
                    <div class="file-controls">
                        <button onclick="pauseItem('${item.id}')" ${item.status !== 'uploading' ? 'disabled' : ''}>暂停</button>
                        <button onclick="resumeItem('${item.id}')" ${item.status !== 'paused' ? 'disabled' : ''}>继续</button>
                        <button onclick="cancelItem('${item.id}')">取消</button>
                    </div>
                `;
                queueContainer.appendChild(itemElement);
            }
        }
        
        // 开始上传
        function startUpload() {
            if (uploadQueue.length === 0) return;
            
            currentUpload = uploadQueue.find(item => item.status === 'pending');
            if (currentUpload) {
                currentUpload.status = 'uploading';
                uploadFile(currentUpload);
            }
        }
        
        // 上传文件
        function uploadFile(fileItem) {
            var file = fileItem.file;
            var totalChunks = Math.ceil(file.size / chunkSize);
            
            // 从本地存储加载断点
            var resumeChunk = localStorage.getItem('resume_' + fileItem.id) || 0;
            
            // 上传分块
            for (var chunkIndex = resumeChunk; chunkIndex < totalChunks; chunkIndex++) {
                if (fileItem.status === 'paused') break;
                
                var start = chunkIndex * chunkSize;
                var end = Math.min(start + chunkSize, file.size);
                var chunk = file.slice(start, end);
                
                var formData = new FormData();
                formData.append('fileId', fileItem.id);
                formData.append('chunkIndex', chunkIndex);
                formData.append('totalChunks', totalChunks);
                formData.append('fileName', fileItem.name);
                formData.append('filePath', fileItem.path);
                formData.append('fileSize', fileItem.size);
                formData.append('chunkData', chunk);
                formData.append('encryption', document.getElementById('encryptionType').value);
                formData.append('encryptionKey', document.getElementById('encryptionKey').value);
                
                // AJAX上传(兼容IE9)
                var xhr = new XMLHttpRequest();
                xhr.open('POST', '/api/upload/chunk', false); // 同步上传
                
                xhr.upload.onprogress = function(e) {
                    var loaded = chunkIndex * chunkSize + e.loaded;
                    fileItem.progress = Math.round((loaded / fileItem.size) * 100);
                    renderQueue();
                };
                
                xhr.onreadystatechange = function() {
                    if (xhr.readyState === 4) {
                        if (xhr.status === 200) {
                            // 保存断点
                            localStorage.setItem('resume_' + fileItem.id, chunkIndex + 1);
                            
                            if (chunkIndex === totalChunks - 1) {
                                // 合并文件
                                mergeFile(fileItem.id);
                                fileItem.status = 'completed';
                                startUpload(); // 开始下一个文件
                            }
                        } else {
                            console.error('上传失败:', xhr.responseText);
                            fileItem.status = 'error';
                            renderQueue();
                        }
                    }
                };
                
                try {
                    xhr.send(formData);
                } catch (e) {
                    console.error('上传出错:', e);
                    fileItem.status = 'error';
                    renderQueue();
                    break;
                }
            }
        }
        
        // 暂停上传
        function pauseUpload() {
            if (currentUpload) {
                currentUpload.status = 'paused';
                renderQueue();
            }
        }
        
        // 继续上传
        function resumeUpload() {
            if (currentUpload && currentUpload.status === 'paused') {
                currentUpload.status = 'uploading';
                uploadFile(currentUpload);
            }
        }
        
        // 暂停单个项目
        function pauseItem(fileId) {
            var item = uploadQueue.find(item => item.id === fileId);
            if (item) {
                item.status = 'paused';
                renderQueue();
            }
        }
        
        // 继续单个项目
        function resumeItem(fileId) {
            var item = uploadQueue.find(item => item.id === fileId);
            if (item && item.status === 'paused') {
                item.status = 'uploading';
                uploadFile(item);
            }
        }
        
        // 取消单个项目
        function cancelItem(fileId) {
            var index = uploadQueue.findIndex(item => item.id === fileId);
            if (index >= 0) {
                // 通知后端取消上传
                cancelUpload(fileId);
                
                // 从队列移除
                uploadQueue.splice(index, 1);
                renderQueue();
            }
        }
        
        // 合并文件
        function mergeFile(fileId) {
            var xhr = new XMLHttpRequest();
            xhr.open('POST', '/api/upload/merge', false);
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
            
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    console.log('文件合并成功:', xhr.responseText);
                    localStorage.removeItem('resume_' + fileId);
                }
            };
            
            xhr.send('fileId=' + encodeURIComponent(fileId) + 
                    '&encryption=' + encodeURIComponent(document.getElementById('encryptionType').value));
        }
        
        // 取消上传
        function cancelUpload(fileId) {
            var xhr = new XMLHttpRequest();
            xhr.open('POST', '/api/upload/cancel', false);
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
            xhr.send('fileId=' + encodeURIComponent(fileId));
        }
        
        // 加载未完成的传输
        function loadPendingTransfers() {
            var xhr = new XMLHttpRequest();
            xhr.open('GET', '/api/upload/pending', false);
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    var pendingFiles = JSON.parse(xhr.responseText);
                    pendingFiles.forEach(function(fileInfo) {
                        uploadQueue.push({
                            id: fileInfo.fileId,
                            name: fileInfo.fileName,
                            path: fileInfo.filePath,
                            size: fileInfo.fileSize,
                            progress: fileInfo.progress,
                            status: 'paused'
                        });
                    });
                    renderQueue();
                }
            };
            xhr.send();
        }
        
        // 生成文件ID
        function generateFileId(file) {
            return file.name + '_' + file.size + '_' + file.lastModified;
        }
        
        // 格式化文件大小
        function formatFileSize(bytes) {
            if (bytes === 0) return '0 B';
            var k = 1024;
            var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
            var i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
        }
        
        // 初始化
        window.onload = init;
    


前端关键功能说明

1. 兼容IE9的黑魔法


  • 使用条件注释只对IE9及以下浏览器加载polyfill
  • json2.js解决IE9没有JSON对象的问题
  • promise-polyfill解决IE9不支持Promise的问题

2. 文件夹上传核心代码

// 文件选择元素添加webkitdirectory和directory属性


// 处理文件时保留路径信息
function handleFiles(files) {
    for (var i = 0; i < files.length; i++) {
        var file = files[i];
        var fileItem = {
            name: file.name,
            path: file.webkitRelativePath || '', // 保留相对路径
            size: file.size
            // ...
        };
        uploadQueue.push(fileItem);
    }
}

3. 断点续传实现

// 上传前检查本地是否有断点记录
var resumeChunk = localStorage.getItem('resume_' + fileItem.id) || 0;

// 上传成功后保存断点
localStorage.setItem('resume_' + fileItem.id, chunkIndex + 1);

// 文件合并成功后清理断点
localStorage.removeItem('resume_' + fileItem.id);

4. 加密传输(伪实现)

// 实际项目中应该使用Web Crypto API或相应库
formData.append('encryption', document.getElementById('encryptionType').value);
formData.append('encryptionKey', document.getElementById('encryptionKey').value);

如何与后端对接

需要后端提供的API接口

  1. 分块上传接口

    POST /api/upload/chunk
    参数:
    - fileId: 文件唯一ID
    - chunkIndex: 当前分块索引
    - totalChunks: 总分块数
    - fileName: 文件名
    - filePath: 文件相对路径(文件夹结构)
    - fileSize: 文件大小
    - chunkData: 分块数据
    - encryption: 加密算法
    - encryptionKey: 加密密钥
    
  2. 合并文件接口

    POST /api/upload/merge
    参数:
    - fileId: 文件唯一ID
    - encryption: 加密算法
    
  3. 取消上传接口

    POST /api/upload/cancel
    参数:
    - fileId: 文件唯一ID
    
  4. 获取未完成任务接口

    GET /api/upload/pending
    返回:
    [
      {
        fileId: string,
        fileName: string,
        filePath: string,
        fileSize: number,
        progress: number
      }
    ]
    

部署注意事项

  1. IE9兼容性

    • 确保服务器正确设置X-UA-Compatible
    • 添加MIME类型.json application/json
  2. 大文件上传

    • 配置Nginx/Apache的上传大小限制
    • 设置PHP的upload_max_filesizepost_max_size
  3. 断点续传

    • 确保localStorage可用(IE8+支持)
    • 对于隐私模式,需要降级使用cookie存储
  4. 加密传输

    • 实际项目中应该使用HTTPS
    • 前端加密应该使用Web Crypto API或相应polyfill

最后吐槽

  1. 100块预算还要兼容IE9?甲方是不是对程序员有什么误解?

  2. 20G文件上传?建议先问问甲方他们服务器硬盘够不够大

  3. 7x24小时免费技术支持?我连7x24小时睡觉都保证不了…

  4. 要源代码?要文档?要一条龙服务?100块连个外卖都点不了好吗!

  5. 加群374992201领红包?兄弟,有这功夫不如多接几个项目…

不过既然你都看到这里了,代码拿去用吧,记得请我喝奶茶(至少得是喜茶级别的)!

将组件复制到项目中

示例中已经包含此目录
image

引入组件

image

配置接口地址

接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表
参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de
image

处理事件

image

启动测试

image

启动成功

image

效果

image

数据库

image

效果预览

文件上传

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件续传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
文件夹上传

下载示例

点击下载完整示例

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐