Node.js+Vue大文件上传(流式上传/断点续传)解决方案

项目中遇到上传大文件的需求(上传视频、大的execl对账表格等),特此记录解决方案,有不足之处望斧正。

解决思路

  1. 将大文件在前端生成签名后,请求后端接口,检测出该文件之前是否上传过,后端返回该文件应上传的字节起始位置。(如果第一次上传,那么起始位置就是0)
  2. 前端拿到起始位置后,根据文件总大小计算出剩余还需上传的字节数
  3. 前端根据一定大小的字节数将剩余所需上传的字节数进行切割成块(向上取整),然后分别将这些数据块依次构建form-data表单提交上传。
  4. 后端接收到上传过来的数据块,写入文件。

Demo环境说明

  • 前端: Vue,axios
  • 后端: Node.js,koa,koa-body

具体实现

  • 前端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

this.sharedSize=1*1024*1024;//分片大小 1MB
//查询api该文件上传剩余所需片
let {fileSize}=await this.$networkHandler.sendRequest("CHECK_FILE_SLICES",{name:this.file.name});
this.completedCount=fileSize;

if(this.completedCount==this.file.size)//this.file 是前端所要上传的文件
return alert("该文件已全部上传");

//向上取整,计算出所需上传片数
let num=Math.ceil((this.file.size-this.completedCount)/this.sharedSize);

for(let i=0;i<num;i++){
if(this.uploadFlag==2)
return undefined;

//计算本次上传文件起止位置
let start=this.completedCount;
let end=Math.min(this.file.size,start+this.sharedSize);
let length=end-start;
//构建Form表单提交
let form=new FormData();
form.append("data",this.file.slice(start,end));//切割文件
form.append("name",this.file.name);//这里正确的姿势应该是传一些可以验证文件唯一性的标识,比如签名
try{
await this.$networkHandler.sendRequest("UPLOAD_BIG_FILE",form);
this.completedCount+=length;
if(this.completedCount==this.file.size){
//上传完毕
this.clearData();
this.completeFlag=true;
}

}catch (e){
console.error(e);
return alert("上传失败!");//上传失败需要终止循环
}
}
  • 后端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 检测文件已经上传的字节数
* @param ctx
* @param next
* @returns {Promise.<*>}
*/
async checkFileSlices(ctx,next){
let name=ctx.request.body.fields.name;
let temp_path='./Uploads/'+name;
let isExist=fs.existsSync(temp_path);
if(!isExist){
ctx.body={
code:'OK',
data:{
fileSize:0
}
};
return await next();
}

let file=fs.statSync(temp_path);
ctx.body={
code:'OK',
data:{
fileSize:file.size
}
};

await next();
}

/**
* 上传文件
* @param ctx
* @param next
* @returns {Promise.<void>}
*/
async uploadFile(ctx,next){

let file=ctx.request.body.files.data;
let name=ctx.request.body.fileds.name;

let uploadPath="./Uploads";
let fullName=uploadPath+"/"+name;//全部上传完成后的完整路径

let tempData=fs.readFileSync(file.path);
fs.writeFileSync(fullName,tempData,{flag:"a"});

fs.unlinkSync(file.path);//写入完毕后删除临时文件

ctx.result={
code:'OK',
message:`上传完成`
};

await next();

}