大文件切片上传续传秒传,以及一些优化思考~

312 阅读4分钟

最近很多人在问,在 Vue3 中如何去做大文件的上传、暂停、续传,接下来就讲讲我的思路吧~

切片上传

大文件上传优化,肯定涉及到切片上传,顾名思义,就是把大文件切成一个一个的小片段,去上传,主要流程分为以下几步:

  • 1.前端接收BGM并进行切片
  • 2.将每份切片都进行上传
  • 3.后端接收到所有切片,创建一个文件夹存储这些切片
  • 4.后端将此文件夹里的所有切片合并为完整的BGM文件
  • 5.删除文件夹,因为切片不是我们最终想要的,可删除
  • 6.当服务器已存在某一个文件时,再上传需要实现“秒传”

640.png

后端代码准备

我这里用 Nodejs 模仿了后端,这不是本文章的重点,大家只要知道它实现了以下三个接口:

  • upload: 切片文件的上传
  • merge: 切片合并成大文件
  • verify: 查询文件是否传输完了,如果没传输完,那么只上传了哪些部分

微信图片_20231102090443.png

前端代码

以下是前端的代码

示例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 { ButtonInputProgress } from 'ant-design-vue';  
import axios, { type AxiosProgressEvent } from 'axios';  
</script>

hanleInputChange存文件

这个函数是接收你上传的文件,并先存起来~

const file = ref<File | null>(null);  
const hanleInputChange = (e: any) => {  
  // 把所需上传的文件先存起来  
  file.value = e.target.files[0];  
};

开始上传

然后我们开始编写上传的代码,需要注意几件事情:

  • 先定义好一个切片的尺寸是多少
  • 把你的大文件分割成一个一个的小切片
  • 所有切片进行上传
  • 每个切片身上有一个finish,代表这个切片是否已上传完成
  • 切片上传完后,发起合并请求
  • 记得把CancelToken放在上传axios中,用于后续的暂停请求
interface IFileChunk {  
  fileBlob;  
  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 listIFileChunk[] = [];  
  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,  
      finishfalse,  
      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.lengthreturn;  
  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, {  
          onUploadProgresse => {  
            onUploadProgress(index, e);  
          },  
          cancelToken: source.token,  
        }),  
      ),  
  );  
};  
// 合并的请求函数  
const merge = () =>  
  axios.post(  
    'http://localhost:3000/merge',  
    JSON.stringify({  
      sizeSIZE,  
      fileName: file.value!.name,  
    }),  
    {  
      headers: {  
        'content-type''application/json',  
      },  
    },  
  );  
// 开始上传  
const start = async () => {  
  if (!file.valuereturn;  
  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;  
  },  
  {  
    deeptrue,  
  },  
);

暂停上传

还记得我们把 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.valuereturn;  
+  const { shouldUpload } = await verify();  
+  if (!shouldUpload) {  
+    console.log('上传成功');  
+    return;  
+  }  
  createChunks();  
  await upload();  
  await merge();  
};

思考优化点

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

并发控制

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

秒传优化

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

作者:林三心