1. 文件分块
将大文件切分成较小的片段(通常称为分片或块),然后逐个上传这些分片。这种方法可以提高上传的稳定性,因为如果某个分片上传失败,只需要重新上传该分片而不需要重新上传整个文件。同时,分片上传还可以利用多个网络连接并行上传多个分片,提高上传速度。
2. 断点续传
在上传过程中,如果网络中断或上传被中止,断点续传技术可以记录已成功上传的分片信息,以便在恢复上传时继续上传未完成的部分,而不需要重新上传整个文件。这种技术可以大大减少上传失败的影响,并节省时间和带宽。
3. node项目目录初始化
-
安装依赖
- express 敏捷启动服务
- multer 读取文件,存储
- cors 解决跨域
-
目录结构
- src
- TED.mp4 (长视频,10分钟,可以下载这个
https://mirror.aarnet.edu.au/pub/TED-talks/911Mothers_2010W-480p.mp4
)
- TED.mp4 (长视频,10分钟,可以下载这个
- uploads 存放切片
- video 存放将切片拼接后的视频
- index.html 前端页面
- src
代码附上
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input id="file" type="file" />
<script>
// functions
// 实现切片的方法
const makeChunk = (file, size = 1024 * 1024 * 4) => {
const chunks = [];
for (let i = 0; i < file.size; i += size) {
const chunk = file.slice(i, i + size);
chunks.push(chunk);
}
return chunks;
};
// 上传分片后的文件方法
const uploadChunks = (chunks) => {
// 1. 使用Promise.all保证所有上传方法执行成功
// 2. 必须要给每个分片文件加标识,才可以让node端进行按序拼接
const list = [];
for (let i = 0; i < chunks.length; i++) {
const formData = new FormData();
formData.append("filename", "ted");
formData.append("index", i);
formData.append("chunk", chunks[i]); // 千万注意,切片文件要最后append否则会出现意外的bug
list.push(
fetch("http://localhost:3000/upload", {
method: "POST",
body: formData,
})
);
}
Promise.all(list)
.then((res) => {
console.log("上传成功", res);
fetch("http://localhost:3000/merge", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
fileName: "TED演讲10分钟长视频",
}),
});
})
.catch((err) => {
console.error("上传失败", err);
});
};
// logic
const file = document.querySelector("#file");
file.addEventListener("change", (e) => {
let file = e.target.files[0]; // 我们只处理单个文件因此取第一个元素即可
// file是一个对象,底层继承于Blob,借助于Blob身上的slice方法,可以实现对大文件的分片操作
console.log(file);
const chunks = makeChunk(file);
console.log("chunks:", chunks);
uploadChunks(chunks);
});
</script>
</body>
</html>
index.js
import fs from "node:fs";
import path from "node:path";
import express from "express";
import multer from "multer";
import cors from "cors";
console.log("cors:", cors);
// 1. 初始化multer
const storage = multer.diskStorage({
// 指定切片存放目录
destination: function (req, file, cb) {
cb(null, "../uploads/");
},
filename: function (req, file, cb) {
console.log("req.body.index:", req.body.index);
console.log("file", file);
cb(null, `${req.body.filename}-${req.body.index}`);
},
});
const upload = multer({ storage });
const app = express();
app.use(cors());
app.use(express.json());
// upload.single("chunk") 其中chunk对应前端上传的文件切片名字
app.post("/upload", upload.single("chunk"), (req, res) => {
console.log("req.body:", req.body);
res.header("Content-Type", "application/json;charset=utf-8");
res.send("ok");
});
// 拼接切片
app.post("/merge", (req, res) => {
// 获取uploadDir的目录
const uploadDir = path.join(process.cwd(), "../uploads");
const dirs = fs.readdirSync(uploadDir); // 发现是乱序的
// 对dirs数组进行排序
dirs.sort((a, b) => {
return a.split("-")[1] - b.split("-")[1];
});
const videoDir = path.join(
process.cwd(),
"../video",
`${req.body.fileName}.mp4`
);
dirs.forEach((item) => {
// 合并成一个完整的文件至video目录
fs.appendFileSync(videoDir, fs.readFileSync(path.join(uploadDir, item)));
// 删除已合并的切片文件
fs.unlinkSync(path.join(uploadDir, item));
});
console.log(dirs);
res.send("okk");
});
app.listen(3000, () => {
console.log("server is running at port 3000");
});
最终效果