开箱即用的大文件上传库

前言

部分场景需要大文件切片上传的方案。因为公司涉及到很多模型和视频需要上传,本来我以为这个功能很简单,最开始做的能用就行,在不断实践下发现这个功能可以深挖下去,最终封装了一个足够完善的库 large-file-upload。现在已经可以做到在1秒内算完1G文件的hash大量请求使cpu过高时如何避免浏览器卡顿。基本已经算最佳实践。

ts 复制代码
npm i large-file-upload

api

  • 文件切片 createFileChunks
  • 计算hash generateFileHash
  • 并发控制器 UploadHelper

为什么需要大文件切片上传

首先如果一个5G的文件直接整体上传,中途出现网络问题就会白白的浪费时间,客户体验也不好,而且单次上传到后台一般会等待时间过长,客户可能不愿意等这么久直接就退出了。

整体思路

要先给文件切片分成更小的块,通常是5M到10M,之后看有没有秒传或者断点续传的需求,如果有的话,需要获得文件的唯一标识,一般是采用计算文件hash的方式。如果没有,像B站的上传视频,因为不需要秒传和续传,刷新后目前已上传的部分不再保留,采用的是一个随机生成UUID的方式。之后需要一个请求的并发控制器,以避免浏览器卡顿,同时这一步可以带上切片的序号以便后端使用。所有切片上传完成后前端发送请求通知后台,可以按照序号进行合并,具体看需求。

创建文件切片

文件的切片非常简单,拿到file文件后直接调用原生的slice方法即可。

ts 复制代码
import { createFileChunks } from 'large-file-upload';

// 可以传入指定的分片大小,没传会根据文件自动指定。
const { fileChunks, chunkSize } = createFileChunks(file)

源码如下:

ts 复制代码
export function createFileChunks(file: File, customChunkSize?: number): FileChunkResult {
  if (!file || !file.size) {
    throw new Error('File not found or size is 0');
  }

  const size = file.size;
  let chunkSize: number;

  if (typeof customChunkSize === 'number' && customChunkSize >= 1) {
    chunkSize = Math.floor(customChunkSize) * BASESIZE;
  } else if (customChunkSize !== undefined) {
    chunkSize = 4 * BASESIZE;
  } else {
    if (size < 100 * BASESIZE) {
      chunkSize = 1 * BASESIZE;
    } else if (size < 1024 * BASESIZE) {
      chunkSize = 5 * BASESIZE;
    } else {
      chunkSize = 10 * BASESIZE;
    }
  }

  const fileChunks: Blob[] = [];
  for (let start = 0; start < size; start += chunkSize) {
    const end = Math.min(start + chunkSize, size);
    fileChunks.push(file.slice(start, end));
  }
  return { fileChunks, chunkSize: chunkSize / BASESIZE };
}

计算文件hash

计算hash最重要的就是标识文件的唯一值,这个过程非常耗时,1G的文件大概10秒左右,严格来说没什么能优化的地方,类似1+1,能压缩的空间不多,所以想要快就要换种方案,最终我采用的多线程计算hash加抽样的方法。1G的文件1秒算完,30G的文件需要6秒左右。

ts 复制代码
import { generateFileHash } from 'large-file-upload';

async function hashLargeFile(file: File) {
  const hash = await generateFileHash(file);
  console.log('Generated hash for the large file:', hash);
}

核心思路是计算部分移到web worker,让主线程专注其他工作,同时借用了wasm加速计算,在这附上源码地址

核心代码

多开web worker充分利用并发能力,之后将文件等分,每部分抽样后分配到不同的线程后计算hash,然后发回主线程合并。具体的实现可以参考源码,这个功能一定要快,不然等太久只会降低用户体验。

大量请求的并发上传

请求的并发控制方案有很多,我这边有试过发布订阅模式,Promise.all也试过,最终决定参考流行的并发库p-limit再改一份并发方案,添加暂停,继续,错误请求重试等功能,比较重点的是低优先级模式 ,用来解决当大量请求发起的时候,导致cpu上升,如果爆满后浏览器必然卡顿,无法优化,在有动画业务的时候会严重影响用户体验。附上源码地址

ts 复制代码
import { UploadHelper, createFileChunks } from 'large-file-upload';

async function uploadFile(file: File) {
  const { fileChunks } = await createFileChunks(file);
  const hash = await generateFileHash(file);

  const fileArr = fileChunks.map((chunk, index) => {
    return {
      blob: chunk,
      index,
    };
  });
  const uploadHelper = new UploadHelper(fileArr, {
    maxConcurrentTasks: 3,
  });

  uploadHelper.onProgressChange(progress => {
    console.log(`Progress: ${progress}/${fileChunks.length}`);
  });

  // Execute the upload tasks with an async function passed to run
  const { results, errorTasks } = await uploadHelper.run(async ({ data, signal }) => {
    const formData = new FormData();
    formData.append('chunk', data.blob);
    formData.append('index', data.index);
    formData.append('hash', hash);
    // Simulate an upload request using fetch or any HTTP client
    const response = await fetch('/upload', {
      method: 'POST',
      body: formData,
      signal,
    });

    if (!response.ok) {
      throw new Error(`Upload failed with status ${response.status}`);
    }

    return await response.json();
  });

  if (errorTasks.length > 0) {
    console.log('Some chunks failed to upload:', errorTasks);
    // Optionally retry failed tasks
    // await uploadHelper.retryTasks(errorTasks);
  } else {
    console.log('All chunks uploaded successfully');
  }
}

请求队列的实现参考的p-limit,使用链表进行实现,性能要更好一点。当单个任务完成后会自动请求下一个直至完成。基本主流的并发控制方案,我测过三四种,在时间上差不多太多,可能有些逻辑会更合理一点,如果需要自己实现的话,不用纠结。另外在这里指出用web worker发请求的方案,基本完全没用,时间上是基本一样的,性能,内存均没有太大提升,不用浪费时间,这里可以参考这个web worker方案实现

重点 并发方案实践下来最重要的点是如何避免cpu爆满导致电脑的卡顿,而不是浏览器的,这里采用requestIdleCallback来降低请求任务的优先级,同时设置保底时间,避免任务长时间不运行。

结尾

基本的流程已经总结完毕,这个库可以直接使用,如果用vite框架的话需要避免预构建,因为采用了web worker,作为库安装时会有资源路径不对的问题。

项目地址

这个库可能还有不足的地方,非常欢迎掘友们提出建议。如果有什么需求需要实现的,我也很乐意研究然后分享出来,可以的话帮忙给项目点点star,感激不尽。

相关推荐
zgscwxd3 小时前
thinkphp6模板调用URL方法生成的链接异常
前端·javascript·html
建群新人小猿3 小时前
退款成功订阅消息点击后提示订单不存在
java·开发语言·前端
y先森3 小时前
js实现导航栏鼠标移入时,下划线跟随鼠标滑动
开发语言·前端·javascript
加德霍克5 小时前
python高级之简单爬虫实现
前端·python·学习
we_前端全家桶6 小时前
小程序中模拟发信息输入框,让textarea可以设置最大宽以及根据输入的内容自动变高的方式
java·前端·小程序
京东菜鸟全球通快递小哥6 小时前
Axios取消重复请求,但能让最新请求作为最终返回,且能共享状态 ,不知小伙您有没有尝到真香~
前端·javascript·axios
NetX行者6 小时前
基于Vue3与ABP vNext 8.0框架实现耗时业务处理的进度条功能
前端·vue.js·进度条·状态模式
顾辰呀7 小时前
WebSocket实战,后台修改订单状态,前台实现数据变更,提供前端和后端多种语言
前端
林奇lc7 小时前
elementUI select,option变化,如果option不存在上次的选项,自动清空上次的选择
前端·vue.js·elementui