图片上传中的切片上传(分片上传) 是针对大文件(如几十 MB 到 GB 级)的优化方案,核心思想是:将大文件分割成多个小 “切片”(Chunk),分别上传到服务器,最后由服务器将所有切片合并成原始文件。
这种方式解决了大文件上传的三大痛点:
1、单次请求体积过大导致的超时或失败;
2、网络中断后需要重新上传整个文件的问题(支持断点续传);
3、并行上传多个切片,提高上传效率。
一、前端实现(核心步骤)
前端负责文件分割、切片标识、并发上传、断点续传控制。
1. 基本准备
- 获取用户选择的文件(通过 <input type="file">);
- 定义切片大小(如 1MB / 片,可根据业务调整);
- 生成文件唯一标识(用于服务器识别同一文件的切片,避免混淆)。
// 获取文件
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
// 配置
const chunkSize = 1024 * 1024; // 1MB/切片
const totalChunks = Math.ceil(file.size / chunkSize); // 总切片数
const fileHash = await generateFileHash(file); // 文件唯一标识(见步骤2)
});
2. 生成文件唯一标识(关键)
需要给同一文件的所有切片分配一个共同的 “父标识”,确保服务器能正确合并。常用方式:
- 基于文件名 + 文件大小 + 最后修改时间生成 hash;
- 读取文件内容生成 MD5/SHA-1(更严谨,但大文件计算耗时)。
// 简化版:基于文件名+大小+最后修改时间生成hash
function generateFileHash(file) {
const { name, size, lastModified } = file;
return `${name}-${size}-${lastModified}`; // 实际项目可加MD5加密
}
3. 分割文件为切片
使用 File.prototype.slice() 方法分割文件(类似字符串 slice,不修改原文件)。
// 分割文件为切片
function createChunks(file, chunkSize, fileHash) {
const chunks = [];
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
// 每个切片包含:文件标识、切片索引、切片内容、总切片数
chunks.push({
fileHash,
chunkIndex: i,
totalChunks,
chunk: file.slice(start, end) // 切片内容(Blob对象)
});
}
return chunks;
}
4. 并发上传切片
- 同时上传多个切片(控制并发数,避免请求过多);
- 每个切片通过 FormData 发送(包含切片信息和二进制内容)。
// 并发上传(控制最大并发数为3)
async function uploadChunks(chunks) {
const maxConcurrent = 3; // 最大并发数
const results = [];
// 按并发数分组上传
for (let i = 0; i < Math.ceil(chunks.length / maxConcurrent); i++) {
const group = chunks.slice(i * maxConcurrent, (i + 1) * maxConcurrent);
// 并发上传当前组
const groupResults = await Promise.all(
group.map(chunk => uploadSingleChunk(chunk))
);
results.push(...groupResults);
}
return results;
}
// 上传单个切片
async function uploadSingleChunk(chunk) {
const formData = new FormData();
formData.append('fileHash', chunk.fileHash);
formData.append('chunkIndex', chunk.chunkIndex);
formData.append('totalChunks', chunk.totalChunks);
formData.append('chunk', chunk.chunk); // 二进制切片
const response = await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
});
return response.json();
}
5. 断点续传(可选但重要)
上传前先查询服务器 “已上传的切片索引”,只上传缺失的部分:
// 检查已上传的切片
async function getUploadedChunks(fileHash) {
const response = await fetch(`/api/check-chunks?fileHash=${fileHash}`);
const { uploadedIndexes } = await response.json(); // 服务器返回已上传的索引数组
return uploadedIndexes;
}
// 断点续传逻辑
async function resumeUpload(file) {
const fileHash = await generateFileHash(file);
const uploadedIndexes = await getUploadedChunks(fileHash); // 获取已传切片
const allChunks = createChunks(file, chunkSize, fileHash);
// 过滤出未上传的切片
const needUploadChunks = allChunks.filter(
chunk => !uploadedIndexes.includes(chunk.chunkIndex)
);
await uploadChunks(needUploadChunks); // 只传缺失的
}
6. 通知服务器合并切片
所有切片上传完成后,调用 “合并接口”,让服务器将切片按顺序拼接成原始文件。
// 通知服务器合并切片
async function mergeChunks(fileHash, fileName) {
const response = await fetch('/api/merge-chunks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileHash,
fileName, // 原始文件名(用于保存)
totalChunks // 总切片数(校验是否完整)
})
});
return response.json();
}
二、后端实现(核心步骤)
后端负责接收切片、临时存储、校验完整性、合并切片(以 Node.js 为例)。
1. 接收切片并临时存储
- 为每个文件创建单独的临时目录(以fileHash命名);
- 切片文件以chunkIndex命名(如0、1、2...),方便后续按顺序合并。
// Node.js + Express 示例(接收切片)
const express = require('express');
const multer = require('multer');
const fs = require('fs-extra'); // 增强版fs,支持递归创建目录
const path = require('path');
const app = express();
const upload = multer({ dest: 'temp/' }); // 临时存储目录
// 接收切片接口
app.post('/api/upload-chunk', upload.single('chunk'), async (req, res) => {
const { fileHash, chunkIndex } = req.body;
const chunkFile = req.file; // 上传的切片文件
// 创建临时目录(如:temp/abc123/)
const tempDir = path.join('temp', fileHash);
await fs.ensureDir(tempDir);
// 将切片移动到临时目录,命名为chunkIndex(如:temp/abc123/0)
const targetPath = path.join(tempDir, chunkIndex);
await fs.move(chunkFile.path, targetPath);
res.json({ code: 0, message: '切片上传成功' });
});
2. 检查已上传的切片(支持断点续传)
查询临时目录中已存在的切片索引,返回给前端。
// 检查已上传切片接口
app.get('/api/check-chunks', async (req, res) => {
const { fileHash } = req.query;
const tempDir = path.join('temp', fileHash);
// 目录不存在,说明未上传过
if (!await fs.exists(tempDir)) {
return res.json({ uploadedIndexes: [] });
}
// 读取目录中的所有切片文件,提取索引
const chunkFiles = await fs.readdir(tempDir);
const uploadedIndexes = chunkFiles.map(name => parseInt(name, 10));
res.json({ uploadedIndexes });
});
3. 合并切片为原始文件
- 按chunkIndex从小到大的顺序读取所有切片;
- 将切片内容依次写入目标文件;
- 合并完成后删除临时目录。
// 合并切片接口
app.post('/api/merge-chunks', async (req, res) => {
const { fileHash, fileName, totalChunks } = req.body;
const tempDir = path.join('temp', fileHash);
const targetPath = path.join('uploads', fileName); // 最终保存路径
// 检查临时目录是否存在
if (!await fs.exists(tempDir)) {
return res.status(400).json({ code: 1, message: '切片不存在' });
}
// 读取所有切片,按索引排序
const chunkFiles = await fs.readdir(tempDir);
if (chunkFiles.length !== totalChunks) {
return res.status(400).json({ code: 1, message: '切片不完整' });
}
// 按数字排序(确保0,1,2...顺序)
chunkFiles.sort((a, b) => parseInt(a) - parseInt(b));
// 创建目标文件的写入流
const writeStream = fs.createWriteStream(targetPath);
// 按顺序拼接切片
for (const chunkIndex of chunkFiles) {
const chunkPath = path.join(tempDir, chunkIndex);
// 读取切片内容并写入目标文件
await new Promise((resolve, reject) => {
const readStream = fs.createReadStream(chunkPath);
readStream.pipe(writeStream, { end: false }); // 不自动关闭写入流
readStream.on('end', resolve);
readStream.on('error', reject);
});
}
// 所有切片写入完成,关闭流
writeStream.end();
// 删除临时目录
await fs.remove(tempDir);
res.json({ code: 0, message: '文件合并成功', url: `/uploads/${fileName}` });
});
三、关键注意事项
- 切片校验:
可给每个切片生成 MD5,上传时携带,服务器接收后校验,避免切片损坏。 - 并发控制:
前端控制并发数(如 3-5 个),避免同时发送过多请求导致服务器压力过大。 - 大文件 hash 优化:
若通过文件内容生成 hash,可只读取前 100KB + 中间 100KB + 最后 100KB 计算,减少大文件 hash 计算耗时。 - 超时与重试:
单个切片上传失败时,实现自动重试机制(如最多重试 3 次)。 - 文件清理:
服务器定期清理长期未合并的临时切片(如 24 小时未完成合并),释放磁盘空间。》
切片上传的核心是 “分而治之”:前端将大文件拆分为小切片并行上传,后端接收后按顺序合并。配合断点续传,可大幅提升大文件上传的稳定性和效率,是企业级文件上传场景的标配方案。
四、可用的轮子
有很多成熟的 npm 库可以简化大文件切片上传的实现,无需从零开发。这些库通常封装了切片分割、并发控制、断点续传、进度监控等核心功能,以下是几个常用的库:
1.resumable.js(经典老牌库)
- 特点:支持断点续传、并发上传、暂停 / 继续功能,兼容性好(支持 IE8+),使用简单。
- 原理:基于 HTML5 File API 实现切片,通过 XMLHttpRequest 上传,后端需配合实现接收和合并逻辑。
- 使用示例:
<input type="file" id="fileInput" />
<script src="node_modules/resumable.js/resumable.js"></script>
<script>
const resumable = new Resumable({
target: '/api/upload', // 后端接收接口
chunkSize: 1*1024*1024, // 1MB 切片
simultaneousUploads: 3, // 并发数
testChunks: true, // 开启断点续传(先检查已上传切片)
throttleProgressCallbacks: 1,
});
// 绑定文件选择
document.getElementById('fileInput').addEventListener('change', (e) => {
resumable.addFiles(e.target.files);
});
// 开始上传
resumable.upload();
// 监控进度
resumable.on('progress', () => {
console.log('进度:', resumable.progress() * 100 + '%');
});
// 上传完成
resumable.on('complete', () => {
console.log('上传完成');
});
</script>
- 后端适配:需要根据库的协议实现接口(接收切片、检查已上传切片、合并切片),官方提供了多种后端语言的示例(Node.js/Java/PHP 等)。
2.webuploader(百度开发,功能全面)
- 特点:百度团队开发的大文件上传组件,支持切片上传、断点续传、图片预览、拖拽上传等,文档丰富,适合国内场景。
- 注意:虽然是较早期的库,但稳定性好,仍被广泛使用。
- 使用示例:
<div id="uploader" class="wu-example">
<div class="btns">
<div id="picker">选择文件</div>
<button id="uploadBtn" class="btn btn-default">开始上传</button>
</div>
</div>
<script src="node_modules/webuploader/dist/webuploader.min.js"></script>
<script>
const uploader = WebUploader.create({
swf: 'node_modules/webuploader/dist/Uploader.swf', // 兼容低版本浏览器的SWF
server: '/api/upload', // 后端接口
pick: '#picker', // 选择文件的按钮
chunked: true, // 开启切片上传
chunkSize: 1*1024*1024, // 1MB 切片
chunkRetry: 3, // 失败重试次数
threads: 3, // 并发数
});
// 绑定上传按钮
document.getElementById('uploadBtn').addEventListener('click', () => {
uploader.upload();
});
// 监控进度
uploader.on('uploadProgress', (file, percentage) => {
console.log('进度:', percentage * 100 + '%');
});
</script>
3.uppy(现代前端上传库,灵活度高)
- 特点:由 Transloadit 开发的现代上传库,支持切片上传、断点续传、拖拽、第三方存储(如 S3)集成,API 设计优雅,可按需扩展。
- 优势:支持 React/Vue 框架,UI 组件可定制,适合现代前端项目。
- 安装:npm install uppy @uppy/core @uppy/xhr-upload
- 使用示例(原生 JS):
import Uppy from '@uppy/core';
import XHRUpload from '@uppy/xhr-upload';
import '@uppy/core/dist/style.min.css';
const uppy = new Uppy()
.use(XHRUpload, {
endpoint: '/api/upload', // 后端接口
chunkSize: 1024 * 1024, // 1MB 切片
parallelUploads: 3, // 并发数
resume: true, // 支持断点续传
retryDelays: [0, 1000, 3000, 5000], // 重试延迟
});
// 监听文件添加
uppy.on('file-added', (file) => {
console.log('添加文件:', file.name);
});
// 监听上传进度
uppy.on('upload-progress', (file, progress) => {
console.log('进度:', progress.bytesUploaded / progress.totalBytes * 100 + '%');
});
// 触发上传
uppy.upload().then((result) => {
console.log('上传完成:', result.successful);
});
- 框架集成:提供 @uppy/react、@uppy/vue 等包,可在框架中组件化使用。
4.tus-js-client(基于 tus 协议,标准化方案)
- 特点:基于 tus 协议(一种标准化的断点续传协议),支持跨语言 / 跨平台,后端若实现 tus 协议,可无缝对接。
- 优势:协议标准化,后端可选择现成的 tus 服务(如 Node.js 的 tus-node-server),无需自己实现合并逻辑。
- 安装:npm install tus-js-client
- 使用示例:
import tus from 'tus-js-client';
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
const upload = new tus.Upload(file, {
endpoint: 'https://your-tus-server.com/files/', // tus 服务端地址
retryDelays: [1000, 3000, 5000],
onProgress: (bytesUploaded, bytesTotal) => {
const percentage = (bytesUploaded / bytesTotal * 100).toFixed(2);
console.log(`进度:${percentage}%`);
},
onSuccess: () => {
console.log('上传完成,文件地址:', upload.url);
},
});
upload.start();
});
如何选择?
- 简单场景 + 兼容性需求:选 resumable.js 或 webuploader,开箱即用,文档成熟。
- 现代前端项目 + 框架集成:选 uppy,API 更现代,支持 React/Vue,可定制性强。
- 追求标准化 + 跨平台:选 tus-js-client,基于通用协议,后端可复用现成服务。