基于Node.js实现大文件断点续传的完整方案,与前端的Vue+Element UI示例完 ...

打印 上一主题 下一主题

主题 1979|帖子 1979|积分 5937

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



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

  1. npm i
  2. nstall p-limit
复制代码
p-limit是一个用于限制并发操纵的JavaScript库,主要用于控制同时执行的异步操纵数量,以制止系统资源过分占用和性能下降。它通过维护一个哀求队列来管理并发操纵,当并发限制数未满时,新操纵会立刻执行;当达到限制数时,新操纵会被加入等候队列,直到有空闲位置再执行。
使用方法
使用p-limit非常简单,起首需要创建一个限制对象并指定并发限制数。然后将需要执行的异步操纵包装成一个函数,并通过调用限制对象的函数来执行这些操纵。当并发限制数未满时,操纵会立刻执行;如果达到限制数,操纵会被加入等候队列,按照先辈先出的次序执行

2、spark-md5

  1. npm i
  2. nstall spark-md5
复制代码
Spark-md5是一个JavaScript库,用于快速盘算文件或数据的MD5值,支持浏览器环境,可用于文件完整性校验和分片盘算。其主要功能包括calculate、append、init和end等方法,适用于大文件分片处理。由于MD5是单向哈希,故无法解密。通过比较文件的MD5值,可以判断文件是否同等。
二、实现步骤

1、文件分片

  1. // 使用 File.slice 方法分片
  2. const chunkSize = 1 * 1024 * 1024; // 1MB
  3. const chunks = [];
  4. let start = 0;
  5. while (start < file.size) {
  6.   const end = Math.min(start + chunkSize, file.size);
  7.   const chunk = file.slice(start, end);
  8.   chunks.push(chunk);
  9.   start = end;
  10. }
复制代码
2、盘算文件哈希

  1. /**
  2. * 计算文件的哈希值
  3. * @param file 文件对象
  4. * @returns Promise<string> 返回计算得到的哈希值
  5. */
  6. const calculateFileHash = async (file) => {
  7.   return new Promise((resolve) => {
  8.     const spark = new SparkMD5.ArrayBuffer();
  9.     const reader = new FileReader();
  10.     reader.readAsArrayBuffer(file);
  11.     reader.onload = (e) => {
  12.       spark.append(e.target.result);
  13.       resolve(spark.end());
  14.     };
  15.   });
  16. };
复制代码
3、查询已上传分片

  1. file.value = uploadFile.raw;
  2. fileHash.value = await calculateFileHash(file.value);
  3. const { data } = await axios.get('/api/check', {
  4.     params: { fileHash: fileHash.value }
  5. });
复制代码
4、上传分片

  1.     // 上传未完成的分片
  2.     const requests = chunks.map((chunk, index) => {
  3.       if (uploadedChunkIndexes.value.includes(index)) return;
  4.       const formData = new FormData();
  5.       formData.append('chunk', chunk);
  6.       formData.append('hash', fileHash.value);
  7.       formData.append('index', index);
  8.       return axios.post('/api/upload', formData, {
  9.         onUploadProgress: (e) => {
  10.           const percent = Math.round((e.loaded / e.total) * 100);
  11.           updateProgress(index, percent);
  12.         }
  13.       }).then(() => {
  14.         uploadedChunkIndexes.value.push(index);
  15.         updateProgress();
  16.       });
  17.     }).filter(Boolean);
  18.     await Promise.all(requests);
复制代码
5、并发控制

  1. // 引入 p-limit
  2. import pLimit from 'p-limit';
  3. // 设置最大并发数(例如 3)
  4. const CONCURRENT_LIMIT = 3;
  5. // 创建并发控制器
  6. const limit = pLimit(CONCURRENT_LIMIT);
  7.     // 生成任务列表(过滤已上传的分片)
  8.     const tasks = chunks.map((chunk, index) => {
  9.       if (uploadedChunkIndexes.value.includes(index)) {
  10.         return Promise.resolve(); // 跳过已上传
  11.       }
  12.       // 将每个任务包裹在并发控制器中
  13.       return limit(() => {
  14.         const formData = new FormData();
  15.         formData.append('chunk', chunk);
  16.         formData.append('hash', fileHash.value);
  17.         formData.append('index', index);
  18.         return axios.post('/api/upload', formData, {
  19.           onUploadProgress: (e) => {
  20.           // 更新进度条(需结合 Vue 响应式状态)
  21.           }
  22.         }).then(() => {
  23.           // 更新已上传的分片索引列表
  24.           uploadedChunkIndexes.value.push(index);
  25.         });
  26.       });
  27.     });
  28. // 等待所有任务完成
  29. await Promise.all(tasks);
复制代码
6、通知合并文件

  1. await axios.post('/api/merge', {
  2.       fileName: file.value.name,
  3.       fileHash: fileHash.value,
  4.       chunkCount: chunks.length
  5. });
复制代码
5、错误重试

单个分片上传失败时自动重试(如 3 次)。
  1. return limit(() => {
  2.   const MAX_RETRY = 3;
  3.   let retryCount = 0;
  4.   const attemptUpload = () => {
  5.     return axios.post('/api/upload').catch((error) => {
  6.       if (retryCount < MAX_RETRY) {
  7.         retryCount++;
  8.         return attemptUpload();
  9.       }
  10.       throw error;
  11.     });
  12.   };
  13.   return attemptUpload();
  14. });
复制代码
前端所有代码
7、前端页面

  1. <template>
  2.   <div class="upload-container">
  3.     <!-- 文件选择 -->
  4.     <el-upload class="upload-demo" drag :auto-upload="false" :on-change="handleFileChange" :show-file-list="false">
  5.       <el-icon class="el-icon--upload"><upload-filled /></el-icon>
  6.       <div class="el-upload__text">将文件拖到此处或<em>点击选择</em></div>
  7.     </el-upload>
  8.     <!-- 上传进度 -->
  9.     <div v-if="file" class="progress-box">
  10.       <div class="file-info">
  11.         {{ file.name }} ({{ formatSize(file.size) }})
  12.       </div>
  13.       <el-progress :percentage="totalProgress" :status="uploadStatus" :stroke-width="16" />
  14.       <div class="action-buttons">
  15.         <el-button type="primary" @click="startUpload" :disabled="isUploading || isMerging">
  16.           {{ isUploading ? '上传中...' : '开始上传' }}
  17.         </el-button>
  18.         <el-button @click="pauseUpload" :disabled="!isUploading">
  19.           暂停
  20.         </el-button>
  21.       </div>
  22.     </div>
  23.   </div>
  24. </template>
复制代码
三、后端实现

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


  • 分片上传的接口:接收前端传来的每个分片,存储到暂时目次。
  • 检查分片接口:告诉前端哪些分片已经上传,制止重复上传。
  • 合并分片接口:当所有分片上传完成后,合并成完整的文件。
  • 文件校验:确保文件在传输过程中没有粉碎,好比使用哈希校验。
  • 暂时文件管理:上传过程中保存分片,合并后清理暂时文件。
1、项目结构

  1. ├── server.js          # 主入口文件
  2. ├── uploads            # 上传文件存储目录
  3. │   ├── temp           # 分片临时存储目录
  4. │   └── merged         # 合并后的文件目录
  5. └── package.json
复制代码
2、依赖安装

  1. npm i
  2. nstall express multer cors fs-extra
复制代码
3、服务端代码

// server.js
  1. const express = require('express');
  2. const multer = require('multer');
  3. const fs = require('fs');
  4. const path = require('path');
  5. const cors = require('cors');
  6. const fse = require('fs-extra');
  7. const app = express();
  8. app.use(cors());
  9. app.use(express.json());
  10. // 文件存储目录
  11. const UPLOAD_DIR = path.resolve(__dirname, 'uploads');
  12. const TEMP_DIR = path.join(UPLOAD_DIR, 'temp');    // 分片临时目录
  13. const MERGED_DIR = path.join(UPLOAD_DIR, 'merged');// 合并文件目录
  14. // 确保目录存在
  15. fse.ensureDirSync(TEMP_DIR);
  16. fse.ensureDirSync(MERGED_DIR);
  17. // 分片上传中间件
  18. const chunkUpload = multer({ dest: TEMP_DIR });
  19. // ================== 接口实现 start ================== //
  20. // 1. 检查分片状态
  21. app.get('/api/check', (req, res) => {
  22.   const { fileHash } = req.query;
  23.   const chunkDir = path.resolve(TEMP_DIR, fileHash);
  24.   // 已上传的分片索引列表
  25.   let uploadedChunks = [];
  26.   if (fse.existsSync(chunkDir)) {
  27.     uploadedChunks = fse.readdirSync(chunkDir).map(name => parseInt(name));
  28.   }
  29.   res.json({
  30.     code: 0,
  31.     data: {
  32.       uploadedIndexes: uploadedChunks
  33.     }
  34.   });
  35. });
  36. // 2. 上传分片
  37. app.post('/api/upload', chunkUpload.single('chunk'), async (req, res) => {
  38.   const { hash, index } = req.body;
  39.   const chunkPath = req.file.path;
  40.   const chunkDir = path.resolve(TEMP_DIR, hash);
  41.   try {
  42.     // 创建哈希目录
  43.     await fse.ensureDir(chunkDir);
  44.    
  45.     // 移动分片到目标目录(使用索引命名)
  46.     const destPath = path.join(chunkDir, index);
  47.     await fse.move(chunkPath, destPath);
  48.     res.json({ code: 0, message: '分片上传成功' });
  49.   } catch (err) {
  50.     res.status(500).json({ code: 1, message: '分片上传失败' });
  51.   }
  52. });
  53. // 3. 合并文件
  54. app.post('/api/merge', async (req, res) => {
  55.   const { fileName, fileHash, chunkCount } = req.body;
  56.   const chunkDir = path.join(TEMP_DIR, fileHash);
  57.   const filePath = path.join(MERGED_DIR, fileName);
  58.   try {
  59.     // 检查分片是否完整
  60.     const chunkPaths = await fse.readdir(chunkDir);
  61.     if (chunkPaths.length !== chunkCount) {
  62.       return res.status(400).json({ code: 1, message: '分片数量不匹配' });
  63.     }
  64.     // 创建写入流
  65.     const writeStream = fs.createWriteStream(filePath);
  66.    
  67.     // 按索引顺序合并
  68.     for (let i = 0; i < chunkCount; i++) {
  69.       const chunkPath = path.join(chunkDir, i.toString());
  70.       const buffer = await fse.readFile(chunkPath);
  71.       writeStream.write(buffer);
  72.     }
  73.     writeStream.end();
  74.    
  75.     // 清理临时目录
  76.     await fse.remove(chunkDir);
  77.     res.json({ code: 0, message: '文件合并成功', data: { path: filePath } });
  78.   } catch (err) {
  79.     res.status(500).json({ code: 1, message: '文件合并失败' });
  80.   }
  81. });
  82. // ================== 接口实现 end ================== //
  83. // 定时清理临时目录(可选)
  84. setInterval(() => {
  85.   fse.emptyDirSync(TEMP_DIR);
  86. }, 1000 * 60 * 60); // 每小时清理一次
  87. // 启动服务
  88. const PORT = 3000;
  89. app.listen(PORT, () => {
  90.   console.log(`Server running on http://localhost:${PORT}`);
  91. });
复制代码
3、后端接口

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

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

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

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

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

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

五、代码上传

具体的全部代码已上传,可以去顶部下载。
1、安装前端依赖

切换到前端目次web
  1. npm i
复制代码
2、启动前端

  1. npm run dev
复制代码
3、安装服务端依赖

切换到服务端目次node
  1. npm i
复制代码
4、启动服务端

  1. npm run serve
复制代码
通过此示例,可以快速实现一个带友爱交互的大文件断点续传功能,结合 Element Plus 的 UI 组件保证用户体验同等性。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

三尺非寒

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表