标签:
前言
这段时间面试官都挺忙的,频频呈此刻博客文章标题,虽然我不是出格想蹭热度,但是实在想不到好的标题了-。-,蹭蹭就蹭蹭 :)
事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,其时虽然思路正确,可惜最终也并不算完全答对
结束后花了一段时间整理了下思路,那么究竟该如何实现一个大文件上传,以及在上传中如何实现断点续传的成果呢?
本文将从零搭建前端和处事端,实现一个大文件上传和断点续传的 demo
文章有误解的处所,欢迎指出,将在第一时间纠正,有更好的实现方法但愿留下你的评论
大文件上传
整体思路
前端
前端大文件上传网上的大部分文章已经给出了解决方案,核心是操作 Blob.prototype.slice 要领,和数组的 slice 要领相似,挪用的 slice 要领可以返回原文件的某个切片
这样我们就可以按照预先设置好的切片最大数量将文件切分为一个个切片,然后借助 http 的可并发性,,同时上传多个切片,这样从原本传一个大文件,酿成了同时传多个小的文件切片,可以大大减少上传时间
此外由于是并发,传输随处事真个挨次可能会产生变革,所以我们还需要给每个切片记录挨次
处事端
处事端需要卖力接受这些切片,并在接收到所有切片后合并切片
这里又引伸出两个问题
何时合并切片,即切片什么时候传输完成
如何合并切片
第一个问题需要前端进行共同,前端在每个切片中都携带切片最大数量的信息,当处事端接受到这个数量的切片时自动合并,也可以特别发一个请求主动通知处事端进行切片的合并
第二个问题,具体如何合并切片呢?这里可以使用 nodejs 的 api fs.appendFileSync,它可以同地势将数据追加到指定文件,也就是说,当处事端接受到所有切片后,先创建一个最终的文件,然后将所有切片逐步合并到这个文件中
talk is cheap,show me the code,接着我们用代码实现上面的思路
前端部分
前端使用 Vue 作为开发框架,对界面没有太大体求,原生也可以,考虑到美不雅观使用 element-ui 作为 UI 框架
上传控件
首先创建选择文件的控件,监听 change 事件以及上传按钮
<template> <div> <input type="file" @change="handleFileChange" /> <el-button @click="handleUpload">上传</el-button> </div></template><script>export default { data: () => ({ container: {
file: null } }), methods: { async handleFileChange(e) {
const [file] = e.target.files;
if (!file) return;
Object.assign(this.$data, this.$options.data());
this.container.file = file; }, async handleUpload() {} }};</script>

请求逻辑
考虑到通用性,这里没有用第三方的请求库,而是用原生 XMLHttpRequest 做一层简单的封装来发请求
request({
url,
method = "post",
data,
headers = {},
requestList }) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
Object.keys(headers).forEach(key =>
xhr.setRequestHeader(key, headers[key])
);
xhr.send(data);
xhr.onload = e => {
resolve({
data: e.target.response
});
};
}); }
上传切片
接着实现对照重要的上传成果,上传需要做两件事
对文件进行切片
将切片传输给处事端
<template> <div> <input type="file" @change="handleFileChange" /> <el-button @click="handleUpload">上传</el-button> </div></template><script>+ const LENGTH = 10; // 切片数量export default { data: () => ({ container: {
file: null,+
data: [] } }), methods: { request() {}, async handleFileChange() {},+ // 生成文件切片+ createFileChunk(file, length = LENGTH) {+
const fileChunkList = [];+
const chunkSize = Math.ceil(file.size / length);+
let cur = 0;+
while (cur < file.size) {+
fileChunkList.push({ file: file.slice(cur, cur + chunkSize) });+
cur += chunkSize;+
}+
return fileChunkList;+ },+ // 上传切片+ async uploadChunks() {+
const requestList = this.data+
.map(({ chunk }) => {+
const formData = new FormData();+
formData.append("chunk", chunk);+ formData.append("hash", hash);+
formData.append("filename", this.container.file.name);+
return { formData };+
})+
.map(async ({ formData }) =>+
this.request({+
url: "http://localhost:3000",+
data: formData+
})+
);+
await Promise.all(requestList); // 并发切片+ },+ async handleUpload() {+
if (!this.container.file) return;+
const fileChunkList = this.createFileChunk(this.container.file);+
this.data = fileChunkList.map(({ file },index) => ({+
chunk: file,+
hash: this.container.file.name + "-" + index // 文件名 + 数组下标+
}));+
await this.uploadChunks();+ } }};</script>
当点击上传按钮时,挪用 createFileChunk 将文件切片,切片数量通过一个常量 Length 控制,这里设置为 10,即将文件分成 10 个切片上传
createFileChunk 内使用 while 循环和 slice 要领将切片放入 fileChunkList 数组中返回