作为刚接触文件上传的JS学习者,一定会遇到这样的困境:当用户尝试上传一个大文件时,占满内存导致页面卡顿、网络波动时整个文件重传、触发服务器请求超时......这些问题就像试图用一根吸管喝完一整桶水------方法不对,体验必然糟糕。今天,就用最基础的切片上传方案解决这个问题。
为什么切片是大文件上传的"安全绳"
当用户点击文件选择框并确认后,浏览器会通过input元素的change事件捕获文件对象。这段简单的交互代码是整个上传流程的起点:
js
const input = document.getElementById('input');
const upload = document.getElementById('upload');
let fileObj = null;
input.addEventListener('change', (e) => {
const [file] = e.target.files;
fileObj = file; // 保存文件引用供后续切片使用
});
upload.addEventListener('click', () => {
if (!fileObj) return alert('请选择文件');
const chunkList = createChunk(fileObj); // 触发切片逻辑
uploadChunks(chunkList.map(...)); // 启动上传流程
});
接下来,我们通过 createChunk 函数来实现安全的"拆解"工作:
js
function createChunk(file, size = 5 * 1024 * 1024) {
const chunkList = [];
let cur = 0;
while (cur < file.size) {
chunkList.push({ file: file.slice(cur, cur + size) });
cur += size; // 指针向前推进 5MB
}
return chunkList;
}
file.slice()是浏览器的"切割机",它不会复制原始文件,而是创建指向原文件片段的引用(类似书签标记段落)。- 每次切5MB(
5 * 1024 * 1024字节),既避免单次请求过大,又防止切片过多增加管理成本。 cur指针像裁纸刀一样匀速推进,确保无重叠、无遗漏地覆盖整个文件。
前端:如何安全打包并发送切片
切片完成后,每个碎片需要贴上"快递单"(元数据)才能被正确识别。
给切片贴上身份标签
js
const chunks = chunkList.map(({file}, index) => ({
file,
size: file.size,
chunkName: `${fileObj.name}-${index}`, // 如 "video.mp4-0"
fileName: fileObj.name,
index
}));
chunkName是切片的唯一身份证(文件名+序号);index确保服务端能按顺序重组文件;- 这像给每块雕塑碎片编号:"左臂-1"、"左臂-2"......
下面可以建一个uploadChunks函数,负责做到封装切片,以及并发上传&合并触发
js
function uploadChunks(chunks) {
//用FormData封装切片
const formChunks = chunks.map(({file, fileName, chunkName, size, index}) => {
const formData = new FormData()
formData.append('file', file) // 二进制切片
formData.append('fileName', fileName) // 原始文件名
formData.append('chunkName', chunkName) // 切片身份证
return {formData, index}
})
const requestList = formChunks.map(({formData, index}) => {
return axios.post('http://localhost:3000/upload', formData)
})
//并发上传与合并触发
Promise.all(requestList).then(res => {
axios.post('http://localhost:3000/merge', {
fileName: fileObj.name,
size: 5 * 1024 * 1024
}).then(res => {
console.log(res.data);
})
})
}
FormData它像定制的快递盒,把二进制切片和文字标签(元数据)安全打包。普通JSON无法传输二进制数据,而FormData能自动处理MIME类型编码。
Promise.all同时发起所有切片请求;- 仅当所有切片确认送达后,才通知服务端合并;
- 合并请求携带了原始文件名和切片大小,这是重组的关键参数。
重要提醒 :此处Promise.all在切片数量极大时可能引发问题(如1000个切片同时请求),但作为基础实现完全合理。进阶方案会控制并发数,但代码目标明确------先跑通核心逻辑。
后端:如何接收并精准拼合切片
前端发得再规范,后端接不住也是徒劳。Node.js服务端代码精准实现了两个关键动作。
动作1:安全接收切片
js
if (req.url === '/upload') {
const form = new multiparty.Form();
form.parse(req, (err, fields, files) => {
const [file] = files.file;
const [fileName] = fields.fileName;
const [chunkName] = fields.chunkName;
// 创建专属切片目录:qiepian/原文件名-chunks
const chunkDir = path.resolve(__dirname, 'qiepian', `${fileName}-chunks`);
if (!fs.existsSync(chunkDir)) fs.mkdirsSync(chunkDir);
// 保存切片:移动临时文件到目标路径
fs.moveSync(file.path, path.resolve(chunkDir, chunkName));
});
}
- 目录隔离 :每个文件的切片存入独立文件夹(如
video.mp4-chunks),避免不同文件切片混淆。 - 无损移动 :
fs.moveSync直接转移系统临时文件,比复制更高效安全。 - 关键细节 :
chunkName保留了序号(如video.mp4-0),这是后续排序的依据。
动作2:按序拼接成完整文件
js
if (req.url === '/merge') {
const { fileName, size } = await resolvePost(req);
const filePath = path.resolve(__dirname, 'qiepian', `${fileName}-chunks`);
await mergeChunks(filePath, fileName, size);
}
// 核心合并函数
const mergeChunks = async (filePath, fileName, size) => {
let chunksPath = fs.readdirSync(filePath);
chunksPath.sort((a, b) => a.split('-')[1] - b.split('-')[1]); // 按序号排序
const arr = chunksPath.map((chunkPath, index) =>
pipeStream(
path.resolve(filePath, chunkPath),
fs.createWriteStream(path.resolve(filePath, '..', fileName), {
start: index * size,
end: (index + 1) * size
})
)
);
await Promise.all(arr);
};
// 流式传输关键实现(原被省略的代码)
function pipeStream(readPath, writeStream) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(readPath);
// 核心流管道操作:这里正是readStream.pipe(writeStream)的实际应用
readStream
.pipe(writeStream)
.on('finish', resolve)
.on('error', reject);
});
}
拼接的精妙之处:
-
严格排序 :
split('-')[1]提取序号0,1,2...确保切片按原始顺序写入。 -
精准定位 :
fs.createWriteStream的start/end参数像手术刀,将第n个切片精确插入目标文件的[n*size, (n+1)*size]区间。 -
流式处理 :
pipeStream函数内部通过readStream.pipe(writeStream)建立管道传输:fs.createReadStream创建文件读取流(从切片文件读取数据)fs.createWriteStream创建带偏移量的写入流(定位到目标文件指定位置)- 管道操作符
pipe直接将读取流数据导向写入流,全程不经过内存缓冲,避免大文件处理时的内存溢出风险
结语
代码完美实现了切片上传的最小可行方案:
- 前端切片→打包元数据→并发上传→服务端归类→按序合并。
- 没有多余的装饰,只有清晰的因果链条。
所有复杂的上传系统,都始于这5MB的切片。当你能彻底搞明白这段代码,理解更高级方案也不在话下了。