
做Vue大文件上传很多前端同学第一个反应是“这有什么难的直接axios post不就完事了”。真踩过坑的人都知道一个5GB的视频文件从网页传到服务器如果走常规的POST上传大概率会先后遇到后端限制请求体大小、内存扛不住、中途断网整包重来、浏览器直接崩溃这一整套连环打击。我这段时间正好在做一个视频素材管理的内部系统把Vue大文件跨平台上传的DEMO完整搭了一遍从分片、断点续传到秒传、并发控制都过了一遍今天把核心实现和踩坑记录整理出来给准备做同类需求的人一个可以直接抄作业的参考。这个DEMO的价值在于它把“大文件上传”从理论变成了一套可以在Windows、macOS、Linux以及移动端H5上稳定运行的完整方案。不管你是刚入门的Vue新手还是被大文件上传折磨过一段时间的进阶开发者下面这套分片上传断点续传秒传的组合拳都能直接套到你自己的项目里。1. DEMO的整体设计思路与功能拆解1.1 分片上传原理与后端路由设计大文件上传的核心思路其实特别简单把一个大文件切成很多小块一块一块传传完了再在服务端合并。这就好比搬家你不可能把整个衣柜原封不动从旧房扛到新房真正靠谱的做法是拆成抽屉、层板、柜门分别搬运到了新房再组装起来。分片上传解决的不只是后端请求体限制的问题更关键的是把“一次性的巨大网络风险”拆成了“很多次小的网络风险”——任何一片失败只需要重传那一片而不是整个文件重来。DEMO里我采用的分片逻辑是// 分片大小 5MB const CHUNK_SIZE 5 * 1024 * 1024; function createChunks(file: File) { const count Math.ceil(file.size / CHUNK_SIZE); const chunks []; for (let i 0; i count; i) { chunks.push({ index: i, blob: file.slice(i * CHUNK_SIZE, (i 1) * CHUNK_SIZE), total: count }); } return chunks; }file.slice()这个API几乎所有现代浏览器都支持它不会修改原文件只是返回一个引用原文件某段数据的新Blob对象。所以我们不管切多少片都不会额外增加内存开销这一点很关键。后端接口设计上我规划了三个接口方法作用/upload/chunkPOST接收单个分片/upload/mergePOST合并所有分片/upload/status/:hashGET查询已上传分片用于续传和秒传这三个接口就是整个上传流程的骨架。前端根据文件内容算出唯一标识哈希先把所有分片传上去传完后调用合并接口后端把所有分片按顺序拼成完整文件。你可能会问为什么要按哈希来组织文件目录因为哈希是文件内容的唯一指纹同样的文件传两次哈希一样可以直接走秒传逻辑不会在服务器上留下两份副本。1.2 断点续传与秒传的实现思路断点续传和秒传经常被放在一起说但其实是两个不同层面的能力。断点续传的本质是上传之前先去后端查询一下哪些分片已经传过了然后只传缺失的部分。比如一个文件被切成100片上次传到第67片断了下次续传时先问后端“这个文件已经有哪些片了”后端把已存在的分片编号返回前端跳过这些片直接从68片开始传。这个流程不需要前端本地保留任何状态完全靠服务端已落盘的分片来判定比较干净。秒传的逻辑是如果后端发现这个文件的所有分片都已经存在直接跳过上传过程调用合并接口或者直接返回一个“已存在”的结果。用户体感就是“瞬间传完”。DEMO里秒传和断点续传共用同一个状态查询接口区别只在于查询结果里已上传分片的数量是多少。如果等于总分片数就走秒传如果介于0和总数之间就走续传如果等于0就是全新的上传。我把这段判断逻辑抽成了一个getUploadStatus函数后面在Vue组件里直接用async function judgeUpload(chunks: any[], fileHash: string) { const res await fetch(/upload/status/${fileHash}); const data await res.json(); // 已上传的分片编号集合比如 [chunk-0, chunk-3] const uploadedSet new Set(data.uploaded); const needUpload chunks.filter(chunk !uploadedSet.has(chunk-${chunk.index})); return { needUpload, complete: chunks.length uploadedSet.size, uploadedSet }; }这里有个非常重要的边界场景容易忽略后端返回的“已上传分片”状态并不是百分百可信的——比如某个分片上次传了一半服务器还没写完或者磁盘满了落盘失败。所以续传之后最终合并前必须对全部分片做完整性校验不能后端说有就真信了这一点在第4章合并接口里会有详细说明。1.3 技术选型和跨平台考虑我做DEMO时前端选用的是Vue 3 TypeScript Vite后端选的是Node.js Express。为什么这么组合首先DEMO的核心目的是验证上传逻辑不是验证后端的高性能所以用一个轻量Node服务就够了Express处理分片接收和文件合并都非常直接。其次前后端都是JS生态共享一个哈希算法实现会方便很多不用像Java后端那样重新写一遍计算逻辑。跨平台这层考虑其实主要体现在三个方面前端浏览器兼容分片用的slice、并发用的Promise都是ES2015能力在Chrome、Firefox、Safari、Edge的最新版本上都能跑移动端H5在iOS Safari和Android Chrome上也验证过没问题。后端环境兼容Node.js服务天然跨平台Windows / macOS / Linux上跑同一套代码文件路径处理需要注意一下。Node的path模块处理过这个问题建议代码里不要自己拼/或\统一用path.join。大文件场景兼容DEMO里对所有超过2GB的文件做了内存保护比如前端读取文件元数据时用File.size而不是把整个文件读进内存后端合并时用流式写入而不是一次性读入内存这两个细节是保证跨平台大文件上传不崩溃的关键。2. 环境准备与项目骨架搭建2.1 Vue项目初始化与依赖安装如果你是从零开始搭这个DEMO环境准备大概十分钟左右能搞定。Vue 3项目用Vite创建最快npm create vitelatest vue-upload-demo -- --template vue-ts cd vue-upload-demo npm install npm install axios spark-md5 npm run dev这里有两个必装依赖axios用来发上传请求比原生fetch好在两点一是能自动处理进度事件上的浏览器差异二是取消请求时接口更友好。spark-md5用来计算文件内容的哈希值它是纯JS实现浏览器端直接能用不需要后端参与计算。这里解释一下为什么选spark-md5而不是用浏览器原生的crypto.subtle.digest。原生API虽然能算SHA-256但有两个麻烦一是函数是异步的处理大内存时表现不稳定二是实现细节在HTTP非安全语境下不可用比如localhost之外的普通HTTP地址会直接不能用。spark-md5支持增量计算可以逐片读入文件分片内存占用非常平稳大文件场景下更可靠。整个项目结构我建议分成三层src/ ├── components/ │ └── Uploader.vue // 上传界面组件 ├── composables/ │ └── useUploader.ts // 上传核心逻辑切片、并发、状态管理 └── api/ └── upload.ts // 接口封装分片、合并、状态查询组件层只负责UI展示和用户交互所有上传逻辑都收敛在useUploader.ts组合式函数里这样后续想在别的页面复用上传能力直接调useUploader就行不用再复制一片逻辑。2.2 后端服务搭建后端部分我建了一个独立的server目录和前端项目分开这样以后想换Java或Go写后端前端完全不用动。mkdir server cd server npm init -y npm install express multer fs-extra corsmulterExpress里处理multipart/form-data请求的中间件分片上传用它来接。fs-extra一个加强版的文件系统模块里面有ensureDir这种“目录不存在就自动创建”的便利方法做分片目录管理很省心。cors解决跨域问题。我做DEMO时前端跑在5173端口后端跑在3000端口没有这个中间件浏览器会直接拦截所有请求。后端入口文件非常简陋但信息密度高const express require(express); const multer require(multer); const fs require(fs-extra); const cors require(cors); const path require(path); const app express(); app.use(cors()); const UPLOAD_DIR path.resolve(__dirname, uploads); const upload multer({ dest: path.join(UPLOAD_DIR, temp) }); app.use(/static, express.static(path.join(UPLOAD_DIR, files)));UPLOAD_DIR是上传文件的根目录里面再分两个子目录temp放分片暂存文件files放合并后的完整文件。static中间件把files目录暴露成静态资源合并完成后前端直接拼接URL就能访问上传好的文件不用再单独写下载接口。3. 前端核心逻辑的实现3.1 文件分片与增量哈希计算DEMO里我做了两个版本的哈希计算一个用于小文件一个用于大文件。小文件比如几十MB直接一次性读取计算代码简单import SparkMD5 from spark-md5; function calcHashSimple(file: File): Promisestring { return new Promise((resolve, reject) { const reader new FileReader(); reader.onload e { const spark new SparkMD5.ArrayBuffer(); spark.append(e.target?.result as ArrayBuffer); resolve(spark.end()); }; reader.onerror reject; reader.readAsArrayBuffer(file); }); }大文件就不能这么干了。一个5GB的文件直接读进内存浏览器连挣扎的机会都没有就会崩掉。我改成分片增量计算只取每个分片的首尾各2MB做哈希这样既保证了哈希的相对唯一性又不会把整个大文件塞进内存。function calcHashWithChunks(file: File, chunkSize 5 * 1024 * 1024): Promisestring { return new Promise((resolve, reject) { const spark new SparkMD5.ArrayBuffer(); const totalChunks Math.ceil(file.size / chunkSize); let currentChunk 0; // 注意只计算每个分片的前2MB加后2MB const processNext async () { if (currentChunk totalChunks) { resolve(spark.end()); return; } const start currentChunk * chunkSize; const end Math.min(start chunkSize, file.size); // 抽样区间每片的开头2MB 结尾2MB const head file.slice(start, Math.min(start 2 * 1024 * 1024, end)); const tailStart Math.max(start, end - 2 * 1024 * 1024); const tail tailStart start ? file.slice(tailStart, end) : null; const buffer await head.arrayBuffer(); spark.append(buffer); if (tail) spermApend(tail, spark); currentChunk; processNext(); }; processNext(); }); } async function spermApend(blob: Blob, spark: SparkMD5.ArrayBuffer) { const buf await blob.arrayBuffer(); spark.append(buf); }这里有个工程上的取舍要说明一下严格的“文件内容哈希”应该把每一个字节都算进去。但大文件全部算一遍耗时很长而且没必要——因为绝大多数场景下文件名相同、大小相同、抽样哈希相同的两个文件基本可以断定是同一个文件。如果你的业务对准确性要求极高比如医疗影像、法律证据可以强制全量哈希代价是计算耗时按文件大小线性增长。这块按业务需求取舍就行。3.2 并发控制给上传加个限流闸门分片数量多了以后一股脑全部发出去服务器会直接被打蒙。比如一个500MB的文件切100个分片并发数开满后端一个小Node服务就得卡死。我封装了一个通用的并发控制函数原理是用一个Set维护当前正在执行的任务超过上限就等待最早完成的一个。async function runWithConcurrency(tasks: (() Promiseany)[], limit 3) { const results: any[] []; const executing new SetPromiseany(); for (const task of tasks) { const p Promise.resolve().then(task).then(r { results.push(r); executing.delete(p); }); executing.add(p); if (executing.size limit) { await Promise.race(executing); } } await Promise.all(executing); return results; }并发数为什么设3而不是10这跟网络环境和服务器能力都相关。我做DEMO时本地局域网测试并发3和并发10差别不明显但放到公网测试机上并发10会经常触发TCP连接超时。经验值普通云服务器建议3~5并发带宽大的服务器可以放宽到6~8。这个参数建议做成可配置项别写死。3.3 进度收集与状态管理大文件上传的进度条逻辑不能像普通上传那样做简单的“已传字节/总字节”。因为有断点续传的存在进度应该拆成两块单个分片的上传进度每个分片内部实际传输了多少数据。整体上传进度所有分片中已成功上传的比例。我用一个reactive对象来管理全部状态const state reactive({ fileName: , fileSize: 0, uploadedChunks: 0, totalChunks: 0, speed: 0, // 当前网速 MB/s progress: 0, // 0 ~ 100 status: idle, // idle | uploading | success errorMsg: });计算整体进度的时候不能简单用已传分片数/总分片数乘100——因为每个分片大小相同这样算没问题但如果你改了分片大小配置或者某个分片需要重试多次进度就会回跳。我在DEMO里的做法是每次分片上传成功累加一个successBytes变量用真实的字节数除以文件总大小这样进度永远是单调递增的。速度计算比较粗糙但够用let lastBytes 0; let lastTime Date.now(); setInterval(() { const now Date.now(); const delta speedBytes - lastBytes; const timeSpan (now - lastTime) / 1000; state.speed delta / timeSpan / 1024 / 1024; lastBytes speedBytes; lastTime now; }, 1000);注意这个setInterval要在上传结束时及时清理不然组件卸载后还在跑会白白消耗性能。3.4 组件的完整拼装把所有逻辑串起来Uploader.vue组件核心部分长这样template div classuploader input typefile changeonFileChange / div v-ifstate.fileName p文件名{{ state.fileName }}{{ formatSize(state.fileSize) }}/p p上传进度{{ state.progress.toFixed(2) }}%/p p当前网速{{ state.speed.toFixed(2) }} MB/s/p p状态{{ state.status }}/p progress :valuestate.progress max100/progress button clickstartUpload :disabledstate.status uploading开始上传/button button clickcopyResultUrl v-ifstate.status success复制文件URL/button /div /div /template script setup langts import { ref, reactive } from vue; import { useUploader } from ../composables/useUploader; const { state, startUpload, handleFileChange } useUploader(); function onFileChange(e: Event) { const input e.target as HTMLInputElement; if (input.files?.length) { handleFileChange(input.files[0]); } } function formatSize(size: number) { if (size 1024) return size B; if (size 1024 * 1024) return (size / 1024).toFixed(1) KB; if (size 1024 * 1024 * 1024) return (size / 1024 / 1024).toFixed(1) MB; return (size / 1024 / 1024 / 1024).toFixed(2) GB; } /script组件的交互逻辑很简单选文件 → 自动开始哈希 → 点击“开始上传” → 跑完复制URL。实际项目里你可能想加“上传中暂停”和“失败重试”按钮这两个能力的底层都是基于断点续传的“查询已上传分片”能力本质上就是多调用一次状态查询接口然后决定是从第0片还是第N片开始代码扩展起来不复杂。3.5 上传主流程串起来useUploader.ts里的startUpload函数是整个前端的驾驶舱我把它完整贴出来async function startUpload() { if (!file.value) return; state.status uploading; // 1. 计算哈希抽样 state.hash await calcHashWithChunks(file.value); // 2. 切片 const chunks createChunks(file.value); state.totalChunks chunks.length; // 3. 查询状态 - 得到需要上传的分片 const { needUpload, complete } await judgeUpload(chunks, state.hash); if (complete) { // 秒传场景服务端已有完整文件 await mergeRequest(state.hash, file.value.name, chunks.length); state.progress 100; state.status success; return; } // 4. 构造上传任务数组只传缺失的分片 const tasks needUpload.map(chunk () uploadChunkRequest({ chunk, hash: state.hash }) .then(() { state.uploadedChunks; successBytes.value chunk.blob.size; state.progress (successBytes.value / file.value.size) * 100; }) ); // 5. 并发执行 await runWithConcurrency(tasks, 3); // 6. 合并 await mergeRequest(state.hash, file.value.name, chunks.length); state.status success; }这个流程大概是全部核心逻辑了。重点关注第6步所有分片传完之后必须等合并接口成功返回才能把状态置为success。有些同学喜欢分片传完就显示“上传成功”结果服务端合并报错用户以为传完了这是很常见的逻辑错误。4. 后端接口的实现与文件合并4.1 接收分片的实现细节后端接分片时最关键的坑是分片文件的命名和组织方式。我用文件哈希 分片索引的双层结构目录按哈希分文件按编号分app.post(/upload/chunk, upload.single(file), async (req, res) { const { index, hash } req.body; // multer 把上传的分片临时存到 uploads/temp 下 const chunkDir path.join(UPLOAD_DIR, chunks, hash); await fs.ensureDir(chunkDir); // 把临时文件移动到正式分片目录命名为 chunk-{index} const targetPath path.join(chunkDir, chunk-${index}); await fs.move(req.file.path, targetPath, { overwrite: true }); res.json({ code: 0, message: ok }); });有几个细节值得说道{ overwrite: true }这个参数很重要。断点续传场景下如果前端判断失误把一个已存在的分片又传了一遍multer会生成一个随机文件名再move。如果目标路径已存在默认会报“file already exists”加了这个参数会直接覆盖保证分片状态一致。这里没有对分片做“内容校验”因为代价太高。如果你对完整性要求极高可以让前端每个分片也传一个chunkHash后端用流式方式算一下MD5再比对费时候但稳妥。multer的默认dest临时目录必须和最终分片目录在同一个磁盘分区上因为fs.move在跨分区时会先拷贝再删速度慢而且容易残留临时文件。这个坑我在Windows开发机上踩过后面换到本地路径后就好了。4.2 合并接口流的正确打开方式合并接口是后端最容易出问题的地方。最粗暴的写法是把所有分片读入内存再拼接比如Buffer.concat在5GB文件面前直接内存溢出。正确姿势是使用流式写入app.post(/upload/merge, async (req, res) { const { hash, name, total } req.body; const chunkDir path.join(UPLOAD_DIR, chunks, hash); const targetDir path.join(UPLOAD_DIR, files); await fs.ensureDir(targetDir); const targetPath path.join(targetDir, ${hash}-${name}); const writeStream fs.createWriteStream(targetPath); for (let i 0; i total; i) { const chunkPath path.join(chunkDir, chunk-${i}); if (!fs.existsSync(chunkPath)) { writeStream.destroy(); return res.status(400).json({ code: 1, message: 缺少分片 ${i} }); } await pipeToStream(chunkPath, writeStream); } writeStream.end(); writeStream.on(finish, () { res.json({ code: 0, url: /static/${hash}-${name} }); }); }); function pipeToStream(filePath, writeStream) { return new Promise((resolve, reject) { const readStream fs.createReadStream(filePath); readStream.on(error, reject); readStream.on(end, resolve); // 注意需要用管道写法才能真正控制背压 readStream.pipe(writeStream, { end: false }); }); }这段代码的核心是{ end: false }。如果不加这个参数第一个分片管道完成后pipe会自动把writeStream关闭第二个分片就写不进去了。这是很多自写合并接口报“write after end”的原因。为什么合并时检查缺失分片返回400因为前面提过断点续传的状态查询接口可能不准确合并在所有分片“声称”上传完成后还不能直接相信这里是一个兜底校验。发现缺片就返回具体缺哪个片前端可以基于错误信息精准重传。一个性能细节pipeToStream是串行处理分片的没有并发。这是因为合并的目标文件只有一份并发写入同一个WriteStream会导致数据交错结果文件直接损坏。但你可以考虑另一种策略先把所有分片按顺序改成不同的临时合并文件再把它们串起来合并成最终文件——这样能利用并发但对磁盘IO压力更大。DEMO规模就用串行就好稳定优先。4.3 状态查询接口状态查询接口其实就是列目录app.get(/upload/status/:hash, async (req, res) { const chunkDir path.join(UPLOAD_DIR, chunks, req.params.hash); // 复查一下文件是否已经完整存在 const fileDir path.join(UPLOAD_DIR, files); const files await fs.readdir(fileDir).catch(() []); const matched files.filter(f f.startsWith(req.params.hash)); if (matched.length 0) { return res.json({ code: 0, uploaded: [], complete: true, url: /static/${matched[0]} }); } if (!fs.existsSync(chunkDir)) { return res.json({ code: 0, uploaded: [], complete: false }); } const uploaded await fs.readdir(chunkDir); res.json({ code: 0, uploaded, complete: uploaded.length /* 前端会把 total 传来这里可以直接读文件数 */ 0 }); });这里有一个细节我处理得比较保守状态查询接口同时检查了“完整文件是否已经存在”。如果文件已经合并过那么就算分片目录还在新上传时也直接返回文件已存在让前端走秒传逻辑不会再重复合并。否则会出现“同样的哈希重复合并文件覆盖”的隐患。5. 实操中的坑与常见问题排查5.1 请求体大小限制与超时设置大文件上传DEMO里最常见的翻车点就是后端框架自带的请求体限制。Express框架本身不限制请求体大小但用了body-parser之后默认会限制在100kb。分片虽然切小了但分片请求的body里如果包含了base64编码或其他扩展数据很容易超限。解决方案分两层只对上传分片的接口不限制body大小其他接口保持默认限制。用multer的时候注意它会自动处理multipart/form-data不走body-parser那套限制逻辑所以分片接口根本不需要手动调body-parser配置。另外前端axios的默认timeout是0不超时但部分打包构建环境或代理会设置超时。我这里在DEMO中显式关闭上传接口的超时uploadChunkRequest({ chunk, hash }) { return axios.post(/upload/chunk, formData, { timeout: 0, headers: { Content-Type: multipart/form-data } }); }timeout: 0的意思是永不超时因为大文件分片虽然不大但在弱网环境下传输一个5MB分片可能持续很久如果走默认超时用户会看到大量分片“假失败”然后反复重试。5.2 大文件的内存与磁盘管理这个坑在Windows上特别明显。开发环境下Express把上传文件先写入dest临时目录然后fs.move到分片目录。如果两个目录跨分区比如C盘到D盘会发生先拷贝再删除5GB文件最后会占用10GB的临时磁盘空间。解决方案是让UPLOAD_DIR下的temp和chunks都在同一个父目录内这样move就是纯重命名操作瞬时完成。生产环境里把上传目录做成一个独立挂载盘也方便做磁盘空间配额。内存方面前端已经做了分片读取后端合使用时也是流式处理理论上全链路的内存占用都和一个分片大小挂钩5MB。唯一需要留意的是spark-md5在计算哈希时如果你传的是ArrayBuffer而不是字符串它内部会持有引用计算过程的峰值内存大约等于抽样缓冲区大小和文件总大小无关所以放心用。5.3 跨平台兼容性细节跨平台这个点我在测试中发现几个有意思的细节Windows路径不区分大小写但Linux区分。所以分片索引命名如果混用大写小写在Windows上没问题一部署到Linux服务器就出现“找不到分片”的情况。DEMO里统一用chunk-${index}全小写命名避免踩这个坑。macOS和Linux的文件系统对某些特殊字符敏感。文件名里带:、?、*在Windows上直接创建失败。合并接口里对文件名做了白名单校验把非法字符替换成_。移动端H5上File.slice在iOS Safari有一个老bug切片时如果start和end相等返回的Blob是空数据。所以分片逻辑里加了一个判断如果end start跳过这个分片。5.4 断点续传失效的场景断点续传看着很美但有几个场景下实际会失效场景一服务器重启分片目录丢失。Demo里分片暂存目录没有做持久化方案重启就清理了。生产环境如果要求高可用得用对象存储OSS/S3或者给分片目录单独做持久化。场景二前端哈希算法和后端不一致。我遇到过一次这样的情况前端用spark-md5算出来的哈希后端Java用的MD5工具类算出来不一样导致状态查询永远为空断点续传形同虚设。这个问题的本质是哈希的输入数据可能不同——比如前端抽样计算了部分分片后端却按全量文件算。解决方式哈希统一在前端计算后端只负责把前端传来的哈希作为目录名使用不再重复计算。场景三并发控制导致的分片覆盖。并发数开大以后同一个分片可能被同时发起两次请求比如重试机制没写好后端收到两次相同的分片写入最后一次覆盖前一次。如果两次写入交错进行最终文件可能被写坏。解决方式后端分片写入时做互斥或者前端保证同一个分片同一时间只有一个请求在传。5.5 大文件上传的后台监控DEMO做到最后我加了一个半分钟级别的“上传任务监控”小模块。其实就是给后端加了一个GET /upload/tasks接口返回当前正在上传的文件哈希、分片进度、最后活跃时间。这个模块的价值在于一旦用户上传出了问题你在后台能立刻看到是卡在哪个文件的哪个分片上而不是用感知模糊来排查。这个功能在生产环境中相当有用但很多DEMO教程里根本没提。最后说点我的实际体会这个DEMO做完之后我最大的感受是大文件上传的难点压根不在“上传”本身而在于把异常场景处理干净。断点续传、秒传、并发控制这些花活做的都是“在糟糕的网络条件下让用户感到这件事仍然是可靠的”这一件事。前端切个片不难难的是你切完片之后后端能不能准确按顺序拼回来上传接口也不难难的是网络断了一半之后你还有没有勇气从第50片继续传。如果你正打算做类似的需求我建议不要一上来就追求完整版。先实现“分片合并”这个最小闭环跑通之后再一步步加状态查询、断点续传、并发控制。每次只加一个能力出问题了也容易定位。等这些基础都稳了再去考虑秒传和文件去重这类优化场景。最后分享一个小技巧DEMO里所有的上传状态我都统一维护在一个reactive对象里并且用watch监控它。这样后端有任何异常前端状态机就会自动落到error分支你只需要在这个分支里写重试逻辑就行。这个状态机设计比在组件里到处散落if/else判断要省心得多后面扩展“暂停”“取消”这种操作时也会轻松很多。希望这篇记录能帮你在做Vue大文件上传时少走几步弯路。