白筱汐

想都是问题,做都是答案

0%

前端分片上传文件

介绍

前端上传文件是项目中经常遇见的一个功能,但是如果文件太大了,我们就需要分片上传了,简单的说就是把文件切割成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') // 项目的根目录需要 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; // 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, 这样就可以避免超过文件的长度。

服务端代码:

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
// 断点续传的后端 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 将文件片段拼接,然后删除片段文件。你可以通过断点调试理解这个步骤。