• Vue3如何实现大文件的断点续传功能?
  • 发布于 2个月前
  • 230 热度
    0 评论
前言
最近很多人在问,在 Vue3 中如何去做大文件的上传、暂停、续传,接下来就讲讲我的思路吧~

切片上传
大文件上传优化,肯定涉及到切片上传,顾名思义,就是把大文件切成一个一个的小片段,去上传,主要流程分为以下几步:
1.前端接收BGM并进行切片
2.将每份切片都进行上传
3.后端接收到所有切片,创建一个文件夹存储这些切片
4.后端将此文件夹里的所有切片合并为完整的BGM文件
5.删除文件夹,因为切片不是我们最终想要的,可删除
6.当服务器已存在某一个文件时,再上传需要实现“秒传”

后端代码准备
我这里用 Nodejs 模仿了后端,这不是本文章的重点,大家只要知道它实现了以下三个接口:
upload: 切片文件的上传
merge: 切片合并成大文件
verify: 查询文件是否传输完了,如果没传输完,那么只上传了哪些部分

前端代码
以下是前端的代码

示例html
首先准备上传文件、进度条、上传、暂停、续传等 html 元素
<template>
  <Input type="file" @change="hanleInputChange" />
  <Progress :percent="percent" />
  <Button @click="start" type="primary" class="mr-2">开始</Button>
  <Button @click="pause">暂停</Button>
  <Button @click="keep">续传</Button>
</template>

<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Button, Input, Progress } from 'ant-design-vue';
import axios, { type AxiosProgressEvent } from 'axios';
</script>

hanleInputChange存文件
这个函数是接收你上传的文件,并先存起来~
const file = ref<File | null>(null);
const hanleInputChange = (e: any) => {
  // 堆代码 duidaima.com
  // 把所需上传的文件先存起来
  file.value = e.target.files[0];
};

开始上传
然后我们开始编写上传的代码,需要注意几件事情:
1.先定义好一个切片的尺寸是多少
2.把你的大文件分割成一个一个的小切片
3.所有切片进行上传
4.每个切片身上有一个finish,代表这个切片是否已上传完成
5.切片上传完后,发起合并请求

6.记得把CancelToken放在上传axios中,用于后续的暂停请求

interface IFileChunk {
  file: Blob;
  size: number;
  finish: boolean;
  chunkName: string;
  fileName: string;
  index: number;
}
// 存储切片
const chunkList = ref<IFileChunk[]>([]);
// 用于axios请求的取消
const CancelToken = axios.CancelToken;
let source = CancelToken.source();

// 每个切片的尺寸
const SIZE = 3 * 1024 * 1024;
// 创建切片
const createChunks = () => {
  const fileName = file.value!.name;
  const list: IFileChunk[] = [];
  let s = 0;
  let index = 0;
  while (s < file.value!.size) {
    const fileChunk = file.value!.slice(s, s + SIZE);
    list.push({
      file: fileChunk,
      size: fileChunk.size,
      finish: false,
      chunkName: `${fileName}-${index}`,
      fileName,
      index,
    });
    s += SIZE;
    index++;
  }
  chunkList.value = list;
};

// 监听上传过程的回调
const onUploadProgress = (index: number, e: AxiosProgressEvent) => {
  const chunkItem = chunkList.value[index];
  const { loaded, total } = e;
  if (loaded >= total!) {
    // 满足这个条件,代表这个切片已经上传完成
    chunkItem.finish = true;
  }
};
// 上传的请求函数
const upload = async (list?: IFileChunk[]) => {
  const fileList = list ?? chunkList.value;
  if (!fileList.length) return;
  return Promise.all(
    fileList
      .map(({ file, fileName, index, chunkName }) => {
        const formData = new FormData();
        formData.append('file', file);
        formData.append('fileName', fileName);
        formData.append('chunkName', chunkName);
        return { formData, index };
      })
      .map(({ formData, index }) =>
        axios.post('http://localhost:3000/upload', formData, {
          onUploadProgress: e => {
            onUploadProgress(index, e);
          },
          cancelToken: source.token,
        }),
      ),
  );
};
// 合并的请求函数
const merge = () =>
  axios.post(
    'http://localhost:3000/merge',
    JSON.stringify({
      size: SIZE,
      fileName: file.value!.name,
    }),
    {
      headers: {
        'content-type': 'application/json',
      },
    },
  );
// 开始上传
const start = async () => {
  if (!file.value) return;
  createChunks();
  await upload();
  await merge();
};

百分比计算
刚刚说到了,每一个切片身上都有一个 finish 参数,记录这个切片是否已经上传完成了,我们想要计算百分比很简单,只需要知道有多少个 finish = true ,去除以总的切片数,就能得到百分比了
const percent = ref(0);
// 监听切片列表的变化
watch(
  () => chunkList,
  v => {
    // 计算出多少个已经上传完成
    const finishChunks = v.value.filter(({ finish }) => finish);
    // 计算百分比
    percent.value = Number((finishChunks.length / v.value.length).toFixed(2)) * 100;
  },
  {
    deep: true,
  },
);

暂停上传
还记得我们把 CancelToken 放在了上传请求中吗?我们想要暂停,只需要利用这个来取消上传的请求,就可以达到暂停的效果
要注意一个点:每次都要重置 source ,因为 source 已经被消费了,需要重置,下次才能继续取消请求~
const pause = () => {
  source.cancel('中断上传!');
  source = CancelToken.source();
};

续传
续传只需要请求 verify 接口,就能得知:
该不该续传?
如需续传,那是哪些切片需要续传?
const verify = async () => {
  const { data } = await axios.post(
    'http://localhost:3000/verify',
    JSON.stringify({
      fileName: file.value!.name,
    }),
    {
      headers: {
        'content-type': 'application/json',
      },
    },
  );
  return data;
};
const keep = async () => {
  const { shouldUpload, uploadedList } = await verify();
  // shouldUpload = true 说明不用续传了
  if (!shouldUpload) return;
  // 计算出哪些切片没有上传
  const uploadList = chunkList.value.filter(({ chunkName }) => !uploadedList.includes(chunkName));
  // 进行续传
  upload(uploadList);
};

秒传
秒传就是:后端那边已经有这个文件了,你前端上传直接提示上传成功就行~
const start = async () => {
  if (!file.value) return;
+  const { shouldUpload } = await verify();
+  if (!shouldUpload) {
+    console.log('上传成功');
+    return;
+  }
  createChunks();
  await upload();
  await merge();
};

思考优化点
实现完上面功能之后,我们可以想象下有哪些地方可以优化,以下只是说一下想法,具体的代码实现,感兴趣的话可以以后开一个文章来讲。

1.并发控制
切片太多了, 一股脑去上传,肯定会对浏览器和服务器造成很大负担,所以可以控制一下并发,使用p-limit这个库,去控制一次只发一定数量的请求,进而达到并发控制的效果~

2.秒传优化
刚刚秒传是用文件名去判断的,这样肯定是不好的,最严谨的做法就是通过文件的hash值去判断,这是最准确的,这个hash值是需要在前端计算的,但是前端计算hash值可能有点慢,所以可以使用WebWorker去优化
用户评论