JavaScript实现大文件切片上传与断点续传
在 Web 开发中,处理 GB 级大文件上传需要优化用户体验并降低服务器压力。本文通过前端 Vue 示例,展示如何实现文件切片、基于内容生成 Hash 验证、利用 Web Worker 避免阻塞主线程、配合 Axios 实现暂停与恢复,以及断点续传的完整逻辑。
准备环境与基础切片
切片上传的核心在于将一个大文件切割为多个小片段(Chunk),分别发送请求到服务器,最后由服务端合并。首先,我们需要定义切片的固定大小,通常设置在 10MB 左右以平衡并发数与连接开销。
获取文件后,通过 slice 方法切割文件对象。以下是一个基础的 Vue 组件结构,定义了数据容器用于存储文件和切片信息:
<script>
const SIZE = 10 * 1024 * 1024; // 单个切片大小,约 10MB
export default {
data: () => ({
container: {
file: null,
hash: null,
worker: null
},
data: [], // 存放加工好的文件切片列表
hashPercentage: 0
}),
methods: {
// 监听文件选择事件
handleFileChange(e) {
const [file] = e.target.files;
if (!file) {
this.container.file = null;
return;
}
this.container.file = file;
},
// 根据指定大小生成切片数组
createFileChunk(file, size = SIZE) {
const fileChunkList = [];
let cur = 0;
while (cur < file.size) {
// slice 方法返回 Blob 对象
fileChunkList.push({ file: file.slice(cur, cur + size) });
cur += size;
}
return fileChunkList;
},
async handleUpload() {
if (!this.container.file) return;
// 1. 生成切片
const fileChunkList = this.createFileChunk(this.container.file);
// 2. 生成文件唯一 Hash
this.container.hash = await this.calculateHash(fileChunkList);
// 3. 组装每个切片的数据(包含 Hash 标识和文件名)
this.data = fileChunkList.map(({ file }, index) => ({
chunk: file,
// 这里的 hash 为 '全局文件hash-切片序号'
hash: this.container.hash + "-" + index
}));
// 4. 执行上传
await this.uploadChunks();
}
}
};
</script>
利用 Web Worker 计算文件 Hash
为了实现秒传和断点续传,必须拥有一个能准确反映文件内容的唯一标识,即 Hash 值。如果使用文件名+下标作为哈希,一旦用户重命名文件或调整上传顺序,校验将失效。因此需基于文件内容计算 MD5 等哈希值。
由于读取二进制流并计算加密哈希是 CPU 密集型任务,直接在主线程执行会导致页面卡顿(UI 冻结)。解决方案是使用 Web Worker 在后台线程进行计算。
Worker 脚本编写
创建一个独立的 JS 文件(例如 /public/hash.js),在该文件中引入 spark-md5 库,因为 Worker 内部无法直接访问 window 作用域下的变量,需通过 importScripts 导入:
// /public/hash.js
self.importScripts("/spark-md5.min.js");
// 接收主线程发来的切片数据并计算 Hash
self.onmessage = e => {
const { fileChunkList } = e.data;
const spark = new self.SparkMD5.ArrayBuffer();
let percentage = 0;
let count = 0;
const loadNext = index => {
const reader = new FileReader();
// 异步读取当前切片的二进制数据
reader.readAsArrayBuffer(fileChunkList[index].file);
reader.onload = e => {
count++;
// 将读取结果追加到 Spark 对象中
spark.append(e.target.result);
// 进度更新与递归处理下一个切片
if (count === fileChunkList.length) {
self.postMessage({
percentage: 100,
hash: spark.end() // 最终生成的完整文件 Hash
});
self.close();
} else {
percentage += 100 / fileChunkList.length;
self.postMessage({ percentage });
loadNext(count);
}
};
};
loadNext(0);
};
在主线程调用 Worker
在 Vue 实例的方法中实例化 Worker,并通过 postMessage 发送切片列表,监听返回值:
calculateHash(fileChunkList) {
return new Promise(resolve => {
this.container.worker = new Worker("/hash.js");
this.container.worker.postMessage({ fileChunkList });
this.container.worker.onmessage = e => {
const { percentage, hash } = e.data;
this.hashPercentage = percentage;
if (hash) {
resolve(hash);
}
};
});
}
构建并发上传逻辑
当准备好所有切片数据后,构建请求列表。利用 FormData 封装文件片段和对应的元数据(如文件名为防止冲突保留字段),随后使用 Axios 发起并发请求。使用 Promise.all 等待全部切片上传完成后,再通知后端合并文件。
async uploadChunks(uploadedList = []) {
this.requestList = this.data
// 如果有已上传列表,过滤掉已存在的切片(用于断点续传)
.filter(({ hash }) => !uploadedList.includes(hash))
.map(({ chunk, hash }) => {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
return { formData };
})
.map(async ({ formData }) =>
request(formData) // request 封装了具体的 Axios 调用
);
await Promise.all(this.requestList);
// 确认所有剩余切片上传完毕后再触发合并
await merge(this.container.file.name);
}
实现断点续传与文件秒传
完整的上传流程需要应对网络中断或重复上传场景,这就需要后端接口的配合支持。
1. 文件秒传
原理:在前端计算完文件 Hash 后,先向服务器发送请求验证该 Hash 是否已存在。若存在,证明同文件内容已上传,无需传输数据,直接提示“秒传成功”。
async handleUpload() {
if (!this.container.file) return;
const fileChunkList = this.createFileChunk(this.container.file);
this.container.hash = await this.calculateHash(fileChunkList);
// 前置检查:查询后端是否已有此文件
const { haveExisted, uploadedList } = await verify(this.container.hash);
if (haveExisted) {
this.$message.success("秒传成功,无需上传");
return;
}
// ... 后续继续切片上传逻辑
}
2. 暂停与中断上传
利用 Axios 支持的 AbortController 可以在中间任意时刻取消正在进行的 HTTP 请求。为了能够暂停整个批次,我们需要保留每一个请求对象的引用。
// 修改上传函数以暴露请求控制能力
async uploadChunks(uploadedList = []) {
this.requestList = this.data
.filter(({ hash }) => !uploadedList.includes(hash))
.map((item) => {
const controller = new AbortController(); // 创建控制器
item.formData = new FormData();
item.formData.append("chunk", item.chunk);
item.formData.append("hash", item.hash);
item.formData.append("filename", this.container.file.name);
item.signal = controller.signal;
return item;
});
this.requestList = await Promise.all(
this.requestList.map(async (req) =>
request(req.formData, req.signal).then(() => {
// 成功后从列表中移除该项
this.requestList.splice(this.requestList.indexOf(req), 1);
}).catch(err => {
// 忽略被主动中止的错误
if(err.name !== 'CanceledError') throw err;
})
)
);
}
// 暂停上传动作
handlePause() {
this.requestList.forEach((req) => {
if (req && req.signal) {
const controller = new AbortController();
req.signal = controller.signal;
controller.abort(); // 中止请求
}
});
}
3. 恢复上传(断点续传)
当用户点击恢复时,前端再次向服务器请求已上传分片的状态接口(假设接口名仍为 verify),获取服务端已成功保存的切片 Hash 列表。
async handleRecovery() {
// 1. 获取当前状态下后端已有的切片列表
const { uploadedList } = await verify(this.container.hash);
// 2. 调用带参数的上传函数,自动跳过已上传部分
await this.uploadChunks(uploadedList);
}
逻辑汇总
- 切片划分:根据文件大小和预设阈值切割为小块。
- 唯一标识:通过 Worker 异步计算 MD5,确保重命名不影响校验准确性。
- 秒传机制:先比对整体文件 Hash,命中则跳过数据传输。
- 防丢失策略:每次上传前先拉取已存切片列表,前端
.filter()剔除已完成项,仅补传缺失块。 - 异常中断:借助
AbortController释放资源并终止连接。
以上就是基于 JavaScript 实现大文件切片上传、秒传及断点续传的详细内容,更多关于 JavaScript 大文件处理的技巧资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













