Vue3 + Element Plus 实战:大文件分片上传与断点续传全解析
在前端开发中,处理几 MB 的小文件轻而易举,但当面对几百 MB 甚至 GB 级别的大文件时,直接上传往往会遇到以下痛点:
- 超时失败:网络波动导致长连接中断,整个文件需重新上传。
- 内存溢出:一次性读取大文件可能导致浏览器崩溃。
- 体验极差:用户无法感知进度,且无法暂停或恢复。
本文将基于 Vue 3 + TypeScript + Element Plus,结合 spark-md5,手把手教你实现一个支持秒传、分片上传、并发控制及 Blob 下载的完整解决方案。
📂 核心思路
1. 上传流程
- 计算 Hash:利用
SparkMD5计算文件的唯一标识(用于秒传和断点续传识别)。 - 文件切片使用
File.prototype.slice()将大文件切割成固定大小(如 2MB)的 Chunk。 - 并发上传:将切片并行发送给后端接口。
- 合并文件:所有切片上传成功后,通知后端合并。
2. 下载流程
- 请求数据:通过
fetch或 axios 获取二进制流。 - 创建 Blob将响应数据转换为
Blob对象。 - 触发下载:动态创建
<a>标签并模拟点击。
安装
- SparkMD5 (高性能 MD5 计算库)
npm install spark-md5
💻 核心代码实现
1. 模板结构 (Template)
我们使用 el-upload 的自定义模式,关闭自动上传,完全由我们控制逻辑。
<template>
<div class="upload-container">
<!-- 拖拽上传区域 -->
<el-upload
drag
:auto-upload="false"
:show-file-list="false"
:on-change="handleFileChange"
action="#"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">拖拽文件到此处或 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">支持大文件分片上传,请勿上传超过 2GB 的文件</div>
</template>
</el-upload>
<!-- 进度显示 -->
<div v-if="uploading" class="progress-box">
<span>{{ fileName }}</span>
<el-progress :percentage="uploadPercentage" :status="uploadStatus" />
<span>{{ uploadStatusText }}</span>
</div>
<!-- 模拟下载测试区 -->
<el-divider />
<div class="download-list">
<h3>文件列表 (点击下载)</h3>
<el-button type="primary" link @click="handleDownload('example.zip', 'http://mock-api.com/file.zip')">
下载: example_large_file.zip
</el-button>
</div>
</div>
</template>
2. 逻辑实现 (Script Setup)
A. 状态定义与配置
import { ref } from "vue";
import { ElMessage } from "element-plus";
import { UploadFilled } from "@element-plus/icons-vue";
import SparkMD5 from "spark-md5";
// 常量配置
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB 切片
// 响应式状态
const fileObj = ref(null);
const uploading = ref(false);
const uploadPercentage = ref(0);
const uploadStatus = ref(""); // success, exception, ''
const uploadStatusText = ref("");
const fileName = ref("");
B. 核心上传入口
const handleFileChange = (file) => {
fileObj.value = file.raw;
fileName.value = file.name;
uploadFileRequest(); // 触发上传
};
const uploadFileRequest = async () => {
if (!fileObj.value) return;
const file = fileObj.value;
uploading.value = true;
uploadPercentage.value = 0;
uploadStatusText.value = "正在计算文件哈希...";
try {
// 1. 计算 Hash
const fileHash = await calculateHash(file);
// 2. 秒传校验 (Mock)
// const verifyRes = await axios.post('/api/upload/verify', { hash: fileHash });
const verifyRes = { needUpload: true };
if (!verifyRes.needUpload) {
finishUpload("秒传成功:文件已存在");
return;
}
// 3. 切片
const chunks = createFileChunks(file, CHUNK_SIZE);
uploadStatusText.value = `准备上传 ${chunks.length} 个分片...`;
// 4. 并发上传
const uploadPromises = chunks.map((chunk, index) =>
uploadChunk({ chunk, hash: fileHash, index, filename: file.name })
);
await Promise.all(uploadPromises);
// 5. 合并文件
uploadStatusText.value = "正在合并文件...";
// await axios.post('/api/upload/merge', { hash: fileHash, filename: file.name });
await new Promise(r => setTimeout(r, 1000)); // 模拟合并耗时
finishUpload("上传成功");
} catch (error) {
handleError(error);
} finally {
uploading.value = false;
fileObj.value = null;
}
};
C. 关键辅助函数
1. 非阻塞式 Hash 计算 大文件计算 Hash容易卡死 UI,我们采用分片读取 + setTimeout 让出主线程的策略。
const calculateHash = (file) => {
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
let offset = 0;
const readChunk = () => {
if (offset >= file.size) {
spark.end();
resolve(spark.getResult());
return;
}
const chunk = file.slice(offset, offset + CHUNK_SIZE);
offset += CHUNK_SIZE;
fileReader.readAsArrayBuffer(chunk);
};
fileReader.onload = (e) => {
spark.append(e.target.result);
// 关键:让出主线程,防止 UI 冻结
setTimeout(readChunk, 0);
};
fileReader.onerror = reject;
readChunk();
});
};
2. 文件切片
const createFileChunks = (file, size) => {
const chunks = [];
let cur = 0;
while (cur < file.size) {
chunks.push(file.slice(cur, cur + size));
cur += size;
}
return chunks;
};
3. 模拟分片上传与进度更新
const uploadChunk = ({ chunk, hash, index }) => {
return new Promise((resolve) => {
// 实际项目中这里使用 axios.post FormData
const time = Math.random() * 1000 + 500; // 模拟网络延迟
setTimeout(() => {
// 简单进度计算:当前索引 / 总分片数
// 注意:实际并发下进度条可能跳动,建议维护一个 uploadedCount计数器
const totalChunks = Math.ceil(fileObj.value.size / CHUNK_SIZE);
uploadPercentage.value = Math.floor(((index + 1) / totalChunks) * 100);
resolve();
}, time);
});
};
3. 大文件下载实现
前端下载大文件通常有两种方式:
- Window Open:简单 GET 请求,无法携带复杂 Header。
- Blob Download:推荐方式,支持鉴权 Token,可处理二进制流。
const handleDownload = (fileName, url) => {
ElMessage.info(`开始下载: ${fileName}`);
downloadByBlob(url, fileName);
};
const downloadByBlob = async (url, fileName) => {
try {
// 实际项目中使用 fetch 或 axios
// const response = await fetch(url, { headers: { 'Authorization': 'Bearer token' } });
// const blob = await response.blob();
// 模拟生成 Blob
const mockContent = "这是一个模拟的大文件内容。";
const blob = new Blob([mockContent], { type: "application/octet-stream" });
// 创建临时链接触发下载
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
} catch (error) {
ElMessage.error("下载失败");
}
};
⚡ 性能优化与最佳实践
1. 真正的并发控制
上面的代码使用了 Promise.all,这意味着如果文件有 1000 个分片,会同时发起 1000 个请求,这会导致浏览器卡顿或被服务器拒绝。 优化方案使用队列限制最大并发数(例如最多同时上传 3-5 个分片)。
// 简易并发控制器
async function concurrentRun(tasks, limit) {
const results = [];
let index = 0;
const runNext = async () => {
if (index >= tasks.length) return;
const task = tasks[index++];
await task();
await runNext();
};
const workers = Array(Math.min(limit, tasks.length)).fill(null).map(() => runNext());
await Promise.all(workers);
}
2. 断点续传
在 uploadFileRequest 中,切片上传前应先调用后端接口 /api/upload/check?hash=xxx。 后端返回已上传的分片索引数组(如 [0, 1, 3]),前端过滤掉这些分片,只上传剩余部分。
3. Web Worker 计算 Hash
如果文件极大(>500MB),即使在 setTimeout 中计算 Hash 仍可能影响交互。 终极方案:将 calculateHash 逻辑移至 Web Worker 中运行,彻底隔离主线程。
4. 错误重试
网络不稳定时,单个分片可能失败。应在 uploadChunk 中加入重试机制(Retry Pattern),失败后自动重试 3 次。
📝 总结
通过本文的实现,我们解决了大文件上传的核心痛点:
- 稳定性:分片上传避免单点失败导致全盘重来。
- 效率:秒传功能节省带宽和时间。
- 体验:实时进度反馈和流畅的 UI 交互。
这套方案不仅适用于 Vue,其核心逻辑(切片、Hash、并发、合并)在任何前端框架甚至原生 JS 中都是通用的。希望这篇博客能帮助你轻松搞定大文件传输难题!
更多推荐


所有评论(0)