在日常的网页开发中,文件上传是一项常见操作。通过文件上传技术,用户可以将本地文件方便地传输到Web服务器上。这种功能在许多场景下都是必不可少的,比如上传文件到网盘或上传用户头像等。
然而,当需要上传大型文件时,可能会遇到以下问题:
1. 长时间上传:由于文件大小较大,上传过程可能会耗费较长时间。
2. 上传中断重新上传:如果在上传过程中出现意外情况导致上传中断,用户需要重新开始整个上传过程,这会增加用户的不便。
3. 服务端限制:通常,服务端会对上传的文件大小进行限制,这可能导致无法上传大型文件。
为了解决这些问题,可以采用分片上传的方式:
分片上传即将大文件分割成小块,然后分块上传到服务器。通过分片上传,可以实现以下优势:
快速上传:由于每个小块的大小相对较小,上传时间大大缩短。
断点续传:如果上传过程中出现中断,只需重新上传中断的部分,而不需要重新上传整个文件,提高了用户体验。
避免大小限制:分片上传可以避免由于文件大小限制而无法上传大文件的问题。
通过采用分片上传技术,可以提升用户体验,加快大文件上传速度,并确保上传过程的稳定性和可靠性。
原理:
分片上传的概念类似于将一个大文件分割成多个小块,然后分别上传这些小块到服务器上。
首先,将待上传的大文件划分为固定大小的小块,比如每块大小为1MB。然后逐个上传这些小块到服务器。在上传过程中,可以同时处理多个小块的上传,也可以按顺序逐一上传小块。每个小块上传完成后,服务器会妥善保存这些小块,并记录它们的顺序和位置信息。
当所有小块都上传完成后,服务器会按照预先记录的顺序和位置信息,将这些小块组合成完整的大文件。最终,整个大文件就成功地被分片上传并合并完成了。这种分片上传的方式能够有效地提升大文件上传的效率和稳定性,确保文件上传过程更加可靠和高效。
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
</head>
<body>
<input type="file" />
<script>
const CHUNK_SIZE = 1024 * 1024
let hashName = ''
let fileName = ''
$('input').change(async (e) => {
const file = e.target.files[0]
const chunks = shardingChunks(file) // 分片
fileName = file.name
hashName = await shardingHash(file) // 获取文件hash值
const { data: { existFile, existChunks } } = await axios.post('http://localhost:3000/uploader/verify', { fileHash: hashName, fileName });
if (existFile) return; // 如果该hash值 && file.name 存在说明该文件已经在服务器上了
uploader(chunks, existChunks)
})
// 分片
const shardingChunks = (file) => {
let start = 0
const chunks = []
while (start < file.size) {
chunks.push(file.slice(start, start + CHUNK_SIZE))
start += CHUNK_SIZE
}
return chunks
}
// 获取文件hash值
const shardingHash = (file) => {
return new Promise((resolve) => {
const fileReader = new FileReader()
fileReader.readAsArrayBuffer(file)
fileReader.onload = (e) => {
const spark = new SparkMD5.ArrayBuffer()
spark.append(e.target.result)
resolve(spark.end())
}
})
}
// 分片上传
const uploader = async (chunks, existChunks) => {
const chunksArr = chunks.map((chunk, index) => ({
fileHash: hashName,
chunkHash: hashName + '-' + index,
chunk
}))
const formDatas = chunksArr.map(item => {
const formData = new FormData();
formData.append("fileHash", item.fileHash);
formData.append("chunkHash", item.chunkHash);
formData.append("chunk", item.chunk);
return formData;
})
let flagArr = []
formDatas.forEach(async (item) => {
const res = await axios.post('http://localhost:3000/uploader/upload', item, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
flagArr.push(res.data.success)
if (flagArr.length == formDatas.length && flagArr.every(item => item == true)) {
mergeFile() // 合并文件
flagArr = []
}
})
}
const mergeFile = async () => {
const res = await axios.post('http://localhost:3000/uploader/merge',
{
fileHash: hashName,
fileName: fileName
})
if (res.data.success) return alert('上传成功')
}
</script>
</body>
</html>
后端代码(Node)
const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
const fse = require("fs-extra");
const path = require("path");
const multipart = require("connect-multiparty");
const multipartMiddleware = multipart();
const app = express();
app.use(cors());
app.use(bodyParser.json());
// 所有上传的文件存放在该目录下
const UPLOADS_DIR = path.resolve("uploads");
/**
* 上传
*/
app.post("/upload", multipartMiddleware, (req, res) => {
const { fileHash, chunkHash } = req.body;
// 如果临时文件夹(用于保存分片)不存在,则创建
const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
if (!fse.existsSync(chunkDir)) {
fse.mkdirSync(chunkDir);
}
// 如果临时文件夹里不存在该分片,则将用户上传的分片移到临时文件夹里
const chunkPath = path.resolve(chunkDir, chunkHash);
if (!fse.existsSync(chunkPath)) {
fse.moveSync(req.files.chunk.path, chunkPath);
}
res.send({
success: true,
msg: "上传成功",
});
});
/**
* 合并
*/
app.post("/merge", async (req, res) => {
const { fileHash, fileName } = req.body;
// 最终合并的文件路径
const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
// 临时文件夹路径
const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
// 读取临时文件夹,获取该文件夹下“所有文件(分片)名称”的数组对象
const chunkPaths = fse.readdirSync(chunkDir);
// 读取临时文件夹获得的文件(分片)名称数组可能乱序,需要重新排序
chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
// 遍历文件(分片)数组,将分片追加到文件中
const pool = chunkPaths.map(
(chunkName) =>
new Promise((resolve) => {
const chunkPath = path.resolve(chunkDir, chunkName);
// 将分片追加到文件中
fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
// 删除分片
fse.unlinkSync(chunkPath);
resolve();
})
);
await Promise.all(pool);
// 等待所有分片追加到文件后,删除临时文件夹
fse.removeSync(chunkDir);
res.send({
success: true,
msg: "合并成功",
});
});
/**
* 校验
*/
app.post("/verify", (req, res) => {
const { fileHash, fileName } = req.body;
// 判断服务器上是否存在该hash值的文件
const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
const existFile = fse.existsSync(filePath);
// 获取已经上传到服务器的文件分片
const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
const existChunks = [];
if (fse.existsSync(chunkDir)) {
existChunks.push(...fse.readdirSync(chunkDir));
}
res.send({
success: true,
msg: "校验文件",
data: {
existFile,
existChunks,
},
});
});
const server = app.listen(3000, () => {
console.log(`Example app listening on port ${server.address().port}`);
});