前端大文件分片上传与断点续传方案
## 一、背景与需求
在处理大文件上传时(如视频、设计稿等),传统单次上传方式存在以下问题:
- 网络波动导致上传失败需重新上传
- 服务器限制单次请求大小
- 无法暂停/恢复上传
- 上传进度难以跟踪
分片上传与断点续传方案能有效解决这些问题,提升用户体验和上传可靠性。
---
## 二、核心概念
### 1. 分片上传(Chunked Upload)
将大文件切割为多个小分片(如2MB/片),逐个上传分片,最后在服务端合并。
### 2. 断点续传(Resume Upload)
记录已上传分片信息,当上传中断后,再次上传时只需上传未完成的分片。
### 3. 秒传(Instant Upload)
通过文件哈希校验,若服务器已存在相同文件,则直接返回成功。
---
## 三、技术方案
### 前端实现流程
```mermaid
graph TD
A[选择文件] --> B(计算文件哈希)
B --> C{查询服务器状态}
C -->|已存在| D[秒传完成]
C -->|未存在| E[分片切割]
E --> F[并发上传分片]
F --> G[全部分片完成?]
G -->|否| F
G -->|是| H[通知合并]
H --> I[上传完成]
```
### 1. 文件分片处理
```javascript
// 计算文件哈希(使用SparkMD5库)
async function calculateFileHash(file) {
return new Promise((resolve) => {
const chunkSize = 2 * 1024 * 1024 // 2MB
const chunks = Math.ceil(file.size / chunkSize)
const spark = new SparkMD5.ArrayBuffer()
let currentChunk = 0
const loadNext = () => {
const start = currentChunk * chunkSize
const end = start + chunkSize >= file.size ? file.size : start + chunkSize
const fileReader = new FileReader()
fileReader.onload = e => {
spark.append(e.target.result)
currentChunk++
if (currentChunk < chunks) {
loadNext()
} else {
resolve(spark.end())
}
}
fileReader.readAsArrayBuffer(file.slice(start, end))
}
loadNext()
})
}
// 文件分片
function createFileChunks(file, chunkSize = 2 * 1024 * 1024) {
const chunks = []
let start = 0
while (start < file.size) {
const end = Math.min(start + chunkSize, file.size)
chunks.push({
index: chunks.length,
file: file.slice(start, end)
})
start = end
}
return chunks
}
```
### 2. 断点续传实现
```javascript
class Uploader {
constructor(file) {
this.file = file
this.chunkSize = 2 * 1024 * 1024 // 2MB
this.chunks = []
this.hash = ''
this.uploadedChunks = new Set() // 已上传分片索引
}
async init() {
this.hash = await calculateFileHash(this.file)
this.chunks = createFileChunks(this.file, this.chunkSize)
// 从localStorage恢复进度
const savedProgress = localStorage.getItem(this.hash)
if (savedProgress) {
this.uploadedChunks = new Set(JSON.parse(savedProgress))
}
}
async upload() {
// 查询服务器上传状态
const { uploaded } = await checkUploadStatus(this.hash)
if (uploaded) return // 秒传
// 过滤未上传分片
const pendingChunks = this.chunks.filter(
chunk => !this.uploadedChunks.has(chunk.index)
)
// 并发控制(最大5个并行)
const pool = new ConcurrentPool(5)
await pool.addTasks(pendingChunks.map(chunk =>
() => this.uploadChunk(chunk)
))
// 通知合并
await mergeChunks(this.hash, this.file.name)
}
async uploadChunk(chunk) {
const formData = new FormData()
formData.append('hash', this.hash)
formData.append('index', chunk.index)
formData.append('file', chunk.file)
await axios.post('/api/upload', formData)
// 更新进度
this.uploadedChunks.add(chunk.index)
localStorage.setItem(this.hash, JSON.stringify([...this.uploadedChunks]))
}
}
```
### 3. 服务端接口设计
| 接口 | 方法 | 参数 | 功能 |
|---------------------|------|--------------------------|-------------------------|
| /api/check | GET | hash | 检查文件上传状态 |
| /api/upload | POST | hash, index, file | 上传分片 |
| /api/merge | POST | hash, filename | 合并分片 |
---
## 四、关键优化点
1. **分片大小选择**
- 2-5MB 平衡网络效率和分片数量
- 根据网络质量动态调整
2. **哈希计算优化**
- Web Worker 中计算避免阻塞主线程
- 抽样计算哈希(首尾各取2MB + 中间随机取3段)
3. **并发控制**
- 限制并行上传数(通常3-5个)
- 失败自动重试(最多3次)
4. **进度保存**
- 使用localStorage/IndexedDB存储进度
- 服务端记录已上传分片
5. **错误处理**
- 网络中断自动重连
- 分片MD5校验防止损坏
- 服务端定时清理未完成的分片
---
## 五、服务端示例(Node.js)
```javascript
// 检查上传状态
router.get('/check', async (ctx) => {
const { hash } = ctx.query
const chunkDir = path.join(UPLOAD_DIR, hash)
// 1. 检查文件是否已存在
if (fs.existsSync(path.join(UPLOAD_DIR, hash + '.file'))) {
return ctx.body = { code: 200, uploaded: true }
}
// 2. 返回已上传分片
const uploaded = fs.existsSync(chunkDir)
? fs.readdirSync(chunkDir).map(Number)
: []
ctx.body = { code: 200, uploaded }
})
// 分片上传
router.post('/upload', async (ctx) => {
const { hash, index } = ctx.request.body
const file = ctx.request.files.file
const chunkDir = path.join(UPLOAD_DIR, hash)
if (!fs.existsSync(chunkDir)) {
fs.mkdirSync(chunkDir)
}
await fs.promises.rename(
file.path,
path.join(chunkDir, index)
)
ctx.body = { code: 200 }
})
// 合并分片
router.post('/merge', async (ctx) => {
const { hash, filename } = ctx.request.body
const chunkDir = path.join(UPLOAD_DIR, hash)
const chunks = fs.readdirSync(chunkDir)
.sort((a, b) => a - b)
await Promise.all(
chunks.map((chunkPath, index) =>
fs.promises.appendFile(
path.join(UPLOAD_DIR, hash + '.file'),
fs.readFileSync(path.join(chunkDir, chunkPath))
)
)
)
// 清理分片目录
fs.rmdirSync(chunkDir, { recursive: true })
ctx.body = { code: 200 }
})
```
---
## 六、注意事项
1. **安全性**
- 限制文件类型和大小
- 服务端校验分片MD5
- 防止目录遍历攻击
2. **性能优化**
- 使用CDN加速分片上传
- 服务端分片合并使用流式操作
3. **浏览器兼容性**
- File API 兼容到IE10+
- 使用axios等库处理上传
---
## 七、总结
该方案通过分片上传解决大文件传输问题,结合断点续传提升可靠性,主要优势包括:
- 支持任意大小文件上传
- 网络中断后可恢复
- 实时进度反馈
- 减少服务器压力
实际部署时需要根据业务需求调整分片策略和并发控制参数。
```