介绍
前端上传文件是项目中经常遇见的一个功能,但是如果文件太大了,我们就需要分片上传了,简单的说就是把文件切割成n个小片段,依次上传到服务器,最后再把这些片段拼接起来,组成完整的文件。
这里我不过多介绍服务端的逻辑,因为大部分上传文件的场景中后端是 oss 处理的,不需要我们写后端代码,大家大概知道个流程就好了。你需要掌握的基础知识包括, File 对象,Blob 对象, node express, stream 。
实现一个简单的上传文件的功能
首先,我们使用 node 写一个简单的后端服务,实现一个简单的文件上传功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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') }, 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 这个库的问题,具体不多说明了,按照备注的代码操作就可以了。
前端代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <!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 属性则可以实现切割计算总的片段数。
前端代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| <!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; 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, 这样就可以避免超过文件的长度。
服务端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| 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) => { const uploadDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } cb(null, uploadDir); } });
const upload = multer({ storage });
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}`); 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); writeStream.write(data); fs.unlinkSync(chunkPath); } writeStream.end(); }
res.send('Chunk uploaded successfully'); });
const PORT = 9000 app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); });
|
服务端主要是 createWriteStream 将文件片段拼接,然后删除片段文件。你可以通过断点调试理解这个步骤。