【文件上传那些事儿】- 05 断点续传

1,336 阅读4分钟

前文链接

【文件上传那些事儿】- 01 简单的拖拽上传和进度条

【文件上传那些事儿】- 02 二进制级别的格式验证

【文件上传那些事儿】- 03 两种计算 hash 的方式

【文件上传那些事儿】- 04 切片上传和网格进度条

V1.5:断点续传

在前面四章循序渐进的迭代开发中,我们的上传 demo 已经初具规模,实现了简单的拖拽上传,二进制级别的格式验证,能够对大文件进行切片上传,接下来就是对切片上传的进一步优化,实现文件秒传和断点续传功能。

其实这两个功能原理都非常简单,下面将分别介绍具体实现。

秒传

前文有提到,我们是通过 hash 来确定文件是否存在于服务器上的,那么在前端计算完 hash 之后,只要在上传之前将 hash 和 ext 传到后端进行查询即可知道文件是否已经存在于服务器中,如果存在,则直接前端提示文件秒传成功,否则正常上传即可。

这里我们先定义后端接口,而这个接口,需要做什么呢?

  • 当文件存在的时候,返回 uploadedtrue 即可

根据上述思路,很容易得出接口代码如下:

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 文件夹,获取其中的 chunkname

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 在上传过程中直接随机让一部分切片失败,这样可以省去手动去删掉切片的测试过程),效果如下:

img01

可以看到,一开始残存切片的进度条展示是正确的,然而在进度条走了一部分之后,就停止了。再来到服务端查看确认:

img02

文件是成功上传并且合并了的,那么问题就出在进度条上了,于是定位到网格进度条的位置:

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 开始往后数的,所以这里我们应该使用 chunkindex,稍微修正一下:

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)
      );
    }
  });
});

最终效果如下:

img03

结束语

今天的文章到这里就告一段落了,接下来还有什么值得注意的呢?

  • 并发控制:前面的切片上传是一股脑直接创建了所有的请求,虽然浏览器有请求限制,但过多的请求同时发出,也会对浏览器造成一定的压力,导致卡顿
  • 错误处理:如果切片在上传过程中失败,能够自动尝试重发,而不是导致整体的失败

这些将在未来的文章中继续探讨迭代,那么新年第一天,新年新气象,祝好运!