基于Node.js实现大文件断点续传的完整方案

 更新时间:2026年04月03日 09:12:22   作者:邹荣乐  
本文介绍了Vue前端和Node.js后端实现大文件断点续传的方法,前端通过分片上传、计算文件哈希、记录已上传分片索引实现断点续传;后端提供检查和上传分片、合并文件的接口,通过并发控制、错误重试等技术提高上传效率和稳定性,需要的朋友可以参考下

在 Vue 前端开发中实现大文件断点续传,需结合分片上传、哈希校验和进度恢复等技术。以下是具体实现方案:

一、技术原理

  • 分片上传:将大文件切割为多个小块(如 1MB/片)。
  • 唯一标识:计算文件哈希(如 MD5)作为文件唯一标识。
  • 断点记录:记录已上传的分片索引,中断后从断点继续上传。
  • 合并请求:所有分片上传完成后,通知后端合并文件。
  • 并发控制:p-limit 是一个控制请求并发数量的库。

1、p-limit

npm install p-limit

p-limit是一个用于限制并发操作的JavaScript库,主要用于控制同时执行的异步操作数量,以避免系统资源过度占用和性能下降。它通过维护一个请求队列来管理并发操作,当并发限制数未满时,新操作会立即执行;当达到限制数时,新操作会被加入等待队列,直到有空闲位置再执行。

使用方法
使用p-limit非常简单,首先需要创建一个限制对象并指定并发限制数。然后将需要执行的异步操作包装成一个函数,并通过调用限制对象的函数来执行这些操作。当并发限制数未满时,操作会立即执行;如果达到限制数,操作会被加入等待队列,按照先进先出的顺序执行

2、spark-md5

npm install spark-md5

Spark-md5是一个JavaScript库,用于快速计算文件或数据的MD5值,支持浏览器环境,可用于文件完整性校验和分片计算。其主要功能包括calculate、append、init和end等方法,适用于大文件分片处理。由于MD5是单向哈希,故无法解密。通过比较文件的MD5值,可以判断文件是否一致。

二、实现步骤

1、文件分片

// 使用 File.slice 方法分片
const chunkSize = 1 * 1024 * 1024; // 1MB
const chunks = [];
let start = 0;
while (start < file.size) {
  const end = Math.min(start + chunkSize, file.size);
  const chunk = file.slice(start, end);
  chunks.push(chunk);
  start = end;
}

2、计算文件哈希

/**
 * 计算文件的哈希值
 * @param file 文件对象
 * @returns Promise<string> 返回计算得到的哈希值
 */
const calculateFileHash = async (file) => {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer();
    const reader = new FileReader();
    reader.readAsArrayBuffer(file);
    reader.onload = (e) => {
      spark.append(e.target.result);
      resolve(spark.end());
    };
  });
};

3、查询已上传分片

file.value = uploadFile.raw;
fileHash.value = await calculateFileHash(file.value);
const { data } = await axios.get('/api/check', {
    params: { fileHash: fileHash.value }
});

4、上传分片

    // 上传未完成的分片
    const requests = chunks.map((chunk, index) => {
      if (uploadedChunkIndexes.value.includes(index)) return;

      const formData = new FormData();
      formData.append('chunk', chunk);
      formData.append('hash', fileHash.value);
      formData.append('index', index);

      return axios.post('/api/upload', formData, {
        onUploadProgress: (e) => {
          const percent = Math.round((e.loaded / e.total) * 100);
          updateProgress(index, percent);
        }
      }).then(() => {
        uploadedChunkIndexes.value.push(index);
        updateProgress();
      });
    }).filter(Boolean);

    await Promise.all(requests);

5、并发控制

// 引入 p-limit
import pLimit from 'p-limit';

// 设置最大并发数(例如 3)
const CONCURRENT_LIMIT = 3;

// 创建并发控制器
const limit = pLimit(CONCURRENT_LIMIT);

    // 生成任务列表(过滤已上传的分片)
    const tasks = chunks.map((chunk, index) => {
      if (uploadedChunkIndexes.value.includes(index)) {
        return Promise.resolve(); // 跳过已上传
      }

      // 将每个任务包裹在并发控制器中
      return limit(() => {
        const formData = new FormData();
        formData.append('chunk', chunk);
        formData.append('hash', fileHash.value);
        formData.append('index', index);

        return axios.post('/api/upload', formData, {
          onUploadProgress: (e) => {
          // 更新进度条(需结合 Vue 响应式状态)
          }
        }).then(() => {
          // 更新已上传的分片索引列表
          uploadedChunkIndexes.value.push(index);
        });
      });
    });

// 等待所有任务完成
await Promise.all(tasks);

6、通知合并文件

await axios.post('/api/merge', {
      fileName: file.value.name,
      fileHash: fileHash.value,
      chunkCount: chunks.length
});

5、错误重试

单个分片上传失败时自动重试(如 3 次)。

return limit(() => {
  const MAX_RETRY = 3;
  let retryCount = 0;

  const attemptUpload = () => {
    return axios.post('/api/upload').catch((error) => {
      if (retryCount < MAX_RETRY) {
        retryCount++;
        return attemptUpload();
      }
      throw error;
    });
  };

  return attemptUpload();
});

前端所有代码

7、前端页面

<template>
  <div class="upload-container">
    <!-- 文件选择 -->
    <el-upload class="upload-demo" drag :auto-upload="false" :on-change="handleFileChange" :show-file-list="false">
      <el-icon class="el-icon--upload"><upload-filled /></el-icon>
      <div class="el-upload__text">将文件拖到此处或<em>点击选择</em></div>
    </el-upload>
    <!-- 上传进度 -->
    <div v-if="file" class="progress-box">
      <div class="file-info">
        {{ file.name }} ({{ formatSize(file.size) }})
      </div>
      <el-progress :percentage="totalProgress" :status="uploadStatus" :stroke-width="16" />
      <div class="action-buttons">
        <el-button type="primary" @click="startUpload" :disabled="isUploading || isMerging">
          {{ isUploading ? '上传中...' : '开始上传' }}
        </el-button>
        <el-button @click="pauseUpload" :disabled="!isUploading">
          暂停
        </el-button>
      </div>
    </div>
  </div>
</template>

三、后端实现

以下是一个基于 Node.js (Express) 实现大文件断点续传后端的完整方案,与前端的 Vue + Element UI 示例完美配合:

  • 分片上传的接口:接收前端传来的每个分片,存储到临时目录。
  • 检查分片接口:告诉前端哪些分片已经上传,避免重复上传。
  • 合并分片接口:当所有分片上传完成后,合并成完整的文件。
  • 文件校验:确保文件在传输过程中没有损坏,比如使用哈希校验。
  • 临时文件管理:上传过程中保存分片,合并后清理临时文件。

1、项目结构

├── server.js          # 主入口文件
├── uploads            # 上传文件存储目录
│   ├── temp           # 分片临时存储目录
│   └── merged         # 合并后的文件目录
└── package.json

2、依赖安装

npm install express multer cors fs-extra

3、服务端代码

// server.js

const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const cors = require('cors');
const fse = require('fs-extra');

const app = express();
app.use(cors());
app.use(express.json());

// 文件存储目录
const UPLOAD_DIR = path.resolve(__dirname, 'uploads');
const TEMP_DIR = path.join(UPLOAD_DIR, 'temp');    // 分片临时目录
const MERGED_DIR = path.join(UPLOAD_DIR, 'merged');// 合并文件目录

// 确保目录存在
fse.ensureDirSync(TEMP_DIR);
fse.ensureDirSync(MERGED_DIR);

// 分片上传中间件
const chunkUpload = multer({ dest: TEMP_DIR });

// ================== 接口实现 start ================== //

// 1. 检查分片状态
app.get('/api/check', (req, res) => {
  const { fileHash } = req.query;
  const chunkDir = path.resolve(TEMP_DIR, fileHash);

  // 已上传的分片索引列表
  let uploadedChunks = [];
  if (fse.existsSync(chunkDir)) {
    uploadedChunks = fse.readdirSync(chunkDir).map(name => parseInt(name));
  }

  res.json({
    code: 0,
    data: {
      uploadedIndexes: uploadedChunks
    }
  });
});

// 2. 上传分片
app.post('/api/upload', chunkUpload.single('chunk'), async (req, res) => {
  const { hash, index } = req.body;
  const chunkPath = req.file.path;
  const chunkDir = path.resolve(TEMP_DIR, hash);

  try {
    // 创建哈希目录
    await fse.ensureDir(chunkDir);
    
    // 移动分片到目标目录(使用索引命名)
    const destPath = path.join(chunkDir, index);
    await fse.move(chunkPath, destPath);

    res.json({ code: 0, message: '分片上传成功' });
  } catch (err) {
    res.status(500).json({ code: 1, message: '分片上传失败' });
  }
});

// 3. 合并文件
app.post('/api/merge', async (req, res) => {
  const { fileName, fileHash, chunkCount } = req.body;
  const chunkDir = path.join(TEMP_DIR, fileHash);
  const filePath = path.join(MERGED_DIR, fileName);

  try {
    // 检查分片是否完整
    const chunkPaths = await fse.readdir(chunkDir);
    if (chunkPaths.length !== chunkCount) {
      return res.status(400).json({ code: 1, message: '分片数量不匹配' });
    }

    // 创建写入流
    const writeStream = fs.createWriteStream(filePath);
    
    // 按索引顺序合并
    for (let i = 0; i < chunkCount; i++) {
      const chunkPath = path.join(chunkDir, i.toString());
      const buffer = await fse.readFile(chunkPath);
      writeStream.write(buffer);
    }

    writeStream.end();
    
    // 清理临时目录
    await fse.remove(chunkDir);

    res.json({ code: 0, message: '文件合并成功', data: { path: filePath } });
  } catch (err) {
    res.status(500).json({ code: 1, message: '文件合并失败' });
  }
});

// ================== 接口实现 end ================== //


// 定时清理临时目录(可选)
setInterval(() => {
  fse.emptyDirSync(TEMP_DIR);
}, 1000 * 60 * 60); // 每小时清理一次


// 启动服务
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

3、后端接口

接口地址方法参数功能
/api/checkGETfileHash查询已上传分片索引
/api/uploadPOSTchunk, hash, index上传分片
/api/mergePOSTfileName, fileHash, chunkCount合并文件

四、实现效果

当用户选择文件时,计算文件的哈希值作为唯一标识,并检查哪些分片已经上传。返回已上传的片段。

文件上传,并发数为3,实现分段上传。

文件中途失败后,会重试并再次上传。

如果用户关闭了浏览器或刷新了页面,当用户再次选择文件时,通过计算哈希值并与服务器上的记录进行比较,以确定哪些分片已经上传。检查服务器上的上传状态来恢复上传。

文件上传成功,并完成文件合并

五、代码上传

详细的全部代码已上传,可以去顶部下载。

1、安装前端依赖

切换到前端目录web

npm i

2、启动前端

npm run dev

3、安装服务端依赖

切换到服务端目录node

npm i

4、启动服务端

npm run serve

通过此示例,可以快速实现一个带友好交互的大文件断点续传功能,结合 Element Plus 的 UI 组件保证用户体验一致性。

以上就是基于Node.js实现大文件断点续传的完整方案的详细内容,更多关于Node.js大文件断点续传的资料请关注脚本之家其它相关文章!

相关文章

  • 2019最新21个MySQL高频面试题介绍

    2019最新21个MySQL高频面试题介绍

    又到了一年的面试季,今年情况特殊,很多人可能都窝在家里,也有一些人准备找工作,但是疫情严重,也没企业发招聘信息。这个时候,最好的做法就是在家里刷面试题
    2020-02-02
  • node.js中npm包管理工具用法分析

    node.js中npm包管理工具用法分析

    这篇文章主要介绍了node.js中npm包管理工具用法,结合实例形式分析了node.js中npm包管理工具查看、安装、更新、卸载等相关操作技巧与注意事项,需要的朋友可以参考下
    2020-02-02
  • 浅谈Koa服务限流方法实践

    浅谈Koa服务限流方法实践

    本篇文章主要介绍了浅谈Koa服务限流方法实践,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • adm-zip-0.4.13-中文文档详解

    adm-zip-0.4.13-中文文档详解

    这篇文章主要介绍了adm-zip-0.4.13-中文文档,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05
  • nodejs中实现sleep功能实例

    nodejs中实现sleep功能实例

    这篇文章主要介绍了nodejs中实现sleep功能实例,本文讲解了sleep功能的开发过程和使用效果及性能测试,需要的朋友可以参考下
    2015-03-03
  • nodejs简单实现TCP服务器端和客户端的聊天功能示例

    nodejs简单实现TCP服务器端和客户端的聊天功能示例

    这篇文章主要介绍了nodejs简单实现TCP服务器端和客户端的聊天功能,结合实例形式分析了nodejs基于TCP协议实现的聊天程序客户端与服务器端具体步骤与相关操作技巧,代码备有较为详尽的注释便于理解,需要的朋友可以参考下
    2018-01-01
  • Node.js读取本地CSV文件并且写入为JSON格式文件过程

    Node.js读取本地CSV文件并且写入为JSON格式文件过程

    本文介绍了使用Node.js v14.18.1读取CSV文件并处理中文乱码的方法,包括使用`fs.readdirSync`读取文件夹、`TextDecoder`处理编码、以及如何解析和处理CSV数据
    2026-01-01
  • NodeJS和浏览器中this关键字的不同之处

    NodeJS和浏览器中this关键字的不同之处

    这篇文章主要给大家介绍了关于NodeJS和浏览器中this关键字不同的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • Nodejs中session的简单使用及通过session实现身份验证的方法

    Nodejs中session的简单使用及通过session实现身份验证的方法

    session的本质使用cookie来实现。本文给大家介绍Nodejs中session的简单使用及通过session实现身份验证的方法,对node.js session相关知识感兴趣的朋友一起学习吧
    2016-02-02
  • nodejs子进程child_process和cluster模块深入解析

    nodejs子进程child_process和cluster模块深入解析

    本文从node的单线程单进程的理解触发,介绍了child_process模块和cluster模块,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09

最新评论