前文链接
【文件上传那些事儿】- 01 简单的拖拽上传和进度条
【文件上传那些事儿】- 02 二进制级别的格式验证
【文件上传那些事儿】- 03 两种计算 hash 的方式
【文件上传那些事儿】- 04 切片上传和网格进度条
V1.5:断点续传
在前面四章循序渐进的迭代开发中,我们的上传 demo 已经初具规模,实现了简单的拖拽上传,二进制级别的格式验证,能够对大文件进行切片上传,接下来就是对切片上传的进一步优化,实现文件秒传和断点续传功能。
其实这两个功能原理都非常简单,下面将分别介绍具体实现。
秒传
前文有提到,我们是通过 hash 来确定文件是否存在于服务器上的,那么在前端计算完 hash 之后,只要在上传之前将 hash 和 ext 传到后端进行查询即可知道文件是否已经存在于服务器中,如果存在,则直接前端提示文件秒传成功,否则正常上传即可。
这里我们先定义后端接口,而这个接口,需要做什么呢?
- 当文件存在的时候,返回
uploaded
为true
即可
根据上述思路,很容易得出接口代码如下:
router.post("/api/v1/checkchunks", async ctx => {
const { hash, ext } = ctx.request.body;
const filepath = path.resolve(uploadPath, `${hash}.${ext}`);
let uploaded = false;
if (fse.existsSync(filepath)) {
uploaded = true;
}
ctx.body = {
uploaded
};
});
这样,就可以在前端通过判断 uploaded
的真假来确认文件是否秒传成功了:
const handleFileUpload = async () => {
/*...*/
const res = await axios.post("/dev-api/checkchunks", {
hash: fileHash,
ext: getFileExtension((fileRef.value as File).name)
});
const uploaded = res.data.uploaded;
if (uploaded) return alert("秒传成功");
/*...*/
};
断点续传
相比于秒传功能,断点续传稍微要复杂一些,不过慢慢分析之后也能将逻辑梳理清晰,一步一步来实现这个功能。
能够实现断点续传最关键的一点是:我们要知道后端有哪些残存的切片,从而在前端上传的时候,过滤掉这些切片。
为了实现这一功能,可以稍微扩展一下 checkchunks
的功能,让其不止返回 uploaded
,同时去读取 chunks
文件夹,获取其中的 chunk
的 name
:
const getUploadList = async chunkspath => {
return fse.existsSync(chunkspath)
// 过滤掉隐藏文件
? (await fse.readdir(chunkspath)).filter(filename => filename !== ".")
: [];
};
router.post("/api/v1/checkchunks", async ctx => {
/*...*/
let uploadedList = [];
if (fse.existsSync(filepath)) {
uploaded = true;
} else {
uploadedList = await getUploadList(chunkpath);
}
ctx.body = {
uploaded,
uploadedList
};
});
这样,我们就能在前端获取到服务器上的残存切片了,在上传之前有几点需要注意:
- 首先要过滤已经存在的切片
- 将已经存在的切片对应的网格进度条设置为 100%
上述二者皆可以通过 filter
来实现,唯一需要注意的是,一个是将存在的设置成 100%,而一个是将存在的过滤掉,条件是相反的:
// 设置进度条
chunks.value = fileChunks.map((c, i) => {
const name = `${fileHash}-${i}`;
return {
name,
index: +i,
hash: fileHash,
chunk: c.fileChunk,
progress: uploadedList.includes(name) ? 100 : 0
};
});
// 过滤服务器上残存的切片
const requests = chunks.value
.filter(({ name }) => !uploadedList.includes(name))
.map/*...*/
为了模拟网络不稳定的环境,我们先将切片上传到服务器上,随后随机删除一部分切片,然后再次上传文件(当然也可以写个 random 在上传过程中直接随机让一部分切片失败,这样可以省去手动去删掉切片的测试过程),效果如下:
可以看到,一开始残存切片的进度条展示是正确的,然而在进度条走了一部分之后,就停止了。再来到服务端查看确认:
文件是成功上传并且合并了的,那么问题就出在进度条上了,于是定位到网格进度条的位置:
const requests =
chunks.value
.filter(({ name }) => !uploadedList.includes(name))
.map(({ name, index, hash, chunk }: ChunkRequestType) => {
console.log("[chunks]:", { name, index, hash, chunk });
const formdata = new FormData();
formdata.append("name", name);
formdata.append("index", index);
formdata.append("hash", hash);
formdata.append("chunk", chunk);
return form;
})
.map((form, idx) => {
return axios.post("/dev-api/upload", form, {
onUploadProgress: progress => {
const { loaded, total } = progress;
chunks.value[idx].progress = Number(
((loaded / total) * 100).toFixed(2)
);
}
});
});
可以看到,在计算进度条的时候,使用的下标是数组的 index
,而这个数字永远是从 0 开始往后数的,所以这里我们应该使用 chunk
的 index
,稍微修正一下:
const requests =
chunks.value
.filter(({ name }) => !uploadedList.includes(name))
.map(({ name, index, hash, chunk }: ChunkRequestType) => {
console.log("[chunks]:", { name, index, hash, chunk });
const formdata = new FormData();
formdata.append("name", name);
formdata.append("index", index);
formdata.append("hash", hash);
formdata.append("chunk", chunk);
M return { formdata, index };
})
M.map(({ formdata, index }) => {
return axios.post("/dev-api/upload", formdata, {
onUploadProgress: progress => {
const { loaded, total } = progress;
M chunks.value[index].progress = Number(
((loaded / total) * 100).toFixed(2)
);
}
});
});
最终效果如下:
结束语
今天的文章到这里就告一段落了,接下来还有什么值得注意的呢?
- 并发控制:前面的切片上传是一股脑直接创建了所有的请求,虽然浏览器有请求限制,但过多的请求同时发出,也会对浏览器造成一定的压力,导致卡顿
- 错误处理:如果切片在上传过程中失败,能够自动尝试重发,而不是导致整体的失败
这些将在未来的文章中继续探讨迭代,那么新年第一天,新年新气象,祝好运!
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!