前端大文件切片上传怎么做?

一、大文件切片上传怎么设计?

大文件上传我一般会拆成:切片、并发上传、断点续传、秒传、失败重试和最终校验这几块,核心是让上传可以暂停、失败后继续,而且不用从头再传。


面试官继续追问后的展开

1. 为什么要切片?

如果一个 1.5GB 视频直接上传:

text 复制代码
1.5GB File
   ↓
一个 HTTP 请求
   ↓
上传失败
   ↓
基本只能重新上传

切片之后:

text 复制代码
1.5GB File
   ↓
┌────┬────┬────┬────┬────┬────┐
│ 0  │ 1  │ 2  │ 3  │ ...│ N  │
└────┴────┴────┴────┴────┴────┘
   ↓
分别上传
   ↓
服务端保存各个 chunk
   ↓
全部完成
   ↓
按顺序合并

前端核心 API 就是:

js 复制代码
const chunk = file.slice(start, end);

但 slice() 本身根本不是这道题的难点。

真正的难点是:

切完以后,怎么可靠地把这些 chunk 上传、恢复、校验并最终合并。


2. 切片之后为什么不能直接 Promise.all()?

这是第一个高频追问。

比如:

js 复制代码
await Promise.all(
  chunks.map(chunk => upload(chunk))
);

代码虽然简单,但实际上等于:

有多少切片就同时发多少请求。但不应该一次性把所有 chunk 都启动上传。

如果一个 1GB 文件按 1MB 切,就是大约 1024 个请求。

这会带来:

  • 浏览器连接和请求调度压力
  • 服务端瞬时并发压力
  • 带宽竞争
  • 大量 Promise 和请求对象
  • 某个网络环境下整体吞吐反而下降

所以实际应该做:固定并发数 + 任务队列。

例如:

text 复制代码
          chunk queue
               │
       ┌───────┼───────┐
       ↓       ↓       ↓
    worker1  worker2  worker3
       ↓       ↓       ↓
    chunk0   chunk1   chunk2
       ↓
    完成后继续取下一个

例如限制:

js 复制代码
const CONCURRENCY = 4;

4 个请求完成一个,再从队列取下一个。

这里还有一个容易被问的点

并发数不是固定的"4~6 个最佳"。

它取决于:

  • HTTP 版本
  • 浏览器
  • 网络质量
  • chunk 大小
  • 服务端处理能力
  • 带宽
  • 是否走 CDN / 对象存储

所以高级回答不要说:

HTTP/1.1 最多只能 6 个,所以设置 6。

更准确的说法是:

HTTP/1.1 同源连接数受浏览器限制,而 HTTP/2 可以在单连接上复用多个请求;实际并发数还是应该根据网络和服务端吞吐做控制,而不是死记一个数字。


3. 怎么实现断点续传?

这是这道题真正开始拉开差距的地方。

错误思路

text 复制代码
浏览器 localStorage
    ↓
记录 chunk 0、1、2、3 已上传

这个方案只能解决:

同一台设备、同一个浏览器、缓存没有被清掉的情况下恢复。

但它解决不了:

text 复制代码
清缓存
换浏览器
换电脑
换手机
重新登录

所以断点续传真正应该以服务端状态为准。


正确流程

用户选择文件:

text 复制代码
File
 ↓
计算 fileHash
 ↓
POST /upload/check
      │
      ├── 文件已经存在 → 秒传
      │
      └── 文件不存在
             ↓
       返回已上传 chunks
             ↓
       前端过滤已上传 chunk
             ↓
       只上传缺失 chunk

例如服务端返回:

json 复制代码
{
  "uploadedChunks": [0, 1, 2, 5, 6]
}

那么:

text 复制代码
0 ✓
1 ✓
2 ✓
3 → 上传
4 → 上传
5 ✓
6 ✓
7 → 上传

这样网络断在 99%,重新上传的时候:不是从 0 开始,而是继续上传缺失部分。


4. 服务端怎么知道这是哪个文件?

这里就需要一个非常关键的概念:

文件唯一标识

通常可以基于:

text 复制代码
fileHash

再结合:

text 复制代码
chunkIndex

来定位 chunk。

例如:

text 复制代码
fileHash = abc123

chunk:
abc123_0
abc123_1
abc123_2
...

或者服务端使用:

text 复制代码
uploadId + chunkIndex

这种设计通常更稳妥。

因为:

文件身份和一次上传任务的身份,不一定应该完全绑定在一起。

例如同一个文件:

text 复制代码
fileHash = abc123

可能同时出现:

text 复制代码
uploadId=A
uploadId=B

这就涉及后面的并发上传同一个文件问题。


5. 秒传到底是什么?

秒传本质上不是"上传很快"。而是:

服务端发现自己已经有这个文件,所以直接复用已有文件,不再上传文件内容。

流程:

text 复制代码
用户选择文件
      ↓
计算 fileHash
      ↓
POST /upload/check
      ↓
服务端查询 fileHash
      ↓
存在
      ↓
直接返回成功

所以:

text 复制代码
1.5GB 文件
      ↓
只需要上传/计算文件指纹
      ↓
服务端发现已有
      ↓
直接完成

这才叫秒传。


6. 那么 fileHash 怎么算?

这里又是一个很容易被忽略的性能问题。如果你直接:

text 复制代码
读取整个 1.5GB 文件
        ↓
计算 MD5

就可能造成:

  • CPU 消耗
  • 主线程卡顿
  • 内存压力
  • 用户界面卡顿

所以实际可以:

text 复制代码
File
 ↓
分块读取
 ↓
Hash Worker
 ↓
计算 fileHash

也可以使用:

text 复制代码
Web Worker

把计算放到 Worker 中,避免阻塞主线程。


7. 文件 Hash 和 Chunk Hash 是一回事吗?

不是。 可以分别存在:

text 复制代码
fileHash

和:

text 复制代码
chunkHash

例如:

text 复制代码
fileHash = SHA256(file)

chunk0Hash = SHA256(chunk0)
chunk1Hash = SHA256(chunk1)
chunk2Hash = SHA256(chunk2)

这样服务端可以进一步校验:

text 复制代码
上传 chunk
      ↓
服务端计算/校验 chunkHash
      ↓
一致 → 接收
不一致 → 重新上传这个 chunk

这样比等到整个文件合并之后才发现错误更加高效。


8. 如果某一个切片上传失败怎么办?

不能因为一个 chunk 失败:

text 复制代码
chunk 0 ✓
chunk 1 ✓
chunk 2 ✗
chunk 3 ✓
chunk 4 ✓

然后:

text 复制代码
全部重新上传

应该:只重试失败的 chunk。

例如:

text 复制代码
chunk2
  ↓
失败
  ↓
retry 1
  ↓
失败
  ↓
retry 2
  ↓
失败
  ↓
retry 3
  ↓
仍然失败
  ↓
任务失败

重试一般配合:

text 复制代码
指数退避

避免网络异常的时候大量请求同时再次打到服务端。


9. 网络断了,怎么恢复?

这里其实有两种情况。

情况一:页面没刷新

前端可以保留当前上传任务状态:

text 复制代码
uploadId
fileHash
uploadedChunks

网络恢复之后:

text 复制代码
重新查询服务端
      ↓
获取最新 uploadedChunks
      ↓
继续上传缺失 chunk

情况二:页面刷新甚至换设备

这时候前端自己的内存状态全部没了。所以仍然依赖:

text 复制代码
fileHash
   ↓
服务端查询
   ↓
返回已经存在的 chunks
   ↓
继续上传

这也是为什么:

localStorage 最多只能做客户端辅助缓存,不能作为断点续传的最终依据。


10. 两个用户同时上传同一个文件怎么办?

这是非常好的追问。

例如:

text 复制代码
User A ── upload ──┐
                   ├── fileHash = abc123
User B ── upload ──┘

服务端不能简单认为:

text 复制代码
A 创建文件
B 又创建一个完全相同的文件

应该围绕:

text 复制代码
fileHash

做去重。例如:

text 复制代码
fileHash = abc123
        ↓
服务端发现已经存在
        ↓
直接复用

但这里又有一个更深的问题:

文件可能正在上传,还没有最终完成。

例如:

text 复制代码
User A
chunk 0 ✓
chunk 1 ✓
chunk 2 ✓
...

User B
        ↓
发现 abc123 正在上传
        ↓
复用已有上传进度

这就涉及:

  • upload session
  • 状态机
  • 并发控制
  • 幂等
  • 数据库唯一约束
  • 分布式锁 / CAS

11. Chunk 上传接口为什么必须幂等?

例如:

http 复制代码
POST /upload/chunk

网络情况可能是:

text 复制代码
客户端发送 chunk
       ↓
服务端已经成功保存
       ↓
响应丢失
       ↓
客户端以为失败
       ↓
再次上传同一个 chunk

如果服务端没有幂等设计,就可能:

text 复制代码
重复保存

所以应该让:

text 复制代码
uploadId + chunkIndex

或者:

text 复制代码
fileHash + chunkIndex

成为唯一定位。再次上传相同 chunk 时:

text 复制代码
已经存在 → 直接返回成功
不存在 → 保存

这就是高级工程设计里非常重要的:幂等性。


12. 所有 Chunk 都上传完之后呢?

这时候才进入:

text 复制代码
Merge

流程:

text 复制代码
chunk0 ✓
chunk1 ✓
chunk2 ✓
...
chunkN ✓
       ↓
POST /upload/complete
       ↓
服务端确认所有 chunk 都存在
       ↓
按 chunkIndex 顺序合并
       ↓
生成最终文件

注意:前端不能简单告诉服务端"我上传完了",服务端应该自己再次确认。


13. 合并的时候怎么保证顺序?

例如:

text 复制代码
chunk0
chunk1
chunk2
chunk3

必须按照:

text 复制代码
0 → 1 → 2 → 3

合并。不能按照上传完成顺序:

text 复制代码
2 → 0 → 3 → 1

因为:

上传顺序和文件顺序是两回事。

这也是为什么每个 chunk 必须带:

text 复制代码
chunkIndex

14. 合并完成后怎么校验完整性?

这是非常关键的一问。

最直接的方案是:

text 复制代码
原文件 fileHash
        ↓
上传 + 合并
        ↓
服务端对最终文件计算 hash
        ↓
比较
        ↓
一致 → 成功
不一致 → 异常

例如:

text 复制代码
Client:
SHA-256(originalFile)
        ↓
abc123

Server:
SHA-256(mergedFile)
        ↓
abc123

        ↓
一致
        ↓
上传成功

15. 如果最终 Hash 不一致,是全部重传还是部分重传?

不能简单地说"部分重传"。

因为:

text 复制代码
fileHash 不一致

只能说明:

最终文件和原文件不一致。

它本身不能直接告诉你:

"到底是 chunk 37 错了。"

如果每个 chunk 都有:

text 复制代码
chunkHash

服务端就可以进一步定位:

text 复制代码
chunk0 ✓
chunk1 ✓
chunk2 ✓
chunk3 ✗
...

那么:

只重新上传 chunk3。

所以更完整的设计是:

text 复制代码
                    fileHash
                       │
                       ↓
                 整体完整性校验
                       │
                  不一致?
                       │
                       ↓
                chunkHash 对比
                       │
              ┌────────┴────────┐
              ↓                 ↓
          找到异常 chunk      无法定位
              ↓                 ↓
          部分重传          整体重新处理

16. 合并是不是一定要同步完成?

不一定。如果是:

text 复制代码
10MB 文件

同步合并可能完全没问题。

但如果是:

text 复制代码
10GB
50GB
100GB

合并可能持续很长时间。这时候更合理的是:

text 复制代码
POST /upload/complete
        ↓
创建 merge task
        ↓
立即返回
        ↓
后台异步合并
        ↓
状态:merging
        ↓
完成
        ↓
状态:completed

前端可以:

text 复制代码
轮询

或者:

text 复制代码
WebSocket
SSE

接收状态变化。

注意:WebSocket/SSE 不是大文件上传本身的必要条件,只是用于把异步合并状态通知给前端。


17. 整个链路怎么串起来?

这是这道题最值得背下来的一张图:

text 复制代码
                    用户选择文件
                         │
                         ↓
                 计算 fileHash
                         │
                         ↓
                  /upload/check
                         │
              ┌──────────┴──────────┐
              ↓                     ↓
        文件已经存在             文件不存在
              ↓                     ↓
             秒传              返回 uploadId
                                      │
                                      ↓
                              查询已上传 chunks
                                      │
                                      ↓
                               过滤已上传 chunk
                                      │
                                      ↓
                              并发队列上传
                                      │
                    ┌─────────────────┴──────────────┐
                    ↓                                ↓
                 成功                            失败
                    │                                │
                    │                         自动重试/指数退避
                    │                                │
                    └──────────────┬─────────────────┘
                                   ↓
                              全部 chunk 完成
                                   │
                                   ↓
                            /upload/complete
                                   │
                                   ↓
                              服务端合并
                                   │
                                   ↓
                           最终文件完整性校验
                                   │
                         ┌─────────┴─────────┐
                         ↓                   ↓
                       一致                不一致
                         ↓                   ↓
                       成功            定位异常 chunk
                                             ↓
                                           重传

18. 这道题真正考什么?

可以把面试官的追问链压缩成:

text 复制代码
会切片
  ↓
会并发控制吗?
  ↓
会断点续传吗?
  ↓
换电脑还能续传吗?
  ↓
能秒传吗?
  ↓
两个用户同时上传呢?
  ↓
请求重试会不会重复?
  ↓
合并怎么保证顺序?
  ↓
怎么校验完整性?
  ↓
校验失败怎么定位?
  ↓
大文件合并会不会阻塞?
  ↓
异常状态怎么恢复?

所以它考的其实不是:

Blob.slice() 会不会。

而是:

你能不能把一个"文件上传 API"设计成一个可靠的端到端系统。


19. 面试官最容易继续追问的几个点

追问 1:localStorage 能不能实现断点续传?

不能作为最终方案。

它只能保存客户端状态,清缓存、换浏览器、换设备都会丢。

真正的上传进度应该以服务端已保存的 chunk 状态为准。


追问 2:为什么不用 Promise.all()?

因为切片数量可能非常大,不能让所有请求同时发出去,要用并发队列控制同时上传的数量。


追问 3:为什么需要 chunkIndex?

因为上传完成顺序是不确定的,服务端必须靠 chunkIndex 恢复文件原来的顺序。


追问 4:为什么需要 fileHash?

主要解决两个问题:

text 复制代码
1. 判断是不是同一个文件
2. 实现秒传和断点续传

追问 5:为什么还需要 chunkHash?

因为 fileHash 只能告诉你最终文件对不对,chunkHash 可以进一步定位到底哪个切片有问题。


追问 6:上传接口为什么要幂等?

因为:

text 复制代码
请求成功
↓
响应丢失
↓
客户端重试

可能导致同一个 chunk 被重复上传。所以服务端要能够安全处理重复请求。


追问 7:HTTP/2 之后还需要控制并发吗?

需要。 HTTP/2 解决的是多个请求共享连接的问题,但不代表服务端、浏览器和网络可以无限并发。并发控制依然是为了控制:

text 复制代码
带宽
CPU
内存
服务端压力
请求队列

20. 最后给你一版真正适合面试开口的答案

大文件上传我一般不会只考虑"切片",而是把它做成一套完整的上传链路:切片、并发控制、断点续传、秒传、失败重试、合并和完整性校验。

前端先用 Blob.slice() 把文件切成多个 chunk,每个 chunk 带上 uploadId、chunkIndex 等信息,然后通过并发队列控制同时上传的请求数量,不能直接 Promise.all() 把所有切片一起发出去。

断点续传不能只依赖 localStorage,真正应该以服务端状态为准。用户重新上传时,先根据 fileHash 查询服务端已经有哪些 chunk,只上传缺失的部分,所以即使刷新页面、清缓存甚至换一台电脑,也能继续。

秒传也是基于 fileHash:服务端如果已经存在这个文件,就直接复用已有文件,不需要再次上传内容。

上传过程中,单个 chunk 失败只重试这个 chunk,并且接口要保证幂等,避免网络重试造成重复数据。

所有 chunk 上传完成后,再通知服务端按 chunkIndex 顺序合并。合并完成后校验最终文件的 fileHash;如果不一致,再通过 chunkHash 定位异常 chunk,优先做部分重传。

所以这道题真正考的不是会不会 slice(),而是能不能把大文件上传做成一个可恢复、可重试、可校验、能处理并发和异常的完整方案。

相关推荐
Sand(ContextGate)2 小时前
Python Agent 测试实战:测试与评估,让 Agent 像传统软件一样可交付
前端·javascript·python·microsoft·ai
广州华水科技2 小时前
单北斗GNSS变形监测系统在城市安全与地质灾害中的应用前景
前端
颜进强2 小时前
20 · NestJs循环依赖与 forwardRef:容器为什么在环面前会死,"占位再补齐"怎么救
前端·后端·ai编程
颜进强2 小时前
19 · NestJs @Global 落地账:装饰器与 `isGlobal` 参数,各在什么场景上岗
前端·后端·ai编程
码艺-Alimjan2 小时前
Tauri 网站To桌面应用实战总结
前端·javascript·vue
颜进强2 小时前
18 · NestJS动态模块与 forRoot:`imports: [ConfigModule.forRoot({...})]` 到底在 import 什么
前端·后端·ai编程
YZ1225523 小时前
【Docker专题】使用Docker部署Vue-Flask项目前后端分离版【前端Docker部署】
前端·vue.js·docker
liangshanbo12153 小时前
浏览器渲染过程:高级前端面试题
前端
太子釢3 小时前
React 函数组件与 Hook 实践指南
前端·react.js