前端大文件上传
整体思路
前端
前端大文件上传网上的大部分文章已经给出了解决方案,核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,调用的 slice 方法可以返回原文件的某个切片
这样我们就可以根据预先设置好的切片最大数量将文件切分为一个个切片,然后借助 http 的可并发性,同时上传多个切片,这样从原本传一个大文件,变成了同时传多个小的文件切片,可以大大减少上传时间
另外由于是并发,传输到服务端的顺序可能会发生变化,所以我们还需要给每个切片记录顺序
服务端
服务端需要负责接受这些切片,并在接收到所有切片后合并切片
这里又引伸出两个问题
何时合并切片,即切片什么时候传输完成
如何合并切片
第一个问题需要前端进行配合,前端在每个切片中都携带切片最大数量的信息,当服务端接受到这个数量的切片时自动合并,也可以额外发一个请求主动通知服务端进行切片的合并
第二个问题,具体如何合并切片呢?这里可以使用 nodejs 的 api fs.appendFileSync,它可以同步地将数据追加到指定文件,也就是说,当服务端接受到所有切片后,先创建一个最终的文件,然后将所有切片逐步合并到这个文件中
代码实现
前端部分
上传:首先创建选择文件的控件,监听 change 事件以及上传按钮
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() {}}};
封装请求逻辑请求逻辑,普通http请求逻辑显然已无法满足
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});};});
}
上传切片
接着实现比较重要的上传功能,上传需要做两件事
对文件进行切片
将切片传输给服务端
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(