Source code:https://github.com/GSemir0418/file-slice-upload
针对大文件上传的业务场景,前后端采用切片上传的方案,即前端将大文件分割为固定大小的 chunk,并循环请求给后端;后端每获取一部分,就写入到服务器指定文件中,最终实现大文件上传。
1 客户端
1.1 初始化
npm init -y
yarn add vite -D
1.2 项目结构
.
├── index.html 项目首页
├── node_modules
├── package.json
├── src
│ ├── app.js 项目入口文件,主要方法都写到这里
│ └── config.js 字段映射等配置
└── yarn.lock
1.3 思路
- 获取上传的文件数据
// 在 oUploader 中获取到 files 数组,并取出第一项,命名为 file
const { files: [file] } = oUploader
// 结构取出上传文件的信息
const { name, size, type } = file
- 校验(size,type)
- 记录当前上传大小
uploadedSize
,用于控制切片及计算进度 - 在
while
循环中,使用slice
对 file 数据进行切片
while (uploadedSize < size) {
const fileChunk = file.slice(uploadedSize, uploadedSize + CHUNK_SIZE);
}
- 构造 formData
function createFormData({ name, fileName, type, size, uploadedSize, chunk }) {
const postData = new FormData();
postData.append("name", name);
postData.append("fileName", fileName);
postData.append("type", type);
postData.append("size", size);
postData.append("uploadedSize", uploadedSize);
postData.append("chunk", chunk);
return postData;
}
const formData = createFormData({
name,
fileName,
type,
size,
uploadedSize,
chunk: fileChunk,
});
- axios 请求
// axios发送请求
try {
response = await axios.post(API, formData);
} catch (error) {
oInfo.innerHTML = INFO["UPLOAD_FAILED"] + error.message;
return;
}
- 每份 chunk 上传结束后,更新
uploadedSize
,并同步进度条
1.4 优化思路
- 如果某一片上传失败了,怎么处理?
- 重试机制:实现自动重试机制,在文件块上传失败时(catch error),可以自动尝试重新上传
- 断点续传:记录已成功上传文件块的信息,如果上传失败,可以从中断的地方重新开始上传。也可以让用户自己决定是否重传失败的文件块。
- 后台验证完整性:在所有文件块上传完成后,后台进行校验,确保所有文件块均正确无误地上传。如果校验失败,可以提示前端做相应处理。
- 取消上传任务:提供一个取消机制,当用户决定取消或者在上传过程中发现某个文件块连续重试失败达到限制次数时,可以触发取消操作。取消时应清理已上传的文件块并释放资源。
- 如果网络波动,如何保证上传顺序?
- 使用文件块元信息:在上传每个文件块时,除了文件块的数据外,还应该发送一个包含文件块序号的元信息,这样即使文件块的上传请求不是按序到达服务器,服务器也能根据元信息中的序号将文件块放置到正确的位置。
- 服务器端排序:在所有文件块上传完毕后,服务器可以根据每个文件块的序号进行排序,以确保文件块在最终组装时的顺序正确。
- 并发请求控制,详见《并发请求》一文
2 Express 服务端
2.1 初始化
npm init
yarn add express express-fileupload
yarn global add nodemon
2.2 项目结构
.
├── app.js
├── node_modules
├── package.json
├── upload_tem
│ └── 1656466982424_1.mp4.mp4
└── yarn.lock
2.3 思路
- 在请求体解构取出 chunk 及其他数据
const { name, fileName, uploadedSize } = req.body;
const { chunk } = req.files;
- 处理文件名、后缀及保存路径
- 根据 uploadedSize 判断新建或追加数据文件
if (uploadedSize !== "0") {
// 注意是字符串0
if (!existsSync(filePath)) {
res.send({
code: 1002,
msg: "No file exists",
});
return;
}
// append数据到文件,结束本次上传
appendFileSync(filePath, chunk.data);
res.send({
code: 200,
msg: "chunk appended",
// 将服务器静态数据文件路径发送给前端
video_url: "http://localhost:8000/" + filename,
});
return;
}
- 如果 uploadedSize 为 0,表示没有正在上传的数据,此时创建并写入这个文件
writeFileSync(filePath, chunk.data);
res.send({ code: 200, msg: "file created" });
附:使用到的中间件等方法
// 请求体数据处理中间件
const bodyParser = require("body-parser");
const uploader = require("express-fileupload");
// extname是获取文件后缀名的
const { extname, resolve } = require("path");
// existsSync检查文件是否存在; appendFileSync同步添加数据
const { existsSync, appendFileSync, writeFileSync } = require("fs");
// 解析并返回的请求体对象配置为任意类型
app.use(bodyParser.urlencoded({ extended: true }));
// 解析json格式的请求体
app.use(bodyParser.json());
// 请求体中上传数据的处理,返回的数据在req.files中
app.use(uploader());
// 指定静态文件url
app.use("/", express.static("upload_tem"));
// 跨域处理
app.all("*", (_, res, next) => {
res.header("Access-Control-Allow-origin", "*");
res.header("Access-Control-Allow-methods", "POST,GET");
next();
});