大文件上传:基本流程

前端处理流程

文件分片处理

客户端需要承担两件重要的事:

  1. 对文件进行分片,并计算每个分片的hash值
  2. 根据所有分片的hash值,计算整个文件的hash值

客户端使用File API读取文件,计算文件哈希值(如MD5)作为唯一标识。通过Blob.prototype.slice将大文件切割为固定大小的分片(一般取 1MB - 10MB),并记录分片序号。

而计算 Hash 是 CPU 密集型的操作,如果不加处理将会导致主线程长时间阻塞

大文件的分片数量一般比较多,在主进程里面切分文件的话,肯定会卡的。于是使用 Web Worker 多线程切片,处理完后交给主进程发送

切完后可将 blob 存储到 IndexedDB,下次用户进来时,先嗅探一下是否存在未完成上传的切片。如果有,则尝试继续上传
另外就是,可以引入 WebSocket 实现实时通知和请求序列的控制


上传分片

由于切片文件太多,浏览器没法同时发送(过多的请求不会提升上传速度,反而会给浏览器带来负担)。所以采用并发上传策略(如限制 6 个并行请求),通过Promise.all或队列管理分片上传顺序,等到其中一个请求有返回结果了,再发起一个新的请求,以此类推,直到所有请求发送完毕

每个分片上传时需要先包装为 FormData 对象,携带分片哈希、分片序号等元数据。

Q:如何避免上传重复分片
服务端记录已接收的分片标识(hash 值),每个分片上传前先查询标识是否存在,存在则跳过上传

在这里插入图片描述
(注意,第三个图中的第二步那里应该是 hash2,图中写错了)


实现断点续传

本地存储(如localStorageIndexedDB)记录已上传分片信息。上传前先请求服务端接口校验哪些分片已存在,跳过已上传分片。


进度监控
通过axiosfetchonUploadProgress事件监听单个分片上传进度,汇总计算整体进度并更新UI。

// 示例:分片上传代码
const chunkSize = 2 * 1024 * 1024;
const chunks = Math.ceil(file.size / chunkSize);
const uploadChunk = async (chunk, index) => {
  const formData = new FormData();
  formData.append('chunk', chunk);
  formData.append('hash', fileHash);
  formData.append('index', index);
  return axios.post('/upload', formData, {
    onUploadProgress: (e) => {
      console.log(`分片${index}进度: ${Math.round((e.loaded / e.total) * 100)}%`);
    }
  });
};

动态分片

动态分片是一种根据网络状况、设备性能等实时条件动态调整文件分片大小的技术,旨在优化大文件上传的效率和稳定性

高速网络:增大分片(如 20MB),减少分片数量以降低请求开销。
弱网环境:减小分片(如 1MB),提升容错能力

Axios 请求配置中,onUploadProgress 绑定一个回调函数,当上传数据时,浏览器会定期触发该回调,并传入一个 progressEvent 对象,包含以下关键属性:

loaded:已上传的字节数。
total:文件总字节数(需服务器返回 Content-Length 头部时才有效)。
lengthComputable:布尔值,表示是否能计算进度(依赖 total 是否有效)

可以通过 AxiosonUploadProgress 计算实时上传速度
通过两次进度事件的时间差(Δt)和已上传字节数的差值(Δloaded),计算瞬时速度。但是直接计算瞬时速度可能因网络波动导致数值抖动,可采用指数移动平均(EMA)算法对连续的速度值加权平均,从而减少突变,获得平滑速度

let lastLoaded = 0;
let lastTime = Date.now();
let smoothSpeed = 0;
const alpha = 0.2; // 平滑系数(0-1,越小越平滑)

axios.post('/upload', formData, {
  onUploadProgress: (progressEvent) => {
    const currentTime = Date.now();
    const timeDelta = (currentTime - lastTime) / 1000;
    const loadedDelta = progressEvent.loaded - lastLoaded;
    const instantSpeed = loadedDelta / timeDelta;

    // EMA平滑
    smoothSpeed = alpha * instantSpeed + (1 - alpha) * smoothSpeed;

    console.log(`平滑速度: ${(smoothSpeed / 1024).toFixed(2)} KB/s`);
    lastLoaded = progressEvent.loaded;
    lastTime = currentTime;
  }
});

得到平滑速度之后,就可以动态调整分片大小,具体的动态调整策略如下:
其中 T 指的是分片预期上传完成的时间
在这里插入图片描述
(跟 TCP 有点像)

我们在上文中提到,上传分片采用并发上传的策略,在多线程的环境下:
线程间可以通过 BroadcastChannel 或主线程广播EMA速度来共享 EMA 数据(即平滑速度),确保所有 Worker 线程使用统一的速度基准
也可以采用分片任务分配的策略,就是主线程根据 EMA 速度动态分配分片大小,Worker 线程仅负责上传

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值