准备工作
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
input {
display: block;
margin: 10px 0;
}
</style>
</head>
<body>
<input type="file" id="file" />
<input type="button" id="upload" value="上传" />
<input type="button" id="continue" value="继续上传" />
<!-- 文件名hash用 -->
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
<script src="./upload.js"></script>
</body>
</html>
upload文件
const fileEle = document.querySelector("#file");
const uploadButton = document.querySelector("#upload");
uploadButton.addEventListener("click", async () => {
let file = fileEle.files[0];
console.log("🚀 ~ file:", file);
uploadFile(file);
});
const uploadFile = (file) => {
let fd = new FormData();
fd.append("file", file);
fetch("http://localhost:3000/upload", {
method: "POST",
body: fd,
});
};
后端代码
npm i express cors multer
const express = require("express");
const cors = require("cors");
const multer = require("multer");
const app = express();
const upload = multer({ dest: "uploads/" });
// 使用 cors 中间件
app.use(cors());
app.use(express.static("static"));
// 导入中间件
const bodyParser = require("body-parser");
// 使用中间件
app.use(bodyParser.urlencoded({ extended: false }));
// 处理JSON格式的数据
app.use(bodyParser.json());
const path = require("path");
const fse = require("fs-extra");
app.post("/upload", upload.single("file"), (req, res) => {
console.log(req.file);
res.send({
msg: "上传成功",
success: true,
});
});
app.listen(3000, () => {
console.log("服务已运行:http://localhost:3000");
});
1、 单文件上传的逻辑
选择完文件点上传后 前后端内容
前端
后端
此时我们后端文件所在的同级目录下已经有这个文件了,但是没有给相对应的后缀名,如果需要给文件加上后缀可参考下面内容
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/"); // 保存到 uploads 目录
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname); // 获取文件后缀
const fileName = file.fieldname + "-" + Date.now() + ext;
cb(null, fileName);
},
});
const upload = multer({ storage: storage });
至此 我们的单文件上传已经完成了
2、分片上传
在单文件上传的背景下,当一个文件足够大的时候,直接进行单文件上传可能会对服务器造成很大的压力,这个时候,我们通常需要分片上传了
步骤
- 1、前端根据文件大小对文件进行分片,同时记录文件名的hash值
- 2、在每一个切片下保存文件内容和文件名确保后端可以进行合并
- 3、因为浏览器最多支持六个请求,在分片的同时做一个请求池
- 4、所有切片上传结束,告诉后端该把文件合并了
1、对文件进行分片同时记录文件名的hash值到每一个切片
//获取文件的hash值
const getHash = (file) => {
return new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = function (e) {
let fileMd5 = SparkMD5.ArrayBuffer.hash(e.target.result);
resolve(fileMd5);
};
});
};
//分片
const createChunks = (file, fileHash) => {
// 使用单独常量保存预设切片大小 1MB
const chunkSize = 1024 * 1024 * 1;
const chunks = [];
let start = 0;
let index = 0;
while (start < file.size) {
let curChunk = file.slice(start, start + chunkSize);
chunks.push({
file: curChunk,
uploaded: false,
chunkIndex: index,
fileHash: fileHash,
});
index++;
start += chunkSize;
}
return chunks;
};
//修改 click 事件内容
uploadButton.addEventListener("click", async () => {
let file = fileEle.files[0];
const fileHash = await getHash(file);
const chunks = createChunks(file, fileHash);
console.log("🚀 ~ chunks:", chunks);
// uploadFile(file);
});
此时前端上传后
2、对得到的chunks进行上传
//首先对chunk进行一个处理
const uploadHandler = (chunk) => {
return new Promise(async (resolve, reject) => {
try {
let fd = new FormData();
fd.append("file", chunk.file);
fd.append("fileHash", chunk.fileHash);
fd.append("chunkIndex", chunk.chunkIndex);
console.log("🚀 ~ fd:", fd);
let result = await fetch("http://localhost:3000/upload", {
method: "POST",
body: fd,
}).then((res) => res.json());
chunk.uploaded = true;
resolve(result);
} catch (err) {
reject(err);
}
});
};
const uploadChunks = (chunks, maxRequest = 6) => {
return new Promise((resolve, reject) => {
if (chunks.length == 0) {
resolve([]);
}
let requestPoor = []; //请求池
let start = 0;
let index = 0;
let requestReaults = [];
while (start < chunks.length) {
//每次添加maxRequest
requestPoor.push(chunks.slice(start, start + maxRequest));
start += maxRequest;
}
//定义请求函数
async function request() {
//当前请求超过requestPoor长度,则表示结束
if (index > requestPoor.length - 1) {
resolve(requestReaults);
return;
}
let currentChunks = requestPoor[index];
const result = [];
for (const chunk of currentChunks) {
result.push(uploadHandler(chunk));
}
const currentResult = await Promise.all(result);
console.log("🚀 ~ currentResult:", currentResult);
index++;
request();
//有需要这里做错误处理
}
request();
});
};
修改我们的后端代码
app.post("/upload", upload.single("file"), (req, res) => {
const { fileHash, chunkIndex } = req.body;
console.log(fileHash, chunkIndex);
// 切片上传的临时目录文件夹
let tempFileDir = path.resolve("uploads", fileHash);
// 如果当前文件的临时文件夹不存在,则创建该文件夹
if (!fse.pathExistsSync(tempFileDir)) {
fse.mkdirSync(tempFileDir);
}
// 目标切片位置
const tempChunkPath = path.resolve(tempFileDir, chunkIndex);
// 当前切片位置(multer默认保存的位置)
let currentChunkPath = path.resolve(req.file.path);
if (!fse.existsSync(tempChunkPath)) {
fse.moveSync(currentChunkPath, tempChunkPath);
} else {
fse.removeSync(currentChunkPath);
}
res.send({
msg: "上传成功",
success: true,
});
});
此时点击上传
同时我们的后端地址会出现这个
3、合并切片
前端在结束的时候告诉后端合并
// 合并分片请求
const mergeRequest = (fileHash, fileName) => {
return fetch(
`http://localhost:3000/merge?fileHash=${fileHash}&fileName=${fileName}`,
{
method: "GET",
}
).then((res) => res.json());
};
uploadButton.addEventListener("click", async () => {
let file = fileEle.files[0];
const fileHash = await getHash(file);
const chunks = createChunks(file, fileHash);
console.log("🚀 ~ chunks:", chunks);
await uploadChunks(chunks);
await mergeRequest(fileHash, file.name);
});
后端代码
app.get("/merge", async (req, res) => {
const { fileHash, fileName } = req.query;
// 最终合并的文件路径
const filePath = path.resolve(
"uploads",
fileHash + path.extname(fileName)
);
// 临时文件夹路径
let tempFileDir = path.resolve("uploads", fileHash);
// 读取临时文件夹,获取所有切片
const chunkPaths = fse.readdirSync(tempFileDir);
// 将切片追加到文件中
let mergeTasks = [];
for (let index = 0; index < chunkPaths.length; index++) {
mergeTasks.push(
new Promise((resolve) => {
// 当前遍历的切片路径
const chunkPath = path.resolve(tempFileDir, index + "");
// 将当前遍历的切片切片追加到文件中
fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
// 删除当前遍历的切片
fse.unlinkSync(chunkPath);
resolve();
})
);
}
await Promise.all(mergeTasks);
// 等待所有切片追加到文件后,删除临时文件夹
fse.removeSync(tempFileDir);
res.send({
msg: "合并成功",
success: true,
});
});