背景
在上传大文件时,分片上传是一种常见且有效的策略。由于大文件在上传过程中可能会遇到内存溢出、网络不稳定等问题,分片上传可以显著提高上传的可靠性和效率。通过将大文件分割成多个小分片,不仅可以减少单次上传的数据量,降低内存消耗,还能在遇到网络中断时仅需重传失败的分片,从而提高整体上传的成功率和用户体验。
步骤
安装 Axios
如果你还没有安装 Axios,可以通过 npm 或 yarn 来安装:
bash
npm install axios
# 或者
yarn add axios
获取文件
点击按钮选择文件上传,通过 event
事件对象拿到文件。
javascript
<template>
<div>
<input type="file" @change="uploadFile"></input>
</div>
</template>
<script>
import axios from "axios";
export default {
methods: {
uploadFile(event) {
const files = event.target.files || event.dataTransfer.files;
const file = files[0];
console.log('file::: ', file);
this.uploadChunks(file, file.name, progress => {
console.log(`Upload progress: ${progress * 100}%`);
});
},
},
}
</script>
文件切片并使用 Axios 上传切片:
1. 文件切片:
- 定义
chunkSize
每片大小为 1MB,计算文件需要分割成的总分块数totalChunks
。
2. 循环分块上传:
-
遍历每个分块,计算每个分块的起始位置
start
和结束位置end
。 -
使用
file.slice
方法创建blob
对象表示当前分块。 -
创建
FormData
对象,并添加分块数据及其他元数据(文件名、分块索引、总分块数)。
3. 循环分块上传:
-
使用
axios.post
发送 POST 请求到/upload
接口,携带分块数据。 -
设置请求头
Content-Type
为multipart/form-data
。
4. 循环分块上传:
-
成功上传分块后,记录已上传的分块数量,并调用上传进度的回调函数
onProgress
。 -
设如果上传失败,捕获并记录错误信息。
javascript
async uploadChunks(file, fileName, onProgress) {
const chunkSize = 1 * 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
let uploadedChunks = 0;
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const blob = file.slice(start, end);
const formData = new FormData();
formData.append('file', blob, `${fileName}_${i}`);
formData.append('filename', fileName);
formData.append('chunkIndex', i.toString());
formData.append('totalChunks', totalChunks.toString());
try {
const response = await axios.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
console.log(`Chunk ${i} uploaded successfully.`);
uploadedChunks++;
if (onProgress) {
onProgress(uploadedChunks / totalChunks);
}
} catch (error) {
console.error(`Failed to upload chunk ${i}:`, error);
}
}
}
完整代码
javascript
<template>
<div>
<input type="file" @change="uploadFile"></input>
</div>
</template>
<script>
import axios from "axios";
export default {
data() {},
methods: {
uploadFile(event) {
console.log('event::: ', event);
// 获取文件对象
const files = event.target.files || event.dataTransfer.files;
console.log('files::: ', files);
const file = files[0];
this.uploadChunks(file, file.name, progress => {
console.log(`Upload progress: ${progress * 100}%`);
});
},
async uploadChunks(file, fileName, onProgress) {
// 定义每个分片的大小为 1MB
const chunkSize = 1 * 1024 * 1024; // 1MB
// 计算总分片数
const totalChunks = Math.ceil(file.size / chunkSize);
let uploadedChunks = 0;
// 遍历所有分片
for (let i = 0; i < totalChunks; i++) {
// 计算当前分片的起始位置
const start = i * chunkSize;
// 计算当前分片的结束位置
const end = Math.min(start + chunkSize, file.size);
// 创建当前分片的 Blob 对象
const blob = file.slice(start, end);
// 创建表单数据对象
const formData = new FormData();
// 添加当前分片的文件
formData.append('file', blob, `${fileName}_${i}`);
// 添加文件名
formData.append('filename', fileName);
// 添加分片索引
formData.append('chunkIndex', i.toString());
// 添加总分片数
formData.append('totalChunks', totalChunks.toString());
try {
// 上传分片
const response = await axios.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
console.log(`Chunk ${i} uploaded successfully.`);
uploadedChunks++;
// 上传进度
if (onProgress) {
onProgress(uploadedChunks / totalChunks);
}
} catch (error) {
console.error(`Failed to upload chunk ${i}:`, error);
}
}
}
}
}
</script>
<style lang="scss" scoped></style>
注意:
- 使用
FormData
上传文件切片,确保文件部分是以二进制格式上传的。- 设置
Content-Type
为multipart/form-data
。
服务端合并切片
实现原理
1. 搭建服务
-
服务搭建:引入
express
模块,创建了一个express
应用实例app
。 -
设置端口号
PORT
并使用app.listen()
启动express
应用,使其监听指定的端口。
2. 接受并存储切片
-
接收切片:服务端定义了一个
/upload
路由,使用multer
中间件处理上传的文件切片。multer
会将上传的文件暂存到指定的目录(例如uploads/
)。 -
保存切片:服务端根据
filename
和chunkIndex
创建一个临时目录,并将上传的切片移动到该目录中。例如,切片路径可能为uploads/filename/chunkIndex
。 -
创建目录:如果临时目录不存在,服务端会使用 mkdir 方法递归创建目录。
3. 切片合并
-
检测最后一个切片:当接收到的切片索引等于
totalChunks - 1
时,说明这是最后一个切片,触发切片合并操作。 -
读取所有切片:在
mergeChunks
函数中,服务端遍历所有已上传的切片,按顺序读取每个切片的内容。 -
合并切片:将所有切片的内容按顺序拼接成一个完整的文件。这里使用
Buffer.concat
方法将多个Buffer
对象合并成一个。 -
写入合并后的文件:将合并后的文件内容写入到目标目录(例如
merged/
)。 -
删除临时文件:合并完成后,删除所有临时切片文件,释放存储空间。
使用 node
示例
javascript
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const util = require('util');
const app = express();
const upload = multer({ dest: 'uploads/' });
// 设置静态文件夹
app.use(express.static('uploads'));
// 将 fs 方法转换为 Promise 版本
const mkdir = util.promisify(fs.mkdir);
const rename = util.promisify(fs.rename);
const unlink = util.promisify(fs.unlink);
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
// 文件合并函数
async function mergeChunks(filename, totalChunks) {
// 定义存储切片临时文件夹路径
const tempDir = `uploads/${filename}/`;
// 定义最终合并文件的路径
const outputFilePath = `merged/${filename}`;
// 创建输出目录
await mkdir(path.dirname(outputFilePath), { recursive: true });
// 初始化一个空的 Buffer 用于存储合并后的数据
let combinedData = Buffer.alloc(0);
// 遍历所有切片文件并读取内容
for (let i = 0; i < totalChunks; i++) {
// 获取每个切片文件的路径
const chunkPath = `${tempDir}${i}`;
// 读取当前切片文件的内容
const chunkData = await readFile(chunkPath);
// 合并切片文件的内容追加到 combinedData 中
combinedData = Buffer.concat([combinedData, chunkData]);
}
// 将合并后的数据写入最终的输出文件
await writeFile(outputFilePath, combinedData);
console.log('File merged successfully.');
// 删除临时切片文件
for (let i = 0; i < totalChunks; i++) {
const chunkPath = `${tempDir}${i}`;
try {
await unlink(chunkPath);
} catch (err) {
console.error(`Error deleting chunk ${i}:`, err);
}
}
// 删除临时文件夹
try {
await rmdir(tempDir, { recursive: true });
console.log('Temporary directory deleted successfully.');
} catch (err) {
console.error('Error deleting temporary directory:', err);
}
}
// 处理文件上传
app.post('/upload', upload.single('file'), async (req, res) => {
const { filename, chunkIndex, totalChunks } = req.body;
const chunkPath = `uploads/${filename}/${chunkIndex}`;
try {
// 创建文件切片目录
await mkdir(path.dirname(chunkPath), { recursive: true });
// 移动上传的文件到切片目录
await rename(req.file.path, chunkPath);
console.log(`Chunk ${chunkIndex} saved successfully`);
// 如果这是最后一个切片,则合并所有切片
if (parseInt(chunkIndex) === parseInt(totalChunks) - 1) {
await mergeChunks(filename, totalChunks);
}
res.status(200).send('Chunk received');
} catch (err) {
console.error(`Error handling chunk ${chunkIndex}:`, err);
res.status(500).send('Internal Server Error');
}
});
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
注意:
- 将
fs
模块的方法转换为Promise
版本,以便防止文件合并顺序错误而导致文件损坏。- 在创建输出文件流时,设置
flags: 'w'
和encoding: null
,确保以二进制格式写入文件。- 在创建输入文件流时,设置
encoding: null
,确保以二进制格式读取文件。
总结
前端:
点击按钮选取文件后,通过事件对象 event
拿到文件并按指定大小(如 1MB)进行分片,使用循环遍历每个分片,创建 blob
对象表示分片,将分片及其相关信息(文件名、分片索引、总分片数)封装到 FormData
对象中,最后使用 axios
发送 POST
请求上传每个分片。
服务端:
服务端通过 API 接口(如 /upload
)接收前端上传的每个分片,解析请求中的 formData
,提取分片数据、文件名、分片索引和总分片数,使用 express
和 multer
接收这些片段,将其保存到临时目录,并在接收到最后一个片段时调用 mergeChunks
函数将所有片段合并成一个完整的文件。合并完成后,删除临时文件。整个过程包括文件切片、上传、保存、合并和清理,确保了大文件的高效传输和处理。