前端大文件上传
大文件上传基本思路
- 将大文件转换成二进制流的格式(当你通过
<input type="file">
元素选择文件时,浏览器已经为你提供了一个File
对象,这个对象本质上就是一个包含文件内容的二进制大对象(Blob),这一步就省略了)。 - 利用文件
Blob
原型上的slice
方法进行切割,将二进制流切割成多份,将得到的切片数 组chunkList
添加一些信息,比如文件名和下标,得到uploadChunkList
- 组装和分割块同等数量的请求块,并行或串行的形式发出请求
- 待我们监听到所有请求都成功发出去以后,再给服务端发出一个合并的信号
<template>
<h1>大文件上传</h1>
<input type="file" @change="handleFileChange" />
<el-button @click="handleUpload">上传</el-button>
</template>
<script>
const SIZE = 3 * 1024 * 1024; // 定义切片的大小
export default {
data() {
return {
file: null, // 文件
hash: '', // 文件的hash
chunkList: [], // 切片列表
};
},
methods: {
handleFileChange(e) {
const [file] = e.target.files;
if (!file) {
this.file = null;
return;
}
this.file = file;
},
// 生成文件切片
createFileChunk(file, size = SIZE) {
const fileChunkList = [];
let cur = 0;
while (cur < file.size) {
// file.slice 返回一个 blob对象
fileChunkList.push({ file: file.slice(cur, cur + size) });
cur += size;
}
return fileChunkList;
},
// 上传文件切片
async uploadChunks(uploadedList = []) {
// 构造请求列表
const requestList = this.chunkList
.map(({ chunk, chunkHash, index, fileHash }) => {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkHash', chunkHash);
formData.append('fileHash', fileHash);
return { formData, index };
})
.map(async ({ formData, index }) =>
this.request({
url: 'http://localhost:8080/upload-chunk',
method: 'post',
data: formData,
})
);
await Promise.all(requestList); // 并发切片
await this.mergeRequest(); // 合并切片
},
// 通知服务的合并切片
async mergeRequest() {
await this.request({
url: 'http://localhost:8080/merge',
method: 'post',
headers: { 'content-type': 'application/json' },
data: JSON.stringify({ filename: this.file.name, fileSize: this.file.size, size: SIZE, hash: this.hash }),
});
},
// 上传按钮点击事件
async handleUpload() {
if (!this.file) {
console.log('请选择一个文件吧');
return;
}
// 文件分片
const fileChunkList = this.createFileChunk(this.file);
// 计算文件hash
this.hash = await this.calculateHash(fileChunkList);
// 构建 chunkList 添加下标以及 上传进度(是每一个chunk的上传进度)
this.chunkList = fileChunkList.map(({ file }, index) => ({
chunk: file,
size: file.size,
chunkHash: `${this.hash}-${index}`,
fileHash: this.hash,
index,
percentage: uploadedList.includes(`${this.hash}-${index}`) ? 100 : 0,
}));
// 上传 chunk
await this.uploadChunks(uploadedList);
},
},
};
</script>
秒传
秒传指的是已经上传过的文件,不必重复传输,可以直接从后台取回文件资源。 前端实现思路如下:
- 用户选择要上传的文件
- 计算文件的
hash值
,利用hash值
判断该文件是否已经存在 - 如果已经存在,直接从后台获取该文件资源,上传结束
- 如果不存在,则走正常的分片上传流程
断点续传
- 为每一个文件切割块添加不同的标识
- 当上传成功的之后,记录上传成功的标识
- 当我们暂停或者发送失败后,可以重新发送没有上传成功的切割文件
进度条
xhr
中提供了上传进度的事件progress
,如果项目中使用的是axios ,axios
在配置时提供了onUploadProgress
监听原生progress
事件。
var xhr = new XMLHttpRequest();
// 监听上传进度
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
var percentComplete = (event.loaded / event.total) * 100;
console.log('Upload progress: ' + percentComplete.toFixed(2) + '%');
}
};
// 监听下载进度
xhr.onprogress = function(event) {
if (event.lengthComputable) {
var percentComplete = (event.loaded / event.total) * 100;
console.log('Download progress: ' + percentComplete.toFixed(2) + '%');
}
};
后端
-
接收每一个切割文件,并在接收成功后,存到指定位置,并告诉前端接收成功
-
收到合并信号,将所有的切割文件排序,合并,生成最终的大文件,然后删除切割小文件,并告知前端大文件的地址