开箱即用的大文件上传库

前言

部分场景需要大文件切片上传的方案。因为公司涉及到很多模型和视频需要上传,本来我以为这个功能很简单,最开始做的能用就行,在不断实践下发现这个功能可以深挖下去,最终封装了一个足够完善的库 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,感激不尽。

相关推荐
谈不譚网安1 分钟前
CSRF请求伪造
前端·网络安全·csrf
TT模板7 分钟前
苹果cmsV10主题 MXonePro二开优化修复开源版
前端·html5
拖孩7 分钟前
【Nova UI】十一、组件库中 Icon 组件的测试、使用与全局注册全攻略
前端·javascript·vue.js·ui·sass
去伪存真13 分钟前
不用动脑,手把手跟着我做,就能掌握Gitlab+Jenkins提交代码自动构部署
前端·jenkins
天天扭码1 小时前
深入解析 JavaScript 中的每一类函数:从语法到对比,全面掌握适用场景
前端·javascript·面试
小希爸爸1 小时前
4、中医基础入门和养生
前端·后端
自由鬼1 小时前
开源AI开发工具:OpenAI Codex CLI
人工智能·ai·开源·软件构建·开源软件·个人开发
kooboo china.1 小时前
Tailwind CSS 实战:基于 Kooboo 构建企业官网页面(一)
前端·css·编辑器
uhakadotcom1 小时前
Fluid:云原生数据加速与管理的简单入门与实战
前端
鬼面瓷2 小时前
CAPL编程_03
前端·数据库·笔记