介绍
前端上传文件是项目中经常遇见的一个功能,但是如果文件太大了,我们就需要分片上传了,简单的说就是把文件切割成n个小片段,依次上传到服务器,最后再把这些片段拼接起来,组成完整的文件。
这里我不过多介绍服务端的逻辑,因为大部分上传文件的场景中后端是 oss 处理的,不需要我们写后端代码,大家大概知道个流程就好了。你需要掌握的基础知识包括, File 对象,Blob 对象, node express, stream 。
实现一个简单的上传文件的功能
首先,我们使用 node 写一个简单的后端服务,实现一个简单的文件上传功能。
js
// 普通的单文件上传
const express = require('express')
const multer = require('multer')
const path = require('path')
const fs = require('fs')
const cors = require('cors')
const app = express()
const PORT = 3000
app.use(cors())
app.use(express.static('public'))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, './uploads') // 项目的根目录需要 uploads 文件夹,不然会报错
},
filename: function (req, file, cb) {
// 解决中文乱码问题
file.originalname = Buffer.from(file.originalname, "latin1").toString(
"utf8"
);
cb(null, file.originalname)
}
})
const upload = multer({ storage: storage })
app.post('/upload', upload.single('file'), (req, res) => {
res.send('File uploaded successfully')
})
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
})
这里使用 express 框架构架了一个简单的服务,需要注意的是代码中注释的内容,因为在我写测试代码的时候发现上传文件名称包含中文的文件时,到服务器端保存后文件名称会出现乱码。
谷歌一下之后发现是 Multer 这个库的问题,具体不多说明了,按照备注的代码操作就可以了。
前端代码实现:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload</title>
</head>
<body>
<p>upload single file</p>
<input type="file" id="single-file">
<script>
// 单文件上传
document.getElementById('single-file').addEventListener('change', function (event) {
const file = event.target.files[0];
console.log(file);
const formData = new FormData()
formData.append('file', file)
fetch('http://localhost:3000/upload', {
method: 'POST',
body: formData
}).then(res => {
console.log(res);
})
})
</script>
</body>
</html>
前后端的代码实现都非常的简单,接下来我们来实现分片上传。
分片上传文件
前端需要把文件切割成多个小片段,当前片段数小于总片段数就递归上传。input 标签选中的 File 文件实现了 Blob 的接口,也就是可以使用 slice 方法切割二进制数据。File 对象的 size 属性则可以实现切割计算总的片段数。
前端代码如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload</title>
</head>
<body>
<p>upload file chunks</p>
<input type="file" id="fileInput">
<script>
// 分片上传
document.getElementById('fileInput').addEventListener('change', function (event) {
const file = event.target.files[0];
if (file) {
uploadFile(file);
}
});
function uploadFile(file) {
const chunkSize = 1 * 1024; // 1KB
const totalChunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
let progress = 0;
function uploadChunk(start) {
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('filename', file.name);
formData.append('chunkNumber', currentChunk);
formData.append('totalChunks', totalChunks);
fetch('http://localhost:9000/upload', {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
currentChunk++;
progress = ((currentChunk / totalChunks) * 100).toFixed(2) + '%';
console.log('progress:',progress);
if (currentChunk < totalChunks) {
uploadChunk(currentChunk * chunkSize);
} else {
console.log('Upload complete');
}
} else {
console.error('Upload failed');
}
}).catch(error => {
console.error('Upload error', error);
});
}
uploadChunk(0);
}
</script>
</body>
</html>
这里需要注意就是 totalChunks 的计算需要使用 Math.ceil 向上舍入, end 的取值使用 Math.min, 这样就可以避免超过文件的长度。
服务端代码:
js
// 断点续传的后端 node 实现
const express = require('express');
const multer = require('multer');
const cors = require('cors')
const fs = require('fs');
const path = require('path');
const app = express();
app.use(cors()) // 允许跨域
// 配置存储选项
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// 如果没有 uplaods 文件夹,则创建文件夹
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
}
});
const upload = multer({ storage });
// 解析 JSON 和 URL 编码数据
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 处理文件上传
app.post('/upload', upload.single('file'), (req, res) => {
const { filename, chunkNumber, totalChunks } = req.body;
if (!filename || chunkNumber === undefined || totalChunks === undefined) {
return res.status(400).send('Filename, chunkNumber or totalChunks is missing');
}
const tempFilePath = path.join(__dirname, 'uploads', `${filename}.part${chunkNumber}`);
// 在将磁盘中上传的 原生 chunk 名称更改为 filename.part0
// xxxx 变更为 xxx.part0
fs.renameSync(req.file.path, tempFilePath);
// 合并文件逻辑
if (Number(chunkNumber) + 1 === Number(totalChunks)) {
const finalFilePath = path.join(__dirname, 'uploads', filename);
const writeStream = fs.createWriteStream(finalFilePath);
for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(__dirname, 'uploads', `${filename}.part${i}`);
const data = fs.readFileSync(chunkPath); // 读取 chunk 文件
writeStream.write(data);
fs.unlinkSync(chunkPath); // 逐一删除 chunk 文件
}
writeStream.end();
}
res.send('Chunk uploaded successfully');
});
const PORT = 9000
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
服务端主要是 createWriteStream 将文件片段拼接,然后删除片段文件。你可以通过断点调试理解这个步骤。