资讯详情

资讯详情

Vue CLI实现内网大文件分片上传与断点续传方案

1. 内网超大附件上传的挑战与解决方案在企业级应用开发中内网环境下的大文件上传是个常见但棘手的问题。最近接手的一个内部文档管理系统项目就遇到了用户需要批量上传数百GB设计稿的需求。传统的单文件上传方式在这里完全失效——浏览器卡死、进度反馈缺失、网络中断导致重传等问题频发。Vue CLI作为现代前端开发的标准工具链配合一些专门优化的库可以构建出稳定可靠的大文件上传方案。这里分享我们最终采用的分片上传断点续传并发控制技术组合实测可支持单文件50GB以上的稳定传输。2. 基础环境搭建与核心库选型2.1 初始化Vue CLI项目使用Vue 3的组合式API能获得更好的代码组织vue create large-file-uploader # 选择Vue 3 TypeScript模板 cd large-file-uploader npm install axios spark-md5 vueuse/core2.2 关键库的作用解析axios处理分片上传的HTTP请求需配置自定义实例实现超时重试spark-md5前端计算文件指纹用于服务端校验文件完整性vueuse/core提供useFileDialog等组合式API简化文件操作注意内网环境通常有严格的代理规则需要在vue.config.js中配置devServer.proxy解决跨域问题。我们遇到过Nginx默认限制上传大小为1MB的情况需要调整client_max_body_size参数。3. 大文件分片上传的核心实现3.1 文件分片策略设计// 分片大小建议根据网络环境动态调整 const CHUNK_SIZE 5 * 1024 * 1024; // 5MB interface Chunk { index: number file: Blob hash: string progress: number } const splitFile (file: File): Chunk[] { const chunks: Chunk[] [] const chunkCount Math.ceil(file.size / CHUNK_SIZE) for (let i 0; i chunkCount; i) { const start i * CHUNK_SIZE const end Math.min(file.size, start CHUNK_SIZE) chunks.push({ index: i, file: file.slice(start, end), hash: , // 后续计算 progress: 0 }) } return chunks }3.2 文件指纹计算优化直接计算整个文件的MD5会导致界面卡顿采用抽样计算提升性能const calculateHash (file: File): Promisestring { return new Promise(resolve { const spark new SparkMD5.ArrayBuffer() const reader new FileReader() const size file.size const offset 2 * 1024 * 1024 // 抽样2MB // 头尾各取2MB 中间均匀取样 const chunks [file.slice(0, offset)] for (let i 1; i 3; i) { const mid i * size / 3 chunks.push(file.slice(mid, mid offset)) } chunks.push(file.slice(size - offset, size)) let currentChunk 0 reader.onload e { spark.append(e.target?.result as ArrayBuffer) currentChunk if (currentChunk chunks.length) { reader.readAsArrayBuffer(chunks[currentChunk]) } else { resolve(spark.end()) } } reader.readAsArrayBuffer(chunks[0]) }) }4. 上传流程的健壮性设计4.1 断点续传实现方案服务端需要提供两个关键接口/check接收文件hash返回已上传的分片索引列表/upload接收分片数据返回处理结果前端逻辑const uploadChunks async (chunks: Chunk[], fileHash: string) { // 先检查已上传分片 const { data: uploaded } await axios.post(/check, { hash: fileHash }) const requests chunks .filter(chunk !uploaded.includes(chunk.index)) .map(chunk { const formData new FormData() formData.append(chunk, chunk.file) formData.append(hash, fileHash) formData.append(index, chunk.index.toString()) return axios.post(/upload, formData, { onUploadProgress: progress { chunk.progress Math.round( (progress.loaded / progress.total) * 100 ) } }) }) // 控制并发数 await Promise.all( requests.map((req, i) req.catch(() { console.error(分片${i}上传失败将自动重试) }) ) ) }4.2 错误处理与重试机制我们实现了三级重试策略单个分片上传失败时立即重试最多3次网络断开后重新连接时继续上传页面刷新后通过本地存储恢复上传状态关键代码// 使用localStorage保存上传状态 const saveProgress (fileHash: string, chunks: Chunk[]) { localStorage.setItem(fileHash, JSON.stringify( chunks.map(chunk chunk.progress) )) } // 恢复上传状态 const loadProgress (fileHash: string, chunks: Chunk[]) { const data localStorage.getItem(fileHash) if (data) { const progresses JSON.parse(data) chunks.forEach((chunk, i) { chunk.progress progresses[i] || 0 }) } }5. 文件夹上传的特殊处理5.1 递归遍历文件夹结构使用webkitdirectory属性获取文件夹内容input typefile webkitdirectory changehandleFolderUpload /处理逻辑const handleFolderUpload async (e: Event) { const files Array.from((e.target as HTMLInputElement).files || []) const folderMap new Mapstring, File[]() // 按文件路径分类 files.forEach(file { const path file.webkitRelativePath.split(/)[0] if (!folderMap.has(path)) { folderMap.set(path, []) } folderMap.get(path)?.push(file) }) // 并行上传各文件夹 await Promise.all( Array.from(folderMap).map(([folder, files]) uploadFolder(folder, files) ) ) }5.2 内存优化策略同时上传多个大文件时容易内存溢出我们采用串行上传不同文件夹单个文件夹内并行上传文件每个文件分片上传完成后立即释放内存const uploadFolder async (folder: string, files: File[]) { // 限制并发数 const CONCURRENCY 3 for (let i 0; i files.length; i CONCURRENCY) { await Promise.all( files.slice(i, i CONCURRENCY).map(file uploadSingleFile(file).finally(() { // 手动触发垃圾回收 if (i % 10 0) { window.performance performance.memory console.log(内存使用:, performance.memory) } }) ) ) } }6. 性能监控与用户体验优化6.1 上传速度动态调整根据网络状况自动调整分片大小let dynamicChunkSize 5 * 1024 * 1024 // 初始5MB const adjustChunkSize (avgSpeed: number) { if (avgSpeed 1024 * 1024) { // 1MB/s以上 dynamicChunkSize Math.min(20 * 1024 * 1024, dynamicChunkSize * 1.5) } else { dynamicChunkSize Math.max(1 * 1024 * 1024, dynamicChunkSize * 0.8) } }6.2 进度反馈的精确计算需要区分不同层级的进度单个分片上传进度单个文件上传进度整个文件夹上传进度const calculateProgress (files: UploadFile[]) { let loaded 0 let total 0 files.forEach(file { file.chunks.forEach(chunk { loaded chunk.file.size * (chunk.progress / 100) total chunk.file.size }) }) return total 0 ? Math.round((loaded / total) * 100) : 0 }7. 服务端配合要点虽然主要讨论前端实现但服务端也需要相应配合7.1 分片存储设计建议采用临时目录最终合并的方案/tmp /{fileHash} /0.chunk /1.chunk /... /merged /final_file.zip7.2 合并文件的高效方式使用流式处理避免内存爆炸# Python示例 def merge_chunks(file_hash, target_path): chunk_files sorted(glob.glob(f/tmp/{file_hash}/*.chunk), keylambda x: int(x.split(/)[-1].split(.)[0])) with open(target_path, wb) as f: for chunk_file in chunk_files: with open(chunk_file, rb) as cf: f.write(cf.read()) os.unlink(chunk_file) # 删除临时分片 os.rmdir(f/tmp/{file_hash}) # 删除临时目录8. 实际部署中的经验教训在内网环境部署这套方案时我们遇到了几个意料之外的问题企业防火墙干扰某些网络设备会重置长时间空闲的连接。解决方案是添加心跳包机制每30秒发送一个空请求保持连接活跃。磁盘IO瓶颈当多个用户同时上传时服务端的磁盘写入成为瓶颈。最终我们实现了分片写入不同物理磁盘的策略。内存泄漏长时间运行的Vue应用可能出现内存增长。通过以下方式解决// 在组件卸载时清理 onUnmounted(() { cancelTokenSource?.cancel() uploadWorker?.terminate() localStorage.removeItem(currentFileHash.value) })路径编码问题Windows和MacOS的文件夹路径编码方式不同需要统一处理const normalizePath (path: string) { return path.replace(/\\/g, /).normalize(NFC) }这套方案目前已在生产环境稳定运行6个月单日处理上传数据超过10TB。最大的改进空间在于增加P2P传输能力让内网客户端之间可以直接交换分片进一步减轻服务器压力。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →