前阵子接了个老项目升级的活,后台管理系统还是五六年前的jQuery写法,为了长期发展,老板要求整体往vue3迁。但业务不能停,老页面不能一下子推翻重写,最头疼的是一个素材上传模块——用户经常要传几个G的视频、压缩包和设计稿,以前用传统form上传动不动就超时,传到一半断网就只能从头再来。
需求写得很明确:新功能必须用vue3实现,老页面继续用jQuery跑,同时必须支持大文件秒传。就这么一个“新老结合”的技术场景,前后磨了一周多。今天把jQuery与vue3混合开发大文件秒传功能的整套实现思路、核心代码和踩过的坑都整理出来,给同样在改造老项目的朋友一个参考。
这套方案的适用面其实很宽:只要你的项目正处在jQuery向vue3过渡的阶段,或者你负责维护一个老系统但需要接入现代化上传体验,都应该能从中找到能直接用的东西。
1. 内容整体设计与思路拆解
1.1 混合开发场景解析:为什么不是重写而是共存
很多团队遇到老项目升级,第一反应是“找时间重写”。但真实业务场景里,老系统背后往往有几十张表、几百个接口、一堆没人敢动的报表逻辑,全量重写周期至少按年算,业务不可能停下来等你。渐进式改造就成了唯一理性选择:老页面继续稳定运行,新功能以vue3模块的方式嵌入到老页面里去。
这条路听起来简单,实际做起来有一堆细节要处理。vue3是组件化、响应式的思维,jQuery是命令式、直接操作DOM的思维,两者混在一起如果不加隔离,很快就乱成一锅粥。我的做法是:把上传这类新功能整体放到一个由vue3控制的独立容器里,jQuery只负责老页面的交互,两者通过约定的接口通信,不互相碰对方的DOM结构。
另外要认清一点,混合开发不是长期目标,它只是过渡期的手段。所以新增代码尽量写框架无关的逻辑,比如文件切片、hash计算、上传队列这些纯JS能力,抽出来谁都能调,将来vue3全面接管了,这部分代码可以直接复用。
1.2 秒传功能的原理与它解决的痛点
很多人以为秒传是“网络快”,其实秒传根本不是传输,而是“跳过传输”。
文件在服务端本质上就是一堆字节。如果两个文件的内容完全一致,那它们的字节也完全一致。前端拿到用户选择的文件后,先对文件内容做一次hash计算(md5或sha1),得到一个指纹字符串。把这个指纹提交给服务端,服务端在自己的存储里查一下——如果之前已经有人上传过内容完全相同的文件,就直接返回一个“已存在”的标识,前端立即提示上传完成。整个过程中,实际传输的只有几KB的请求数据,大几G的文件瞬间完成,这就是“秒传”的真相。
秒传解决的核心痛点是“重复上传”。在素材管理、视频处理、文档协作这类场景里,团队内反复传同一个大文件的情况非常常见。如果没有秒传,每个用户都要完整传一遍,既浪费带宽又浪费时间。有了秒传,相同内容只真正上传一次,后续所有人都是秒开。
但秒传也不是万能的,它只能处理“服务端已经存在相同内容”的场景。如果服务端没有这个文件,那还是得走分片上传。所以完整的方案是:秒传优先,分片兜底,两者结合。
1.3 上传方案选型对比:为什么最终选择分片加秒传
在确定方案之前,我把几种主流上传方式放在一起做了对比:
| 方案 | 核心原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 普通form上传 | 浏览器直接POST整个文件 | 实现最简单 | 无法监听进度、超时容易失败、无断点续传 | 小文件、内部工具 |
| 分片上传 | 文件切成多块顺序上传 | 支持断点续传、失败重传成本低 | 需要前后端配合分片与合并 | 大文件、弱网环境 |
| 断点续传 | 记录已上传分片,续传时代跳过 | 对网络波动容忍度高 | 需要服务端记录分片状态 | 大文件、移动端弱网 |
| 秒传 | hash判重,跳过实际传输 | 体验极致,零等待 | 依赖服务端已有数据 | 重复内容多的平台 |
最终方案定为“分片上传 + 断点续传 + hash秒传”的组合。前两步保证大文件在常规网络环境下能传得动,第三步提升重复场景下的体验。三者在前端流程里是串在一起的:先算hash查重,查到了直接秒传;没查到就切分片,上传过程中记录已上传分片,中断后可以从断点继续。
2. 核心细节解析与实操要点
2.1 分片大小、并发数与内存的关系
分片大小没有绝对标准,但选错了体验差别很大。我常用的分片大小是5MB,同时控制并发数为3到5。这个组合在多数办公网环境下表现稳定。
分片太小会有问题。比如1GB文件,如果按1MB切,就是1024个分片,每个分片都要发起一次HTTP请求,光请求开销就把带宽优势抵消了。再加上服务端合并分片时,文件数量多意味着排序、I/O操作更频繁,合并耗时也明显变长。分片太大也有问题,一个分片传半天,中途失败重传的成本高,进度反馈也不够细。
并发数同样需要控制。并发太高,浏览器会创建大量连接,突破服务器连接上限后反而互相争抢带宽,单个分片传输速度直线下降。我做过一个简单测试,在10MB带宽下,并发3到5基本能跑满带宽;并发上到10之后,总吞吐量反而掉了。原因是分片传输也要经过TCP握手、TLS协商等过程,连接过多会引入大量排队和重传。
内存方面,前端要尽量避免一次性把整个文件读入内存。切分片用的是File.slice方法,它返回的是原始文件的引用视图,并不是真正把数据拷贝出来,所以内存压力基本可控。真正吃内存的是计算hash时的FileReader读取过程,这个需要分块读取、逐块追加,不能一次性读整个大文件。
2.2 文件指纹hash的计算策略
文件hash是整个秒传功能的地基,地基歪了什么都白搭。目前工程里用得最多的是spark-md5这个库,它支持增量追加,可以配合FileReader把大文件一片一片读进内存计算,这样内存占用始终保持在较低水平。
这里有两个策略选择:全量计算和抽样计算。
全量计算就是读文件的每一个字节参与hash计算,准确率最高,但大文件耗时长。一个500MB的文件在全量计算下可能需要5到10秒,期间如果放在UI线程会直接卡死页面。解决办法是把计算放到Web Worker里异步执行,或者用requestIdleCallback在浏览器空闲时段分片处理。抽样计算则只取文件头部、中间、尾部的一部分字节参与hash,速度很快但准确率差一些,不同文件可能算出相同hash,容易造成误判。我的建议是:追求体验可以先用抽样hash做快速判断,命中后进入分片上传流程,由服务端按完整hash做最终确认。如果项目规模不大、使用人数少,直接全量计算更省心。
还有一点必须注意,hash计算必须基于文件内容,不能基于文件名。同一个文件重命名后内容不变,hash应该一致;反过来,两个文件内容完全一样但名字不同,也应该判重为同一个文件。有的实现图省事直接用文件路径加文件名算hash,这种方案遇到同名不同内容的文件就会出严重问题。
2.3 秒传判重、分片上传、合并确认的三段式接口设计
接口设计好不好,直接决定前后端对接效率。我按照“判重、上传、合并确认”三个阶段设计了三个核心接口。
第一个是hash判重接口,前端计算完文件hash后调用。请求参数带上fileHash、fileName、fileSize,服务端返回文件是否存在。为了防止hash碰撞,服务端可以顺带比对fileSize,大小不一致就直接判不存在,不用再读文件内容。
第二个是分片上传接口,用于真实传输文件分片。请求参数要带fileHash、index、total、fileName,文件本体放在FormData的file字段里。服务端接收到分片后存到临时目录,文件名建议用“fileHash_index.part”这种格式,方便后续合并定位。
第三个是合并确认接口,当前端所有分片上传完成后触发。服务端根据fileHash在临时目录找到全部分片,按index排序后流式合并成最终文件,合并完成返回文件访问路径。这一步尽量不要做成“边传边合”,因为分片到达顺序是乱序的,边传边合容易写坏文件。
2.4 并发控制与进度上报机制
并发控制如果写在每个分片请求的for循环里,那等于没有控制。我在实现里是手动维护一个任务队列,队列里有待上传的分片任务,通过一个控制函数限制同时执行的任务数,每完成一个就从队列取下一个补上。这种写法虽然比并行丢弃全部请求要复杂一些,但对网络和服务端压力都很友好,断网重试也能自然衔接。
进度上报要区分两个维度:上传进度和整体进度。上传进度指的是当前分片请求的字节进度,可以用axios的onUploadProgress或XHR的upload.onprogress拿到,把它换算成总进度里的一个比例。整体进度则是“已成功上传分片数/总分片数”,在主线程根据任务完成数计算,直接驱动vue3的进度条。两个进度相互配合,用户看到的进度条才会既有颗粒感又有稳定性。
3. 实操过程:jQuery与vue3混合开发的核心环节实现
3.1 vue3在老页面里安全挂载,互不干扰
在jQuery页面里引入vue3,最大的忌讳是直接拿vue3接管整个页面的body。老页面有一堆绑定在body上的事件、弹窗、定时器,vue3挂载时会重建DOM,这些老逻辑会瞬间失效。
我的做法是单独划出一个上传区域,只要老页面上留一个空的div容器,在jQuery代码里调用vue3的createApp,把这个容器作为挂载点。这样vue3只管自己的这块DOM,老页面的其他部分继续由jQuery掌控。
// 老页面的jQuery逻辑里,在DOM ready之后挂载vue3 $(document).ready(function () { // 老业务代码继续跑 initOldList(); // 新上传功能由vue3接管 import('./vue/UploadModule.js').then(({ mountUploadModule }) => { mountUploadModule('#upload-module'); }); });UploadModule.js内部就是标准的vue3启动逻辑,创建应用实例、挂载router或状态、mount到指定容器。这种方式的好处是:vue3模块加载是异步的,不阻碍老页面首屏渲染;挂载位置可控,不会误伤老逻辑。
3.2 上传核心逻辑抽成框架无关的纯JS模块
混合开发最怕写出来的代码和框架绑死。我在这个项目里把上传核心逻辑完全抽离成纯JS模块uploader.js,内部不依赖vue、不依赖jQuery,只暴露几个方法:计算hash、判重、分片上传、断点续传、取消任务。UI层无论是vue3还是jQuery,都只是调用它然后展示状态。
这样设计的好处很明显:第一,vue3组件和jQuery页面可以共用同一套上传能力;第二,将来把整个页面都迁到vue3时,上传逻辑不用重写;第三,出问题时很好调试,纯JS模块可以脱离UI单独测试。
// uploader.js 模块导出结构 export const Uploader = { calcFileHash(file, onProgress) {}, checkExist(fileHash) {}, uploadChunks(file, fileHash, options) {}, cancel() {}, getProgress() {} };// jquery老页面直接引用,不需要vue3 const uploader = new Uploader(); uploader.calcFileHash(file, (p) => { $('#hash-progress').text(Math.round(p * 100) + '%'); });而在vue3组件里,同样调用Uploader,只是把回调绑定到响应式变量上,UI由模板驱动。这样一份逻辑服务两端,真正体现混合开发的优雅之处。
3.3 jQuery与vue3之间的通信机制
两套框架共存,通信是绕不开的问题。我总结出三个层级的通信方式,按场景选用。
第一个是全局方法挂载。在vue3模块启动时,把一些能力挂到window上,比如window.$uploader = { addFile, getStatus }。老页面jQuery里可以直接调用这个方法,把选中的文件交给vue3上传组件处理。这个方式最简单,但要注意命名空间,避免污染全局。
第二个是自定义事件。vue3侧通过dispatchEvent发自定义事件,比如window.dispatchEvent(new CustomEvent('upload-finished', { detail: { fileHash, url } })),jQuery侧用$(window).on('upload-finished', handler)监听。反过来jQuery触发vue3也一样用事件。这种方式的优点是解耦,双方各自监听自己关心的事件,不需要互相引用实例。
第三个是共享状态对象。定义一个小小的状态store,用vue3的reactive包裹,但把store实例暴露出去。jQuery和vue3都能读写store里的字段,vue3由于是响应式的,store变化会自动更新UI。这个适合需要频繁同步状态的场景,比如上传队列、任务进度、错误信息等。
实际项目中,我大多数场景用第一种和第二种结合,状态同步用第三种。训练下来的体会是:通信越简单越好,不要让jQuery代码直接操作vue3内部的ref变量,封装成方法或事件是更安全的边界。
3.4 样式与依赖冲突的规避方案
jQuery项目多数用的是老式全局CSS,vue3这边如果用element-plus这类组件库,两边的样式很容易互相打架。最典型的是reset样式和全局padding、font-family不一致,vue组件在jQuery页面里渲染出来按钮大小、间距都和预期不一样。
我的处理方式是三条线并行。第一,vue组件内部样式全部加scoped,避免组件样式泄漏到老页面;第二,引入UI库时不要全量引入,按需加载组件和样式,减少全局污染面;第三,在vue3挂载容器下单独恢复一套基础样式变量,和老页面的样式设定做隔离。
依赖冲突方面,最怕的是两套框架各自引入不同版本的公共库。老jQuery项目可能全局挂在window.$,vue3项目里如果也直接依赖jQuery就乱套了。我的原则是:vue3组件里绝不直接使用全局的jQuery对象,所有DOM操作和事件都走vue的方式;老jQuery页面里也不直接操作vue3内部的DOM结构。两条线彻底分开,不要交叉引用。
4. 完整实现流程与关键代码
4.1 文件hash计算的完整实现
hash计算我用的是spark-md5,读取分块追加计算。下面的实现可以直接放到uploader.js里:
import SparkMD5 from 'spark-md5'; export function calcFileHash(file, onProgress) { return new Promise((resolve, reject) => { const chunkSize = 2 * 1024 * 1024; // 每块2MB const chunkCount = Math.ceil(file.size / chunkSize); const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); let currentChunk = 0; fileReader.onerror = (e) => { reject(new Error('文件读取失败')); }; fileReader.onload = (e) => { spark.append(e.target.result); currentChunk++; if (onProgress) { onProgress(currentChunk / chunkCount); } if (currentChunk < chunkCount) { loadNext(); } else { const fileHash = spark.end(); resolve(fileHash); } }; function loadNext() { const start = currentChunk * chunkSize; const end = Math.min(start + chunkSize, file.size); fileReader.readAsArrayBuffer(file.slice(start, end)); } loadNext(); }); }实际运行中,500MB文件在普通PC上大约需要5到8秒完成hash计算。如果觉得这个时间太长,优先考虑把它搬进Web Worker,而不是降低hash计算的准确性。Web Worker里不能直接访问File对象,需要先把文件传到worker里,但spark-md5在worker里运行完全没问题,UI线程不会卡。
4.2 分片并发上传与秒传判定流程
整个上传流程走的是状态机模式。每次用户选中文件,先计算hash,然后做秒传判定。判定命中就直接完成;未命中就进入分片上传,中间随时可以取消、暂停、续传。
export async function uploadFile(file, options) { const { checkUrl, uploadUrl, mergeUrl, chunkSize = 5 * 1024 * 1024 } = options; // 第一步:计算文件hash const fileHash = await calcFileHash(file, (p) => { options.onHashProgress && options.onHashProgress(p); }); // 第二步:秒传判定 const checkRes = await axios.post(checkUrl, { fileHash, fileName: file.name, fileSize: file.size }); if (checkRes.data.data && checkRes.data.data.exist) { options.onSuccess && options.onSuccess({ quick: true, url: checkRes.data.data.url, fileHash }); return; } // 第三步:分片上传与断点续传 const totalChunks = Math.ceil(file.size / chunkSize); let uploadedChunks = await getUploadedChunks(fileHash); // 查询已上传分片 const needUploadChunks = []; for (let i = 0; i < totalChunks; i++) { if (!uploadedChunks.includes(i)) { needUploadChunks.push(i); } } await runWithConcurrency(needUploadChunks, 3, async (index) => { const start = index * chunkSize; const end = Math.min(start + chunkSize, file.size); const formData = new FormData(); formData.append('file', file.slice(start, end)); formData.append('fileHash', fileHash); formData.append('index', index); formData.append('total', totalChunks); formData.append('fileName', file.name); await axios.post(uploadUrl, formData, { headers: { 'Content-Type': 'multipart/form-data' } }); }); // 第四步:触发服务端合并 await axios.post(mergeUrl, { fileHash, fileName: file.name, totalChunks }); options.onSuccess && options.onSuccess({ quick: false, fileHash }); }并发控制函数我用一个手写的runWithConcurrency,简洁直观:
async function runWithConcurrency(tasks, limit, worker) { const queue = tasks.slice(); let running = 0; return new Promise((resolve, reject) => { function next() { if (queue.length === 0) { if (running === 0) resolve(); return; } while (running < limit && queue.length > 0) { const task = queue.shift(); running++; worker(task) .then(() => { running--; next(); }) .catch((err) => { reject(err); }); } } next(); }); }这个写法的好处是控制明确,出错时可以整体退出,也可以按需求改成单个分片重试几次。
4.3 vue3 UI层的组件实现与jQuery侧调用方式
vue3这边的UI我封装成一个UploadPanel组件,负责展示文件列表、进度条和状态信息。它只通过props接收配置,通过事件向外发通知,完全不关心外层是jQuery还是vue3。
<template> <div class="upload-panel"> <div v-for="task in taskList" :key="task.fileHash" class="task-item"> <div class="task-name">{{ task.fileName }}</div> <div class="task-progress"> <div class="progress-bar" :style="{ width: task.progress + '%' }"></div> </div> <div class="task-status">{{ task.statusText }}</div> </div> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import { Uploader } from '../utils/uploader.js'; const taskList = ref([]); function addFile(file) { const task = { fileName: file.name, fileHash: '', progress: 0, statusText: '等待开始' }; taskList.value.push(task); Uploader.calcFileHash(file, (p) => { task.fileHash = uploadState.fileHash; task.progress = Math.round(p * 100); task.statusText = '计算文件指纹中'; }); uploadFile(file, { checkUrl: '/api/file/hash/check', uploadUrl: '/api/file/upload', mergeUrl: '/api/file/merge' }).then((res) => { task.statusText = res.quick ? '秒传成功' : '上传成功'; task.progress = 100; }); } defineExpose({ addFile }); </script>注意组件里没有iframe相关的代码,这里截图的是简化版。实际使用中,如果jQuery页面是普通script引入方式,需要通过window.$uploader把addFile方法暴露出去:
// 启动vue3模块时,挂载方法到window import { createApp } from 'vue'; import UploadPanel from './UploadPanel.vue'; export function mountUploadModule(selector) { const app = createApp(UploadPanel); const instance = app.mount(selector); window.$uploader = { addFile: (file) => instance.addFile(file), getTaskList: () => instance.taskList }; return instance; }jQuery侧在原来的文件选择事件里调用:
$('#btn-select-file').on('change', function (e) { const file = e.target.files[0]; if (window.$uploader) { window.$uploader.addFile(file); } else { // 降级到老上传逻辑,或提示模块尚未加载完成 } });这样老页面只是新增了一行调用,原来绑定在按钮上的其他jQuery事件完全不受影响。
4.4 后端接口约定的建议写法与伪代码
前端做完,后端如果不知道按什么规范接,整个方案也跑不起来。这里给出一个后端接口的伪代码约定,用Node.js风格示意,逻辑同样适用于Java、Go等其他语言。
// POST /api/file/hash/check // request body: { fileHash, fileName, fileSize } // response: { code: 0, data: { exist: true, url: '/files/xxx.zip' } } async function checkHash(req, res) { const { fileHash, fileSize } = req.body; const record = await db.findFileByHash(fileHash); if (record && record.size === fileSize) { res.json({ code: 0, data: { exist: true, url: record.url } }); } else { res.json({ code: 0, data: { exist: false } }); } }// POST /api/file/upload // multipart/form-data 里带 file 文件本体和其他字段 async function uploadChunk(req, res) { const { fileHash, index, total, fileName } = req.body; const file = req.file; const tmpPath = path.join(tmpDir, `${fileHash}_${index}.part`); await fs.promises.writeFile(tmpPath, file.buffer); // 记录分片索引到redis或db,方便断点续传查询 await redis.sadd(`upload:${fileHash}:chunks`, index); res.json({ code: 0 }); }// POST /api/file/merge async function mergeChunks(req, res) { const { fileHash, fileName, totalChunks } = req.body; const chunkList = []; for (let i = 0; i < totalChunks; i++) { chunkList.push(path.join(tmpDir, `${fileHash}_${i}.part`)); } // 必须按index顺序合并 const writeStream = fs.createWriteStream(finalPath); for (const chunkPath of chunkList) { const data = await fs.promises.readFile(chunkPath); writeStream.write(data); await fs.promises.unlink(chunkPath); // 合并后删除分片 } writeStream.end(); await db.createFileRecord({ fileHash, url: finalUrl, size: stat.size }); res.json({ code: 0, data: { url: finalUrl } }); }后端一个关键点是分片临时文件的清理策略,防止用户上传到一半放弃,临时文件成了垃圾。我一般会在记录里加上创建时间,定时任务清理超过24小时且未合并的分片。
5. 踩坑实录与排查技巧
5.1 秒传判定成功,但下载下来的文件是空的
这个坑让我排查了一个下午。现象是hash判重接口返回exist:true,前端提示秒传成功,但用户打开文件发现是0字节。
最后查下来是服务端判重逻辑的问题:分片合并完成后,服务端只是写了一条文件记录,但物理文件因为存储路径配置错误没有真正落盘。判重接口却只看数据库记录,于是返回了错误的存在标记。解决方法是判重接口必须同时比对文件在存储系统中的真实状态和文件大小,不能只查记录。前端侧也要加一层体验保障——秒传成功后如果用户立即访问,接口返回的文件地址必须能正常打开,等价于增加一次可用性探测。
5.2 jQuery的contentType坑:FormData被转成字符串
这个坑专门针对混合开发场景。老项目里有很多用$.ajax发请求的代码,如果你在jQuery里写上传分片,很容易想当然:
$.ajax({ url: '/api/file/upload', method: 'POST', data: formData, success: function () {} });这个写法看起来没问题,实际上jQuery默认的contentType是application/x-www-form-urlencoded; charset=UTF-8,把FormData强行转成了表单字符串,后端拿不到文件。正确的写法必须显式设置两个选项:
$.ajax({ url: '/api/file/upload', method: 'POST', data: formData, processData: false, // 不要处理data contentType: false, // 让浏览器自动设置multipart boundary success: function () {} });如果你在vue3侧用axios,默认没这个问题;但混合开发里很容易出现“vue3里能传,切到jQuery页就传不上去”,90%都是这个原因。我后来为了统一,把上传请求全部走uploader.js里的XHR封装,不再让jQuery经手上传逻辑。
5.3 hash计算期间浏览器假死
大文件全量计算hash确实吃CPU,如果直接在主线程跑,文件一大页面就完全卡住,用户点哪都没反应。我的解决方案是把hash计算丢到Web Worker里。
Web Worker里接收整个File对象不行,但可以接收ArrayBuffer分片。主线程负责按块读取文件,然后用postMessage传给worker,worker里调用SparkMD5追加,算完再把结果传回主线程。这样UI线程几乎无感,期间还可以正常滚动页面、点击按钮。
如果项目里不方便起Worker,也可以用requestIdleCallback把计算片段拆散到浏览器空闲时段执行,但效果比Worker差一截,高峰期还是会卡。建议有条件直接上Worker。
5.4 并发上传后合并出来的文件偶尔损坏
文件合并损坏的检查方法很简单:合并完成后用hash工具重新算一遍最终文件的hash,和前端算出来的fileHash比对,不一致就是合并有问题。
常见的合并问题有两个。第一个是分片顺序错乱,服务端没有按index排序就拼接内容;第二个是合并过程中还在接收新的上传分片,文件被并发写入行为弄脏。我的处理方式是在merge接口里做两层保护:第一层,合并前检查临时目录下的分片数量是否等于totalChunks,数量不对直接拒绝合并;第二层,合并期间对同一个fileHash加锁,防止重复合并或边传边合。这样处理后,文件损坏的问题就再没出现过了。
5.5 vue3挂载区域导致老jQuery事件失效
老页面里如果某个按钮点击后动态往上传区域塞DOM,或者用了html()方法替换节点,很容易把vue3挂载出来的DOM给换掉,导致元素虽然看着还在,但事件绑定全没了。
这是因为vue3的虚拟DOM会维护自己的一套节点引用,jQuery直接改DOM结构,vue3完全感知不到,两边就产生了不一致。我的处理方法是立一条规矩:凡是被vue3接管的DOM区域,老代码一律不许直接操作,需要改数据就用暴露出来的方法更新。vue3区域的DOM增删、属性修改都交给vue3响应式系统。同时,老页面如果要销毁上传区域(比如切换菜单),不要用remove()直接删节点,而是调vue3实例的unmount方法,这样才能保证资源正确释放。
5.6 断点续传查询接口要谨慎处理
断点续传需要前端在重新上传时查询哪些分片已经传过。这个接口的数据准确性和性能非常重要,如果查询结果返回了已上传但实际不存在的分片,就会导致最终合并时缺块。
服务端在返回已上传分片列表时,最好同时校验分片文件是否还物理存在。也就是说,Redis或数据库里记录的分片索引只能作为参考,真正的判定标准是临时目录里能不能找到对应的.part文件。我在排查阶段加过一个检测逻辑:凡是记录存在但文件不存在的分片,统一标记为未上传,重新传一遍。这样虽然多传了几个分片,但至少最终文件完整,不会因为脏数据导致合并失败。
写在最后
这套jQuery与vue3混合开发的大文件秒传方案,目前已经在我们几个老后台页面稳定跑了两个月,累计上传文件总量超过1TB,秒传命中率大概在30%左右——团队内部经常传同一批源文件,这个比例带来的带宽节省已经非常可观。
技术上踩过的坑不少,但回过头看,核心就是两条:一是上传逻辑绝不和UI框架绑死,纯JS模块是混合开发最稳妥的底座;二是两套框架共存时,边界要划清楚,各自管好各自的DOM和事件,不要越界操作。
最后分享一个我个人的小偏好:一切追求“秒传”的功能,都要做好异常降级,不要因为秒传失败就阻断整个上传流程。我在代码里给秒传判定加了一个容错开关,判重接口超时或报错时自动降级为分片上传,虽然慢一点,但用户的文件绝不会因为一个优化功能而传不上去。