万普插件库

jQuery插件大全与特效教程

切片上传的完整实现流程(前端 + 后端)

图片上传中的切片上传(分片上传) 是针对大文件(如几十 MB 到 GB 级)的优化方案,核心思想是:将大文件分割成多个小 “切片”(Chunk),分别上传到服务器,最后由服务器将所有切片合并成原始文件。

这种方式解决了大文件上传的三大痛点:

1、单次请求体积过大导致的超时或失败;

2、网络中断后需要重新上传整个文件的问题(支持断点续传);

3、并行上传多个切片,提高上传效率。

一、前端实现(核心步骤)

前端负责文件分割、切片标识、并发上传、断点续传控制

1. 基本准备

  • 获取用户选择的文件(通过 <input type="file">);
  • 定义切片大小(如 1MB / 片,可根据业务调整);
  • 生成文件唯一标识(用于服务器识别同一文件的切片,避免混淆)。
// 获取文件
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;

  // 配置
  const chunkSize = 1024 * 1024; // 1MB/切片
  const totalChunks = Math.ceil(file.size / chunkSize); // 总切片数
  const fileHash = await generateFileHash(file); // 文件唯一标识(见步骤2)
});

2. 生成文件唯一标识(关键)

需要给同一文件的所有切片分配一个共同的 “父标识”,确保服务器能正确合并。常用方式:

  • 基于文件名 + 文件大小 + 最后修改时间生成 hash;
  • 读取文件内容生成 MD5/SHA-1(更严谨,但大文件计算耗时)。
// 简化版:基于文件名+大小+最后修改时间生成hash
function generateFileHash(file) {
  const { name, size, lastModified } = file;
  return `${name}-${size}-${lastModified}`; // 实际项目可加MD5加密
}

3. 分割文件为切片

使用 File.prototype.slice() 方法分割文件(类似字符串 slice,不修改原文件)。

// 分割文件为切片
function createChunks(file, chunkSize, fileHash) {
  const chunks = [];
  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    // 每个切片包含:文件标识、切片索引、切片内容、总切片数
    chunks.push({
      fileHash,
      chunkIndex: i,
      totalChunks,
      chunk: file.slice(start, end) // 切片内容(Blob对象)
    });
  }
  return chunks;
}

4. 并发上传切片

  • 同时上传多个切片(控制并发数,避免请求过多);
  • 每个切片通过 FormData 发送(包含切片信息和二进制内容)。
// 并发上传(控制最大并发数为3)
async function uploadChunks(chunks) {
  const maxConcurrent = 3; // 最大并发数
  const results = [];
  // 按并发数分组上传
  for (let i = 0; i < Math.ceil(chunks.length / maxConcurrent); i++) {
    const group = chunks.slice(i * maxConcurrent, (i + 1) * maxConcurrent);
    // 并发上传当前组
    const groupResults = await Promise.all(
      group.map(chunk => uploadSingleChunk(chunk))
    );
    results.push(...groupResults);
  }
  return results;
}

// 上传单个切片
async function uploadSingleChunk(chunk) {
  const formData = new FormData();
  formData.append('fileHash', chunk.fileHash);
  formData.append('chunkIndex', chunk.chunkIndex);
  formData.append('totalChunks', chunk.totalChunks);
  formData.append('chunk', chunk.chunk); // 二进制切片

  const response = await fetch('/api/upload-chunk', {
    method: 'POST',
    body: formData
  });
  return response.json();
}

5. 断点续传(可选但重要)

上传前先查询服务器 “已上传的切片索引”,只上传缺失的部分:

// 检查已上传的切片
async function getUploadedChunks(fileHash) {
  const response = await fetch(`/api/check-chunks?fileHash=${fileHash}`);
  const { uploadedIndexes } = await response.json(); // 服务器返回已上传的索引数组
  return uploadedIndexes;
}

// 断点续传逻辑
async function resumeUpload(file) {
  const fileHash = await generateFileHash(file);
  const uploadedIndexes = await getUploadedChunks(fileHash); // 获取已传切片
  const allChunks = createChunks(file, chunkSize, fileHash);
  // 过滤出未上传的切片
  const needUploadChunks = allChunks.filter(
    chunk => !uploadedIndexes.includes(chunk.chunkIndex)
  );
  await uploadChunks(needUploadChunks); // 只传缺失的
}

6. 通知服务器合并切片

所有切片上传完成后,调用 “合并接口”,让服务器将切片按顺序拼接成原始文件。

// 通知服务器合并切片
async function mergeChunks(fileHash, fileName) {
  const response = await fetch('/api/merge-chunks', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      fileHash,
      fileName, // 原始文件名(用于保存)
      totalChunks // 总切片数(校验是否完整)
    })
  });
  return response.json();
}

二、后端实现(核心步骤)

后端负责接收切片、临时存储、校验完整性、合并切片(以 Node.js 为例)。

1. 接收切片并临时存储

  • 为每个文件创建单独的临时目录(以fileHash命名);
  • 切片文件以chunkIndex命名(如0、1、2...),方便后续按顺序合并。
// Node.js + Express 示例(接收切片)
const express = require('express');
const multer = require('multer');
const fs = require('fs-extra'); // 增强版fs,支持递归创建目录
const path = require('path');

const app = express();
const upload = multer({ dest: 'temp/' }); // 临时存储目录

// 接收切片接口
app.post('/api/upload-chunk', upload.single('chunk'), async (req, res) => {
  const { fileHash, chunkIndex } = req.body;
  const chunkFile = req.file; // 上传的切片文件

  // 创建临时目录(如:temp/abc123/)
  const tempDir = path.join('temp', fileHash);
  await fs.ensureDir(tempDir);

  // 将切片移动到临时目录,命名为chunkIndex(如:temp/abc123/0)
  const targetPath = path.join(tempDir, chunkIndex);
  await fs.move(chunkFile.path, targetPath);

  res.json({ code: 0, message: '切片上传成功' });
});

2. 检查已上传的切片(支持断点续传)

查询临时目录中已存在的切片索引,返回给前端。

// 检查已上传切片接口
app.get('/api/check-chunks', async (req, res) => {
  const { fileHash } = req.query;
  const tempDir = path.join('temp', fileHash);

  // 目录不存在,说明未上传过
  if (!await fs.exists(tempDir)) {
    return res.json({ uploadedIndexes: [] });
  }

  // 读取目录中的所有切片文件,提取索引
  const chunkFiles = await fs.readdir(tempDir);
  const uploadedIndexes = chunkFiles.map(name => parseInt(name, 10));

  res.json({ uploadedIndexes });
});

3. 合并切片为原始文件

  • 按chunkIndex从小到大的顺序读取所有切片;
  • 将切片内容依次写入目标文件;
  • 合并完成后删除临时目录。
// 合并切片接口
app.post('/api/merge-chunks', async (req, res) => {
  const { fileHash, fileName, totalChunks } = req.body;
  const tempDir = path.join('temp', fileHash);
  const targetPath = path.join('uploads', fileName); // 最终保存路径

  // 检查临时目录是否存在
  if (!await fs.exists(tempDir)) {
    return res.status(400).json({ code: 1, message: '切片不存在' });
  }

  // 读取所有切片,按索引排序
  const chunkFiles = await fs.readdir(tempDir);
  if (chunkFiles.length !== totalChunks) {
    return res.status(400).json({ code: 1, message: '切片不完整' });
  }
  // 按数字排序(确保0,1,2...顺序)
  chunkFiles.sort((a, b) => parseInt(a) - parseInt(b));

  // 创建目标文件的写入流
  const writeStream = fs.createWriteStream(targetPath);

  // 按顺序拼接切片
  for (const chunkIndex of chunkFiles) {
    const chunkPath = path.join(tempDir, chunkIndex);
    // 读取切片内容并写入目标文件
    await new Promise((resolve, reject) => {
      const readStream = fs.createReadStream(chunkPath);
      readStream.pipe(writeStream, { end: false }); // 不自动关闭写入流
      readStream.on('end', resolve);
      readStream.on('error', reject);
    });
  }

  // 所有切片写入完成,关闭流
  writeStream.end();
  // 删除临时目录
  await fs.remove(tempDir);

  res.json({ code: 0, message: '文件合并成功', url: `/uploads/${fileName}` });
});

三、关键注意事项

  1. 切片校验
    可给每个切片生成 MD5,上传时携带,服务器接收后校验,避免切片损坏。
  2. 并发控制
    前端控制并发数(如 3-5 个),避免同时发送过多请求导致服务器压力过大。
  3. 大文件 hash 优化
    若通过文件内容生成 hash,可只读取前 100KB + 中间 100KB + 最后 100KB 计算,减少大文件 hash 计算耗时。
  4. 超时与重试
    单个切片上传失败时,实现自动重试机制(如最多重试 3 次)。
  5. 文件清理
    服务器定期清理长期未合并的临时切片(如 24 小时未完成合并),释放磁盘空间。》

切片上传的核心是 “分而治之”:前端将大文件拆分为小切片并行上传,后端接收后按顺序合并。配合断点续传,可大幅提升大文件上传的稳定性和效率,是企业级文件上传场景的标配方案。

四、可用的轮子

有很多成熟的 npm 库可以简化大文件切片上传的实现,无需从零开发。这些库通常封装了切片分割、并发控制、断点续传、进度监控等核心功能,以下是几个常用的库:

1.resumable.js(经典老牌库)

  • 特点:支持断点续传、并发上传、暂停 / 继续功能,兼容性好(支持 IE8+),使用简单。
  • 原理:基于 HTML5 File API 实现切片,通过 XMLHttpRequest 上传,后端需配合实现接收和合并逻辑。
  • 使用示例
<input type="file" id="fileInput" />
<script src="node_modules/resumable.js/resumable.js"></script>
<script>
  const resumable = new Resumable({
    target: '/api/upload', // 后端接收接口
    chunkSize: 1*1024*1024, // 1MB 切片
    simultaneousUploads: 3, // 并发数
    testChunks: true, // 开启断点续传(先检查已上传切片)
    throttleProgressCallbacks: 1,
  });

  // 绑定文件选择
  document.getElementById('fileInput').addEventListener('change', (e) => {
    resumable.addFiles(e.target.files);
  });

  // 开始上传
  resumable.upload();

  // 监控进度
  resumable.on('progress', () => {
    console.log('进度:', resumable.progress() * 100 + '%');
  });

  // 上传完成
  resumable.on('complete', () => {
    console.log('上传完成');
  });
</script>
  • 后端适配:需要根据库的协议实现接口(接收切片、检查已上传切片、合并切片),官方提供了多种后端语言的示例(Node.js/Java/PHP 等)。

2.webuploader(百度开发,功能全面)

  • 特点:百度团队开发的大文件上传组件,支持切片上传、断点续传、图片预览、拖拽上传等,文档丰富,适合国内场景。
  • 注意:虽然是较早期的库,但稳定性好,仍被广泛使用。
  • 使用示例
<div id="uploader" class="wu-example">
  <div class="btns">
    <div id="picker">选择文件</div>
    <button id="uploadBtn" class="btn btn-default">开始上传</button>
  </div>
</div>

<script src="node_modules/webuploader/dist/webuploader.min.js"></script>
<script>
  const uploader = WebUploader.create({
    swf: 'node_modules/webuploader/dist/Uploader.swf', // 兼容低版本浏览器的SWF
    server: '/api/upload', // 后端接口
    pick: '#picker', // 选择文件的按钮
    chunked: true, // 开启切片上传
    chunkSize: 1*1024*1024, // 1MB 切片
    chunkRetry: 3, // 失败重试次数
    threads: 3, // 并发数
  });

  // 绑定上传按钮
  document.getElementById('uploadBtn').addEventListener('click', () => {
    uploader.upload();
  });

  // 监控进度
  uploader.on('uploadProgress', (file, percentage) => {
    console.log('进度:', percentage * 100 + '%');
  });
</script>

3.uppy(现代前端上传库,灵活度高)

  • 特点:由 Transloadit 开发的现代上传库,支持切片上传、断点续传、拖拽、第三方存储(如 S3)集成,API 设计优雅,可按需扩展。
  • 优势:支持 React/Vue 框架,UI 组件可定制,适合现代前端项目。
  • 安装:npm install uppy @uppy/core @uppy/xhr-upload
  • 使用示例(原生 JS)
import Uppy from '@uppy/core';
import XHRUpload from '@uppy/xhr-upload';
import '@uppy/core/dist/style.min.css';

const uppy = new Uppy()
  .use(XHRUpload, {
    endpoint: '/api/upload', // 后端接口
    chunkSize: 1024 * 1024, // 1MB 切片
    parallelUploads: 3, // 并发数
    resume: true, // 支持断点续传
    retryDelays: [0, 1000, 3000, 5000], // 重试延迟
  });

// 监听文件添加
uppy.on('file-added', (file) => {
  console.log('添加文件:', file.name);
});

// 监听上传进度
uppy.on('upload-progress', (file, progress) => {
  console.log('进度:', progress.bytesUploaded / progress.totalBytes * 100 + '%');
});

// 触发上传
uppy.upload().then((result) => {
  console.log('上传完成:', result.successful);
});
  • 框架集成:提供 @uppy/react@uppy/vue 等包,可在框架中组件化使用。

4.tus-js-client(基于 tus 协议,标准化方案)

  • 特点:基于 tus 协议(一种标准化的断点续传协议),支持跨语言 / 跨平台,后端若实现 tus 协议,可无缝对接。
  • 优势:协议标准化,后端可选择现成的 tus 服务(如 Node.js 的 tus-node-server),无需自己实现合并逻辑。
  • 安装:npm install tus-js-client
  • 使用示例
import tus from 'tus-js-client';

const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (e) => {
  const file = e.target.files[0];
  const upload = new tus.Upload(file, {
    endpoint: 'https://your-tus-server.com/files/', // tus 服务端地址
    retryDelays: [1000, 3000, 5000],
    onProgress: (bytesUploaded, bytesTotal) => {
      const percentage = (bytesUploaded / bytesTotal * 100).toFixed(2);
      console.log(`进度:${percentage}%`);
    },
    onSuccess: () => {
      console.log('上传完成,文件地址:', upload.url);
    },
  });
  upload.start();
});
  • 后端选择:可直接使用现成的 tus 服务端实现(如 tus-node-server、tusd),无需自己写合并逻辑。
  • 如何选择?

    • 简单场景 + 兼容性需求:选 resumable.js 或 webuploader,开箱即用,文档成熟。
    • 现代前端项目 + 框架集成:选 uppy,API 更现代,支持 React/Vue,可定制性强。
    • 追求标准化 + 跨平台:选 tus-js-client,基于通用协议,后端可复用现成服务。
    控制面板
    您好,欢迎到访网站!
      查看权限
    网站分类
    最新留言