React+Node(Express)本地服务器视频分片上传方案咨询
大视频分片上传至本地服务器实现方案(React + Express)
以下是针对React前端+Express后端的大视频分片上传实现方案,完全基于本地服务器,无需依赖AWS S3:
前端React实现
核心逻辑
- 文件分片切割:利用浏览器原生
File.slice()方法将大文件切割为固定大小的分片(推荐10MB-50MB,可根据网络情况调整)。 - 分片上传:逐个或并发上传分片,每个分片携带唯一文件标识、分片索引、总分片数等元信息。
- 进度跟踪:监听每个分片的上传进度,计算整体上传进度。
- 断点续传(可选):上传前先查询服务器已上传的分片,跳过已完成的部分,节省流量和时间。
代码示例
import axios from 'axios'; import { useState } from 'react'; const VideoUploader = () => { const [progress, setProgress] = useState(0); // 生成文件唯一标识(避免同名文件冲突) const getFileId = (file) => `${file.name}-${file.lastModified}`; // 切割文件为分片 const splitFile = (file, chunkSize = 10 * 1024 * 1024) => { const chunks = []; let start = 0; const totalChunks = Math.ceil(file.size / chunkSize); while (start < file.size) { const end = Math.min(start + chunkSize, file.size); chunks.push(file.slice(start, end)); start = end; } return { chunks, totalChunks, fileId: getFileId(file) }; }; // 检查已上传分片(断点续传用) const checkUploadedChunks = async (fileId) => { const res = await axios.post('/api/check-chunks', { fileId }); return res.data.uploadedChunks; }; // 上传单个分片 const uploadSingleChunk = async (chunk, index, totalChunks, fileId, fileName) => { const formData = new FormData(); formData.append('chunk', chunk); formData.append('index', index); formData.append('totalChunks', totalChunks); formData.append('fileId', fileId); formData.append('fileName', fileName); return axios.post('/api/upload-chunk', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { const chunkProgress = e.loaded / e.total; const overallProgress = ((index + chunkProgress) / totalChunks) * 100; setProgress(Math.min(overallProgress, 100)); } }); }; // 批量上传分片(限制并发数为3) const uploadChunks = async (file) => { const { chunks, totalChunks, fileId } = splitFile(file); const uploadedChunks = await checkUploadedChunks(fileId); const tasks = []; // 跳过已上传的分片 for (let i = 0; i < chunks.length; i++) { if (!uploadedChunks.includes(i)) { tasks.push(uploadSingleChunk(chunks[i], i, totalChunks, fileId, file.name)); // 限制并发数,避免服务器压力过大 if (tasks.length >= 3) { await Promise.all(tasks); tasks.length = 0; } } } // 完成剩余分片上传 if (tasks.length > 0) await Promise.all(tasks); // 所有分片上传完成,请求合并 await axios.post('/api/merge-chunks', { fileId, fileName: file.name }); setProgress(100); alert('视频上传完成!'); }; return ( <div> <input type="file" accept="video/*" onChange={(e) => uploadChunks(e.target.files[0])} /> <div>上传进度:{progress.toFixed(2)}%</div> </div> ); }; export default VideoUploader;
后端Express实现
核心逻辑
- 分片接收与存储:使用
multer中间件处理分片上传,将分片临时存储到指定目录,文件名包含文件唯一标识和分片索引。 - 已上传分片查询:根据文件唯一标识,遍历临时目录返回已上传的分片索引。
- 分片合并:收到合并请求后,按分片索引顺序读取所有临时分片,写入最终文件到目标存储目录,完成后清理临时分片文件。
代码示例
首先安装依赖:
npm install express multer
然后创建上传路由文件routes/upload.js:
const express = require('express'); const router = express.Router(); const multer = require('multer'); const fs = require('fs'); const path = require('path'); // 配置目录 const tempChunkDir = path.join(__dirname, '../temp-chunks'); const uploadDir = path.join(__dirname, '../uploads'); // 确保目录存在 if (!fs.existsSync(tempChunkDir)) fs.mkdirSync(tempChunkDir, { recursive: true }); if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); // multer配置:存储分片到临时目录 const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, tempChunkDir), filename: (req, file, cb) => { const { fileId, index } = req.body; cb(null, `${fileId}-${index}.chunk`); } }); const upload = multer({ storage }); // 上传单个分片 router.post('/upload-chunk', upload.single('chunk'), (req, res) => { res.status(200).json({ success: true }); }); // 查询已上传的分片 router.post('/check-chunks', (req, res) => { const { fileId } = req.body; const uploadedChunks = []; fs.readdirSync(tempChunkDir).forEach(file => { if (file.startsWith(fileId)) { const index = parseInt(file.split('-')[1].split('.')[0]); uploadedChunks.push(index); } }); res.status(200).json({ uploadedChunks }); }); // 合并所有分片 router.post('/merge-chunks', (req, res) => { const { fileId, fileName } = req.body; const finalFilePath = path.join(uploadDir, fileName); const writeStream = fs.createWriteStream(finalFilePath); // 获取所有分片并按索引排序 const chunkFiles = fs.readdirSync(tempChunkDir) .filter(file => file.startsWith(fileId)) .sort((a, b) => { const idxA = parseInt(a.split('-')[1].split('.')[0]); const idxB = parseInt(b.split('-')[1].split('.')[0]); return idxA - idxB; }); // 逐个写入分片并清理临时文件 const writeChunk = (index) => { if (index >= chunkFiles.length) { writeStream.end(() => { res.status(200).json({ success: true, filePath: `/uploads/${fileName}` }); }); return; } const chunkPath = path.join(tempChunkDir, chunkFiles[index]); const chunkData = fs.readFileSync(chunkPath); writeStream.write(chunkData, () => { fs.unlinkSync(chunkPath); writeChunk(index + 1); }); }; writeChunk(0); }); module.exports = router;
在主app.js中引入路由:
const express = require('express'); const app = express(); const uploadRoutes = require('./routes/upload'); app.use(express.json()); app.use('/api', uploadRoutes); app.listen(3001, () => console.log('Server running on port 3001'));
关键注意事项
- 分片大小选择:建议10MB-50MB,过小会增加请求次数,过大则容易导致单次上传失败。
- 文件冲突避免:使用
文件名+最后修改时间或文件哈希(如spark-md5)作为唯一标识,防止同名文件覆盖。 - 错误重试机制:前端需捕获分片上传失败的情况,自动重试对应分片,提升上传稳定性。
- 临时文件清理:可定时清理
temp-chunks目录中超过24小时的分片文件,避免磁盘空间浪费。 - 并发控制:前端限制并发上传数量(如3-5个),防止服务器因同时处理过多请求而崩溃。
内容的提问来源于stack exchange,提问作者Hayk Kirakosyan
相关产品推荐
相关产品推荐

