JavaScript实现大文件分片上传:代码与Node.js后端合并示例
在处理视频等大体积文件时,直接上传易遭遇服务端限制、请求超时或断点重传导致前功尽弃。通过 JavaScript 结合 SparkMD5 计算文件唯一标识,将大文件切割为多个切片并发上传,并在前端增加进度条与暂停控制,最后由 Node.js 后端接收并合并文件,可显著提升传输稳定性与用户体验。
实现原理与目标
大文件上传的核心逻辑分为三部分:
- 识别文件:利用
SparkMD5对文件内容+文件名进行加密,生成唯一的hash值,用于区分文件及作为切片存储目录名。 - 切分文件:
File对象继承自Blob,利用其slice方法按指定大小(如 1MB)截取片段。 - 合并还原:所有切片上传至服务器后,调用合并接口,将临时文件按顺序读取并追加写入目标文件。
准备条件
- 前端资源:需引入
axios发送请求,以及spark-md5库处理 MD5 算法。 - 依赖环境:后端使用 Node.js,配合
koa框架、koamulter处理上传、fs-extra操作文件系统。 - 基础设置:确定每个切片的大小
chunkSize,建议范围 1MB-5MB,过大会占用带宽时间过长,过小会增加请求次数。
前端实现步骤
第1步:构建 HTML 页面结构
包含文件选择器、提交/暂停按钮以及自定义样式的进度条输入框。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传</title>
<!-- 引入必要库 -->
<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.js"></script>
</head>
<body>
<h1>大文件分片上传测试</h1>
<input id="file" type="file" name="avatar" />
<button id="submitBtn">提交</button>
<button id="pauseBtn">暂停</button>
<div class="precent">
<input type="range" value="0" />
<span id="precentVal">0%</span>
</div>
</body>
</html>
第2步:计算文件 MD5 Hash
异步读取文件流并计算哈希。为避免内存溢出,采用分段读取方式计算 ArrayBuffer 的 MD5,最后再拼接文件名再次计算以获得文件全局唯一性。
const chunkSize = 1 * 1024 * 1024; // 1MB
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
const hashFile = (file) => {
return new Promise((resolve, reject) => {
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
function loadNext() {
const start = currentChunk * chunkSize;
const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
fileReader.onload = e => {
spark.append(e.target.result);
currentChunk += 1;
if (currentChunk < chunks) {
loadNext();
} else {
// 拼接文件名防止同名不同文件被覆盖
const sparkMd5 = new SparkMD5();
sparkMd5.append(spark.end());
sparkMd5.append(file.name);
resolve(sparkMd5.end());
}
};
fileReader.onerror = () => console.warn('文件读取失败');
loadNext();
});
};
第3步:切片上传、进度控制与暂停
点击提交后,根据总片数循环发起 POST 请求。上传过程中实时计算百分比更新 UI,并支持通过标记变量 pauseStatus 中断后续上传。
submitBtn.on('click', async () => {
var pauseStatus = false;
var nowUploadNums = 0;
const fileDom = $('#file')[0];
const file = fileDom.files[0];
if (!file) { alert('请选文件'); return; }
const hash = await hashFile(file);
const blockCount = Math.ceil(file.size / chunkSize);
const axiosPromiseArray = [];
const uploadFile = (offset = 0) => {
const start = offset * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const form = new FormData();
form.append('file', blobSlice.call(file, start, end));
form.append('index', offset);
form.append('hash', hash);
const onProgress = (e) => {
nowUploadNums++;
const pct = ((nowUploadNums / blockCount) * 100).toFixed(2);
updateProgressBar(pct);
if (nowUploadNums < blockCount) {
uploadFile(nowUploadNums);
} else {
// 全部分片上传完毕,触发合并
axios.all(axiosPromiseArray).then(() => {
mergeChunks(hash, file.name, blockCount);
});
}
};
if (!pauseStatus) {
const req = axios.post('/file/upload', form, { onUploadProgress: onProgress });
axiosPromiseArray.push(req);
}
};
// 暂停逻辑
pauseBtn.on('click', (e) => {
pauseStatus = !pauseStatus;
e.currentTarget.value = pauseStatus ? '继续' : '暂停';
if (!pauseStatus) uploadFile(nowUploadNums);
});
uploadFile();
});
后端实现步骤
第4步:配置 Koa 接收分片
使用 koa-multer 拦截文件上传,并根据 MD5 创建临时文件夹 temp/{hash}/ 保存切片。切片文件名格式为 {hash}-{index}。
const Router = require('koa-router');
const multer = require('koa-multer');
const fs = require('fs-extra');
const path = require('path');
const router = new Router();
const uploadPath = path.join(__dirname, 'upload');
const chunkUploadPath = path.join(uploadPath, 'temp');
const upload = multer({ dest: chunkUploadPath });
router.post('/file/upload', upload.single('file'), async (ctx) => {
const { index, hash } = ctx.req.body;
const dirPath = path.join(chunkUploadPath, hash);
// 确保目录存在
await fs.ensureDir(dirPath);
// 重命名文件到目标路径
const finalName = `${hash}-${index}`;
await fs.move(ctx.req.file.path, path.join(dirPath, finalName));
ctx.status = 200;
ctx.body = 'Success';
});
第5步:合并切片文件
当所有切片上传完毕后调用合并接口。遍历该哈希目录下所有切片,按索引顺序 0 到 N 读取并追加到最终文件中,随后清理临时文件。
router.post('/file/merge_chunks', async (ctx) => {
const { name, total, hash } = ctx.request.body;
const chunkDir = path.join(chunkUploadPath, hash);
const destFile = path.join(uploadPath, name);
// 验证切片数量是否一致
const files = await fs.readdir(chunkDir);
if (files.length !== parseInt(total)) {
ctx.status = 200;
ctx.body = '切片数量不符';
return;
}
const destStream = fs.createWriteStream(destFile);
// 由于需要按顺序,建议使用循环同步或有序队列处理
for (let i = 0; i < total; i++) {
const chunkPath = path.join(chunkDir, `${hash}-${i}`);
const data = await fs.readFile(chunkPath);
destStream.write(data);
// 可选:边写边删释放空间
await fs.unlink(chunkPath);
}
destStream.end();
// 清理空目录
await fs.rmdir(chunkDir);
ctx.body = 'Success';
});
注意事项与排错
在实施过程中可能遇到以下情况:
- 内存溢出:若文件极大且浏览器环境较差,建议增大
chunkSize或在计算 MD5 时严格控制 buffer 大小,上述示例已做分段处理。 - 并发风暴:若同时上传几十个并行请求,可能会占满客户端出口带宽。建议在 `uploadFile` 中使用信号量或队列(Queue)限制同时运行的
axios数量。 - 切片丢失:如果合并时发现
total与实际切片文件数量不匹配,应检查网络状态日志,允许用户针对特定index发起断点续传而非重新上传所有数据。
总结
通过 JavaScript 分片技术与后端简单的文件流操作,可有效解决大文件上传的不稳定性问题。核心在于通过 MD5 确保一致性,利用 Blob 切片保证低内存消耗,并通过异步回调机制优化用户体验。以上就是 JavaScript 实现大文件分片上传处理的详细内容,更多关于前端大文件处理与 Node.js 后端交互的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













