前言
在现代的 Web 开发中,大文件上传是一个常见的需求。无论是上传高清图片、大型视频,还是其他大文件,传统的单文件上传方式往往会因为文件过大而导致上传失败、网络卡顿等问题。为了解决这些问题,分片上传技术应运而生。今天,我们就来详细探讨如何在前后端实现分片上传大文件,让文件上传变得轻松又高效。
一、为什么需要分片上传?
分片上传的核心思想是将一个大文件拆分成多个小块(分片),然后分别上传这些小块。在服务器端再将这些分片合并成完整的文件。这种方式有以下优点:
- 提高上传成功率:小分片更容易上传成功,避免了因文件过大导致的网络超时等问题。
- 支持断点续传:如果某个分片上传失败,可以重新上传该分片,而不需要重新上传整个文件。
- 优化用户体验:用户可以看到每个分片的上传进度,提升整体的使用体验。
接下来,我们就通过一个实际的案例,来看看如何在 Node.js 后端和 React 前端实现分片上传。
二、Node.js 后端实现
1. 设置文件存储路径
我们创建一个express项目,然后使用 multer 来处理文件上传。
首先,需要设置文件的存储路径。如果文件是分片上传的,我们会为每个文件创建一个临时文件夹,用于存储分片。
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const fileId = req.body.fileId;
if (fileId) {
const tempDir = path.join(uploadDir, fileId);
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
cb(null, tempDir);
} else {
cb(null, uploadDir);
}
},
filename: function (req, file, cb) {
const originalName = req.body.originalName || file.originalname;
if (req.body.index !== undefined) {
const chunkIndex = req.body.index;
cb(null, `${originalName}-chunk-${chunkIndex}`);
} else {
cb(null, originalName);
}
}
});
const upload = multer({ storage }).fields([
{ name: 'file', maxCount: 1 },
{ name: 'chunk', maxCount: 1 }
]);
2. 上传接口
上传接口会根据是否有分片信息来决定是直接保存文件信息,还是将分片信息存入数据库。
const uploadFile = async (req, res, next) => {
try {
const { files } = req;
let file = files['file'] ? files['file'][0] : null;
let chunk = files['chunk'] ? files['chunk'][0] : null;
const fileToProcess = chunk || file;
if (!fileToProcess) {
return next();
}
const { index, totalChunks, fileId, originalName, userId, folderId } = req.body;
const chunkFile = fileToProcess.path;
if (!totalChunks || totalChunks === '1') {
const fileId = await saveFileInfo(userId, folderId, originalName, chunkFile, fileToProcess.size);
res.json({ success: true, message: '文件上传成功', fileId });
} else {
const insertQuery = 'INSERT INTO file_chunks (fileId, chunkIndex, totalChunks, filePath, originalFilename) VALUES (?, ?, ?, ?, ?)';
await db.query(insertQuery, [fileId, index, totalChunks, chunkFile, originalName]);
res.json({ success: true, message: '分片上传成功' });
}
} catch (error) {
next('文件上传失败');
}
};
3. 合并分片
当所有分片都上传完成后,我们需要将分片合并成完整的文件,并删除临时文件夹。
const mergeFile = async (req, res, next) => {
const { fileId, totalChunks, originalName, userId, folderId } = req.body;
const targetDir = path.join(__dirname, '../uploads', fileId);
const filePath = path.join(__dirname, '../uploads', originalName);
const [rows] = await db.query('SELECT COUNT(*) AS count FROM file_chunks WHERE fileId = ?', [fileId]);
if (rows[0].count == totalChunks) {
fs.writeFileSync(filePath, '');
for (let i = 0; i < totalChunks; i++) {
const currentChunkPath = path.join(targetDir, `${originalName}-chunk-${i}`);
fs.appendFileSync(filePath, fs.readFileSync(currentChunkPath));
fs.unlinkSync(currentChunkPath);
}
fs.rmdirSync(targetDir);
const fileSize = fs.statSync(filePath).size;
const fileId = await saveFileInfo(userId, folderId, originalName, filePath, fileSize);
res.json({ success: true, message: '文件合并成功', fileId });
} else {
res.status(400).json({ success: false, message: '所有的分片未全部上传' });
}
};
三、React 前端实现
1. 分片上传逻辑
在前端,我们需要判断文件大小是否超过 5MB,这个文件大小可以根据自己的项目需求做一个限制。
如果超过,则进行分片上传;否则进行普通上传。
const handleUpload = async (file) => {
// 5MB 文件大小
const chunkSize = 5 * 1024 * 1024;
const fileSize = file.size;
// 创建一个唯一ID
const fileId = Date.now().toString();
setProgress(0);
setUploading(true);
if (fileSize > chunkSize) {
const totalChunks = Math.ceil(fileSize / chunkSize);
for (let i = 0; i < totalChunks; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
const chunkInfo = {
userId,
folderId,
file,
chunk,
index: i,
totalChunks,
fileId
};
await uploadsFile(chunkInfo);
setProgress((prevProgress) => prevProgress + 100 / totalChunks);
}
try {
await mergeChunks({
fileId,
totalChunks,
userId,
folderId,
originalName: file.name
});
message.success('文件合并成功!');
} catch (error) {
message.error('文件合并失败!');
console.error('Error merging file chunks:', error);
}
} else {
const fileInfo = {
userId,
folderId,
file
};
await uploadsFile(fileInfo);
setProgress(100);
}
setUploading(false);
message.success('文件上传成功!');
};
2. 上传接口调用
在上传接口调用时,我们需要根据是否有分片信息来构建不同的表单数据。
export const uploadsFile = (fileInfo) => {
const formData = new FormData();
formData.append('userId', fileInfo.userId.toString());
formData.append('folderId', fileInfo.folderId.toString());
formData.append('originalName', fileInfo.file.name);
if (fileInfo.chunk && fileInfo.index !== undefined && fileInfo.totalChunks && fileInfo.fileId) {
formData.append('index', fileInfo.index.toString());
formData.append('totalChunks', fileInfo.totalChunks.toString());
formData.append('fileId', fileInfo.fileId);
formData.append('chunk', fileInfo.chunk);
} else {
formData.append('file', fileInfo.file);
}
return request.post(`/files/upload`, formData);
};
export const mergeChunks = (chunkInfo) => {
return request.post(`/files/merge`, chunkInfo);
};
四、总结
通过上述的前后端实现,我们可以轻松地完成大文件的分片上传。这种方式不仅提高了上传的成功率,还优化了用户体验。 在实际开发中,你可以根据具体需求调整分片大小、并发上传的分片数等参数,以达到最佳的性能。
希望这篇文章能帮助你在项目中实现高效的大文件上传功能!如果你有任何问题或建议,欢迎在评论区留言交流。