大文件上传最全指南:分片、断点续传、秒传,一篇就够了

上传一个2G的视频,网络断了,从头再来。用户心态崩了,你也崩了。今天我们用前端+Node.js实现一套完整的大文件上传方案:分片上传、断点续传、秒传、进度条。代码可直接复制到项目中使用。

一、痛点:为什么大文件上传要"分片"?

传统<input type="file"> + FormData上传,是一次性把整个文件发到服务器。缺点:

  • 网络中断就得重头传
  • 占用大量内存和带宽
  • 无法实时看到进度
  • 服务器接收超时风险高

分片上传:把大文件切成小块(比如每片1MB),一片一片传,失败只重传失败的片。所有片传完,服务器再合并。

二、前端实现:分片 + 断点续传 + 秒传

1. 文件分片 & 计算hash

js 复制代码
// 分片大小:1MB
const CHUNK_SIZE = 1024 * 1024;

// 计算文件hash(用于秒传和断点续传)
async function calculateHash(file) {
  const buffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

// 生成分片
function createChunks(file) {
  const chunks = [];
  let start = 0;
  while (start < file.size) {
    const end = Math.min(start + CHUNK_SIZE, file.size);
    chunks.push(file.slice(start, end));
    start = end;
  }
  return chunks;
}

2. 上传前检查(秒传 & 断点续传)

js 复制代码
async function uploadFile(file) {
  const hash = await calculateHash(file);
  // 1. 检查文件是否已存在(秒传)
  const checkRes = await fetch('/api/check', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ hash })
  });
  const { uploaded } = await checkRes.json();
  if (uploaded) {
    alert('秒传成功!');
    return;
  }

  // 2. 获取已上传的分片索引(断点续传)
  const progressRes = await fetch('/api/progress', {
    method: 'POST',
    body: JSON.stringify({ hash })
  });
  const { uploadedChunks = [] } = await progressRes.json();

  const chunks = createChunks(file);
  const total = chunks.length;
  let completed = 0;

  // 3. 并发上传未完成的分片(限制并发数)
  const concurrency = 5;
  const queue = [...chunks.entries()];
  const active = new Set();

  while (queue.length || active.size) {
    while (active.size < concurrency && queue.length) {
      const [index, chunk] = queue.shift();
      if (uploadedChunks.includes(index)) {
        completed++;
        updateProgress(completed, total);
        continue;
      }
      const promise = uploadChunk(hash, index, chunk, total);
      active.add(promise);
      promise.finally(() => {
        active.delete(promise);
        completed++;
        updateProgress(completed, total);
      });
    }
    await Promise.race(active);
  }
  // 4. 通知服务器合并
  await fetch('/api/merge', {
    method: 'POST',
    body: JSON.stringify({ hash, fileName: file.name, total })
  });
}

3. 上传单个分片

js 复制代码
async function uploadChunk(hash, index, chunk, total) {
  const formData = new FormData();
  formData.append('hash', hash);
  formData.append('index', index);
  formData.append('total', total);
  formData.append('chunk', chunk);
  await fetch('/api/upload', { method: 'POST', body: formData });
}

4. 进度条

js 复制代码
function updateProgress(completed, total) {
  const percent = (completed / total * 100).toFixed(2);
  document.getElementById('progress').style.width = `${percent}%`;
  document.getElementById('progress-text').innerText = `${percent}%`;
}

三、Node.js后端实现

js 复制代码
const express = require('express');
const multer = require('multer');
const fs = require('fs-extra');
const path = require('path');

const app = express();
const UPLOAD_DIR = path.resolve(__dirname, 'uploads');
const TEMP_DIR = path.resolve(__dirname, 'temp');

app.use(express.json());

// 检查文件是否已存在(秒传)
app.post('/api/check', async (req, res) => {
  const { hash } = req.body;
  const filePath = path.join(UPLOAD_DIR, hash);
  if (await fs.pathExists(filePath)) {
    res.json({ uploaded: true });
  } else {
    res.json({ uploaded: false });
  }
});

// 获取已上传的分片(断点续传)
app.post('/api/progress', async (req, res) => {
  const { hash } = req.body;
  const chunkDir = path.join(TEMP_DIR, hash);
  let uploadedChunks = [];
  if (await fs.pathExists(chunkDir)) {
    const files = await fs.readdir(chunkDir);
    uploadedChunks = files.map(f => parseInt(f.split('_')[1]));
  }
  res.json({ uploadedChunks });
});

// 上传分片
const storage = multer.diskStorage({
  destination: async (req, file, cb) => {
    const { hash } = req.body;
    const chunkDir = path.join(TEMP_DIR, hash);
    await fs.ensureDir(chunkDir);
    cb(null, chunkDir);
  },
  filename: (req, file, cb) => {
    const { index } = req.body;
    cb(null, `${file.originalname}_${index}`);
  }
});
const upload = multer({ storage });
app.post('/api/upload', upload.single('chunk'), (req, res) => {
  res.json({ ok: true });
});

// 合并分片
app.post('/api/merge', async (req, res) => {
  const { hash, fileName, total } = req.body;
  const chunkDir = path.join(TEMP_DIR, hash);
  const filePath = path.join(UPLOAD_DIR, `${hash}_${fileName}`);
  const writeStream = fs.createWriteStream(filePath);
  for (let i = 0; i < total; i++) {
    const chunkPath = path.join(chunkDir, `${fileName}_${i}`);
    const data = await fs.readFile(chunkPath);
    writeStream.write(data);
  }
  writeStream.end();
  await fs.remove(chunkDir);
  res.json({ ok: true });
});

四、扩展:秒传原理 & 断点续传

  • 秒传:上传前计算文件hash,服务端检查是否已存在。有则直接返回,不需要重新上传。
  • 断点续传:每次上传前询问服务端已上传的分片索引,只传缺失的。
  • 并发控制:限制同时上传的分片数量,避免网络拥塞(上面代码限制5个并发)。
  • 失败重试 :可在uploadChunk中加入重试逻辑,比如失败后重试3次。

五、生产环境注意事项

  1. 分片大小:网络好的环境可以用5MB/片,差的用1MB。可根据文件大小动态调整。
  2. hash计算 :大文件计算hash较慢,可以用requestIdleCallback或在Web Worker中进行,避免阻塞UI。
  3. 服务端合并 :上面的合并方式是逐个读取写入,大文件可能占用内存。可用createReadStream + pipe流式合并。
  4. 清理临时文件:定期清理未合并完成的临时分片(比如超过24小时未合并的删除)。
  5. HTTPS:分片上传涉及文件数据,务必使用HTTPS。

六、总结

功能 实现方式
分片上传 File.slice() + 并发控制
断点续传 服务端记录已上传分片 + 前端跳过
秒传 文件hash + 服务端比对
进度条 已上传分片数 / 总分片数

这套方案已在我司多个项目中稳定运行,支持GB级文件上传。你可以根据自己的需求调整分片大小、并发数、重试策略。

你在大文件上传中还遇到过什么坑?评论区交流。点个赞让更多需要的人看到。

相关推荐
我叫黑大帅2 小时前
解决聊天页内部滚轮改为页面滚动问题
javascript·后端·面试
郑洁文2 小时前
基于Python的Web命令执行漏洞自动化检测系统
前端·python·网络安全·自动化
新酱爱学习2 小时前
手搓 10 个 Skill 后,我把重复劳动收敛成了一套零依赖 CLI 工具
前端·javascript·人工智能
罗超驿2 小时前
13.JavaScript 新手入门指南:语法、变量、流程控制全解析
开发语言·javascript
IT_陈寒2 小时前
Python的线程池居然把我坑在了垃圾回收这块
前端·人工智能·后端
ct9783 小时前
Three.js 性能优化(测量-定位-优化)
javascript·性能优化·three
研☆香3 小时前
es6新特性功能介绍(一)
前端·ecmascript·es6
陈_杨3 小时前
鸿蒙开发-疾阅App阅读训练功能技术解析
前端·javascript