资讯动态

Vue2大文件上传实践:WebUploader分片上传与进度可视化

发布时间:2026/10/6 9:33:12 来源:尧图企业网站定制
做 Vue2 老项目的人迟早会碰上大文件上传这种需求。我之前维护过一个内部管理系统用户最爱传的就是几十兆甚至几百兆的 Excel 和视频。一开始用原生 form 和 axios 硬顶结果就是页面卡死、进度条根本不走、偶尔传了十几分钟报个超时。后来我把百度WebUploader插件接进来配合 Vue2 做了完整的上传进度可视化才彻底把这块体验捞回来。这篇文章就围绕这个方案展开讲清楚 WebUploader 的核心机制、在 Vue2 里的封装方式、进度 UI 的实现思路以及我实际踩过的几个坑适合正在维护 Vue2 老项目、想快速接手大文件上传模块的开发者参考。1. 为什么大文件上传必须换一种思路分片、并发与断点续传1.1 一次性整传的问题到底出在哪大文件到底多大我的经验是超过 50MB 就该认真对待超过 200MB 基本不能指望一次请求搞定。原因很直白浏览器把整个文件作为 body 发送服务器也得一次性接完。网络稍差中途断连前端最多显示一个 error已传的字节全浪费了。有些网关还会在 Nginx 层设置超时时间比如 60s文件一大必然超时。更麻烦的是用户体验原生 XMLHttpRequest 虽然有 progress 事件但遇到网络波动时无法重传用户对着一个卡死的百分比只能干等。所以在老项目里上传大文件需要的不是单个请求而是一整套分片、并发、重试、断点续传的策略。这也是我最终选择 WebUploader 的原因。1.2 WebUploader 给出的解法WebUploader 是百度 FEX 团队维护过的插件它做的核心事情就是把一个文件切成多个小块再通过队列调度并发上传。下面这张表可以比较直观地对比普通整传和 WebUploader 分片上传的差异关键维度普通整传WebUploader 分片上传单次请求数据量整个文件一个 2MB~10MB 的小片失败成本全部重传只重传失败的那一片并发控制无单请求串行线程池式并发threads 可调进度展示只有整体 progress 事件分片粒度 整体粒度双重进度断点续传不支持配合后端可恢复未完成的文件它的内部原理其实不神秘。现代浏览器用Blob.slice()把文件按固定字节数切开每个分片就是一个独立的请求对象为了不让请求顺序乱掉又用了一个异步队列来管理哪些片在传、哪些片在等、哪些片失败要重试。再加上uploadProgress事件不断上报当前文件的已上传字节前端拿到这些数据后自然就能画出一个可信的进度条。1.3 分片上传对进度可视化的意义如果你自己写分片上传大概是这样const chunkSize 2 * 1024 * 1024; for (let start 0; start file.size; start chunkSize) { const chunk file.slice(start, start chunkSize); // 把这个 chunk 发到后端 }WebUploader 帮你把这些细节包装好了但它暴露给前端的事件仍然是文件维度的进度。也就是说你不需要自己去累加每个分片的上传字节数它会在uploadProgress事件里直接给你一个 0~1 的百分比。这个百分比的计算逻辑就是已上传的总字节数 / 文件总字节数。理解这一点之后后面做进度可视化才不会被百分比突然跳一截的现象吓到——因为分片是离散上传的每完成一片百分比就会有一个台阶式的跳跃这很正常。2. 在 Vue2 项目里接入 WebUploader一个最小可用封装2.1 依赖与资源为什么绕不开 jQueryWebUploader 插件本身依赖 jQuery官方版本建议用 jQuery 1.x所以接入前得先把 jQuery 引进来。在老项目里我不太推荐用 npm 安装 webuploader因为官方包已经很久没更新npm 上的版本来源也比较杂。更稳妥的办法是把静态资源放进项目的 static 目录或 public 目录然后在入口页面直接引入static/ jquery-1.12.4.min.js webuploader/ webuploader.css webuploader.js Uploader.swf!-- index.html 或 public/index.html -- script src/static/jquery-1.12.4.min.js/script link relstylesheet href/static/webuploader/webuploader.css script src/static/webuploader/webuploader.js/scriptCSS 别漏掉否则文件选择按钮的外部样式、进度条框架都会是默认效果后面自己调样式反而更麻烦。如果你用的是 Vue CLIpublic 目录下的文件会映射到根路径/static/...这种引用方式在绝大多数场景下都是可用的。2.2 封装 Uploader 组件初始化、事件绑定与销毁在 Vue2 中我习惯把上传能力封装成一个独立组件而不是散落在业务页面里。这样上传逻辑可以复用后续改样式或加功能也方便。下面是一个最小可用的组件结构template div classbig-uploader div classpick-btn idfilePicker选择文件/div div v-foritem in fileList :keyitem.id classfile-item div classfile-info span{{ item.name }}/span span{{ item.sizeText }}/span span{{ item.statusText }}/span /div div classprogress div classprogress-bar :style{ width: item.percent % }/div /div div classprogress-tip{{ item.percent.toFixed(1) }}%/div /div /div /template script export default { name: BigFileUploader, data() { return { fileList: [], }; }, mounted() { this.initUploader(); }, beforeDestroy() { if (this.uploader) { this.uploader.destroy(); } }, methods: { initUploader() { const uploader (this.uploader WebUploader.create({ swf: /static/webuploader/Uploader.swf, server: /api/upload, pick: #filePicker, accept: { title: Files, extensions: xlsx,xls,mp4,zip, mimeTypes: application/vnd.ms-excel,video/mp4,application/zip }, fileVal: file, chunked: true, chunkSize: 2 * 1024 * 1024, threads: 3, duplicate: true, formData: { token: this.getToken() } })); uploader.on(fileQueued, (file) { this.fileList.push({ id: file.id, name: file.name, sizeText: this.formatSize(file.size), percent: 0, statusText: 等待上传 }); }); uploader.on(uploadProgress, (file, percentage) { const target this.fileList.find((item) item.id file.id); if (target) { target.percent percentage * 100; } }); uploader.on(uploadSuccess, (file) { const target this.fileList.find((item) item.id file.id); if (target) target.statusText 上传成功; }); uploader.on(uploadError, (file, reason) { const target this.fileList.find((item) item.id file.id); if (target) { target.statusText 上传失败 reason; } }); uploader.on(uploadComplete, () { // 所有文件处理完成 }); }, formatSize(bytes) { if (bytes 1024) return bytes B; if (bytes 1024 * 1024) return (bytes / 1024).toFixed(1) KB; return (bytes / 1024 / 1024).toFixed(1) MB; }, getToken() { return window.localStorage.getItem(upload_token) || ; } } }; /script这里有几个关键点要特别说明提示this.uploader WebUploader.create(...)中的uploader属性不要写在data()里。Vue2 会把 data 对象遍历并改造成响应式而 WebUploader 实例内部有大量方法放进响应式系统里既浪费性能又容易出现某些内部属性被监听后发生异常的情况。直接在mounted里给this挂一个普通属性就好。组件销毁时调用this.uploader.destroy()也很重要否则页面反复切换时WebUploader 注册的 DOM 事件会残留导致内存泄漏。2.3 服务端配合要点别让前端白干如果你的后端接口不支持分片参数前端把 chunked 打开也没有意义。WebUploader 在分片模式下会往请求里带上这些关键字段文件名、文件大小、当前分片索引、总分片数、文件唯一标识等。后端需要能根据这些信息把分片文件暂存最后一块传完后合并返回最终文件 URL。所以我建议的落地顺序是先和后端确认接口协议再动前端页面。否则前端进度做到 99%最后一刻因为后端合并失败而报错用户看到的还是失败弹窗。3. 把进度“画”出来从事件回调到可视化 UI3.1 uploadProgress 到底给你了什么WebUploader 的uploadProgress事件在分片模式下不是平滑递增的而是每完成一个分片后突然跳一截。事件回调参数是(file, percentage)其中percentage是 0~1 的小数。你直接把它乘以 100 就能作为进度条的宽度百分比。有些场景下你还需要知道当前已上传的字节数可以用file.size * percentage算出来。不过要注意上传之前如果需要计算 MD5 做秒传或断点续传那段时间uploadProgress不会触发进度条会停在那里不动。这个阶段要单独设计文件分析中的 UI不能让用户以为卡死了。3.2 多文件队列和整体进度怎么算如果用户一次选择多个文件每个文件都会进入队列并各自触发uploadProgress。这时候如果你想在页面顶部展示一个整体上传进度不能简单把所有文件的百分比平均。正确做法是用字节加权calcTotalPercent(fileList) { const totalSize fileList.reduce((sum, f) sum (f.size || 0), 0); const loaded fileList.reduce((sum, f) sum (f.size || 0) * (f.percent / 100), 0); return totalSize ? Math.min(100, (loaded / totalSize) * 100).toFixed(1) : 0; }这段代码里的fileList需要保存每个文件的原始 size而不是只保存格式化之后的字符串。我在 2.2 的组件里用formatSize展示文件大小但内部计算时还是要用数字类型的size字段。像这种细节等做到多文件上传时才会意识到。3.3 上传速度与剩余时间的计算细节很多上传组件只显示百分比其实加上实时速度和剩余时间才是真正让人安心。速度不能直接用当前进度除以总用时因为网络波动会导致结果忽快忽慢。我常用的做法是用最近一段时间内的字节增量来估算瞬时速度再做一次平滑处理data() { return { lastLoadedBytes: 0, lastTime: 0, smoothSpeed: 0 }; }, methods: { onProgress(file, percentage) { const loaded file.size * percentage; const now Date.now(); const diffTime now - this.lastTime; if (diffTime 300) { const speed (loaded - this.lastLoadedBytes) / (diffTime / 1000); // B/s this.smoothSpeed this.smoothSpeed ? this.smoothSpeed * 0.7 speed * 0.3 : speed; this.lastLoadedBytes loaded; this.lastTime now; } } }diffTime如果太短算出来的速度容易抖动大于 300ms 再刷新一次比较稳定。剩余时间则用(file.size - loaded) / this.smoothSpeed计算注意速度为零时给出一个占位符比如“计算中”避免出现 Infinity 或负数。3.4 避免进度刷新把页面搞卡节流与局部更新一次拖入多个大文件时uploadProgress事件会非常密集。如果每次事件都去更新 Vue2 data 里的对象属性整个文件列表都会被重新渲染页面帧率会明显下降。我在实际项目里做了三层防护设置一个 200ms 的节流窗口只有超过这个时间才更新fileList中的数据。百分比变化小于 1% 时跳过更新因为视觉上根本看不出区别。进度条颜色变化和宽度变化用 CSS transition 过渡避免出现突变抖动。const now Date.now(); const oldPercent target.percent; const newPercent percentage * 100; if (now - this.lastRenderTime 200 Math.abs(newPercent - oldPercent) 1) { target.percent newPercent; this.lastRenderTime now; }如果你还想更极致可以直接操作 DOM绕开 Vue 的响应式渲染但这样会增加维护成本。我的建议是先用节流方案因为 200ms 的间隔对用户来说已经很流畅了。4. 实战中的坑与调优从“能传”到“传得稳”4.1 回调里的 this 与 Vue 响应式坑WebUploader 的 on 回调中如果使用普通函数回调里的this指向 WebUploader 实例不是 Vue 组件。我在代码里全都用了箭头函数确保this指向组件实例。另外很多人习惯把 WebUploader 实例放到data()里结果发现组件变得异常卡顿这是因为 Vue 会给它做响应式劫持。正确做法是挂在组件实例的非响应式属性上就像前面代码注释里写的那样。还有一个容易被忽视的问题fileList中每个对象只保存需要展示的字段不要直接把 file 原对象 push 进去。file 原对象里有大量内部状态被 Vue observe 之后每次 progress 更新都可能触发更深层次的变更检测拖慢性能。4.2 动态 token 与参数更新大多数后台系统的上传接口都需要携带 token而 token 在长时间停留后可能会过期。WebUploader 初始化时写入formData.token只对初始化那一刻有效。如果用户从选择文件到真正点击上传间隔了很久token 可能已经失效。我的处理方式是在before-send-file钩子里统一更新uploader.on(before-send-file, (file) { uploader.options.formData.token this.getLatestToken(); return true; });这里return true表示继续上传返回false会中断。要注意before-send-file是整个文件开始上传之前触发如果你的 token 是在上传过程中刷新还需要在before-send钩子里再更新一次。分片上传场景下before-send还可以顺便把分片序号、文件 MD5 等字段塞进 formData服务端就能根据这些信息做分片校验和合并。4.3 弹窗中的 picker 失效问题如果你把选文件按钮放在 Element UI Dialog 或自己封装的弹窗组件里很容易遇到按钮第一次能点打开弹窗后再点没反应的故障。原因很简单弹窗关闭后 DOM 被 v-if 移除WebUploader 初始化时给 picker 绑定的 click 事件也跟着消失了。解决方案是弹窗每次打开后用this.$nextTick重新初始化 uploader如果组件已经被初始化先调用destroy()再重建。另一个更省事的做法是把选择按钮放在弹窗外部的常驻节点上弹窗内部只负责展示文件列表和进度条。这个方案可以避免重复初始化带来的资源浪费但需要你根据业务场景权衡。4.4 分片大小、并发数与失败重试的平衡我见过有人为了追求极致进度粒度把分片设成 256KB结果后端接口压力巨大整个上传反而更慢。分片大小和并发数需要根据文件大小和服务端能力来定。下面是我常用的推荐配置参数建议值说明chunkSize2MB~10MB小于 1MB 请求数过多大于 20MB 容易单分片超时threads3~5并发太高容易触发服务端限流或网络争抢retries3~5每个分片最多重试次数防止临时网络抖动导致失败retryTimeout30~60s分片超时时间根据实际网关超时配置调整如果你上传的文件只有几 MB完全可以关闭分片用普通整传反而更快。WebUploader 支持在创建时根据文件大小动态决定是否chunked但官方没有直接提供按大小自动设置的开关你需要在fileQueued事件里判断文件大小并修改上传配置。我一般用 100MB 作为阈值小于 100MB 不分片大于 100MB 开启 2MB 分片。这样做的好处是常规中小文件流程简单大文件又能享受分片带来的稳定性。5. 从“能用”到“好用”进阶体验和选型思考5.1 上传前的 MD5 计算把“分析中”变成进度条如果要做秒传或断点续传前端在上传前要算文件指纹。我推荐用spark-md5在通过 WebUploader 上传之前自己读取文件并计算顺便把计算进度显示出来import SparkMD5 from spark-md5; function calculateFileMd5(file, onProgress) { const spark new SparkMD5.ArrayBuffer(); const reader new FileReader(); const chunkSize 2 * 1024 * 1024; let loaded 0; reader.onload (e) { loaded e.target.result.byteLength; spark.append(e.target.result); const percent (loaded / file.size) * 100; onProgress(percent); if (loaded file.size) { readNext(); } else { onProgress(100); console.log(spark.end()); } }; const readNext () { const start loaded; const end Math.min(start chunkSize, file.size); reader.readAsArrayBuffer(file.slice(start, end)); }; readNext(); }拿到 MD5 之后可以先向后端发起一次秒传检查如果文件已经存在直接提示用户文件已存在不需要再上传。这一步和大文件上传的进度可视化是配套的因为 MD5 计算阶段也是用户感知的一部分不显示进度等于让用户看着屏幕发呆。5.2 暂停、续传与状态文案的交互细节WebUploader 原生支持uploader.stop()和uploader.upload()分别对应暂停队列和继续上传。我用它做过一个带暂停按钮的上传列表效果还不错。但要注意暂停后正在上传的分片会被打断已经上传完成的其它分片会保存在服务端临时目录里。恢复后WebUploader 会先向服务端确认哪些分片已经存在再继续传缺失的部分所以用户看到的进度不会从 0 重新开始。在 UI 上暂停状态需要单独展示否则用户点击暂停后发现进度条还在动会认为是 bug。我的做法是在文件对象上加一个status字段pending、uploading、paused、success、error渲染时根据status切换文案和颜色。这样比单纯显示一个百分比信息量大得多。5.3 新项目要不要换掉 WebUploader说句公道话如果你正从零开始搭 Vue3 项目我建议不要再用 WebUploader因为它依赖 jQuery和现代前端工程体系有些割裂。用Blob.slice()分片、XMLHttpRequest并发上传、Web Worker处理文件读取完全能实现同样甚至更可控的进度可视化。但对于 Vue2 老项目尤其是在线上已经跑了几年的项目强行重写上传模块风险很高WebUploader 仍然是性价比很高的选择。它的分片、并发、队列和进度事件设计放到今天依然没过时。我后来把这套方案原样迁移到了 Vue2 的另外两个项目里改动量非常小。核心经验就是进度可视化从来不只是画一根进度条而是要让用户在每个阶段都能看到系统正在正常工作——无论是计算 MD5、等待队列、上传分片还是重试失败块都要有明确的反馈。最后一个建议是动手前先和后端把分片合并、token 校验、断点续传的接口协议定清楚否则前端做得再花哨用户依旧会看到一个传完后失败的弹窗。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑