You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+Node(Express)本地服务器视频分片上传方案咨询

大视频分片上传至本地服务器实现方案(React + Express)

以下是针对React前端+Express后端的大视频分片上传实现方案,完全基于本地服务器,无需依赖AWS S3:

前端React实现

核心逻辑

  1. 文件分片切割:利用浏览器原生File.slice()方法将大文件切割为固定大小的分片(推荐10MB-50MB,可根据网络情况调整)。
  2. 分片上传:逐个或并发上传分片,每个分片携带唯一文件标识、分片索引、总分片数等元信息。
  3. 进度跟踪:监听每个分片的上传进度,计算整体上传进度。
  4. 断点续传(可选):上传前先查询服务器已上传的分片,跳过已完成的部分,节省流量和时间。

代码示例

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实现

核心逻辑

  1. 分片接收与存储:使用multer中间件处理分片上传,将分片临时存储到指定目录,文件名包含文件唯一标识和分片索引。
  2. 已上传分片查询:根据文件唯一标识,遍历临时目录返回已上传的分片索引。
  3. 分片合并:收到合并请求后,按分片索引顺序读取所有临时分片,写入最终文件到目标存储目录,完成后清理临时分片文件。

代码示例

首先安装依赖:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 07:26:07