Vue2项目中如何实现大文件的断点续传?

武汉光谷XX软件公司大文件传输组件选型与自研方案

一、项目背景与需求分析

作为武汉光谷地区专注于软件研发的高新技术企业,我司长期服务于政府和企业客户,在政务信息化、企业数字化转型等领域积累了丰富的经验。当前,我司核心产品面临大文件传输(特别是GB级以上文件)的技术升级需求,主要痛点如下:

  1. 功能需求

    • 支持断点续传、分片上传/下载
    • 支持文件校验(MD5/SHA256)
    • 支持传输进度实时反馈
    • 支持并发传输控制
  2. 兼容性要求

    • 主流浏览器(Chrome/Firefox/Edge/国产浏览器)
    • 信创国产化环境(麒麟/统信UOS+龙芯/飞腾/鲲鹏)
  3. 技术架构约束

    • 后端:Spring Boot (Java 11+)
    • 前端:Vue CLI 3.x + Element UI
    • 需提供完整源代码
  4. 特殊需求

    • 自主可控(避免开源组件停更风险)
    • 企业级技术支持
    • 符合等保2.0安全要求

二、现有方案评估

2.1 已评估开源方案

组件名称优点缺点
WebUploader成熟度高已停更(最后更新2018年),不支持信创环境
Uppy插件化设计社区支持弱,国产浏览器兼容性差
Plupload多浏览器支持文档陈旧,大文件分片实现效率低
Resumable.js轻量级仅支持前端,无完整后端实现

2.2 核心问题

  1. 信创环境兼容性:现有开源组件均未针对国产CPU架构和操作系统进行优化
  2. 技术断层风险:依赖的Flash技术在信创环境中被完全禁用
  3. 安全合规性:开源组件缺乏等保2.0要求的传输加密和审计日志功能

三、自研组件技术方案

3.1 架构设计

┌───────────────┐    ┌───────────────┐    ┌───────────────┐
│   Vue前端      │    │   Nginx        │    │   Java后端    │
│  (分片组件)    │←──→│ (静态资源+代理)│←──→│  (传输服务)    │
└───────────────┘    └───────────────┘    └───────────────┘
       ↑                     ↑                     ↑
    浏览器API              WebSocket             Spring WebFlux
    (File/Blob API)       (进度通知)           (Reactor非阻塞IO)

3.2 核心代码实现

前端实现(Vue组件)
// FileUploader.vue



export default {
  data() {
    return {
      file: null,
      chunkSize: 5 * 1024 * 1024, // 5MB分片
      isUploading: false,
      progress: 0,
      fileId: ''
    }
  },
  methods: {
    handleFileChange(e) {
      this.file = e.target.files[0]
    },
    async calculateFileHash(file) {
      // 使用Web Worker计算文件MD5(避免主线程阻塞)
      return new Promise(resolve => {
        const worker = new Worker('/js/hash.worker.js')
        worker.postMessage({ file })
        worker.onmessage = e => resolve(e.data.hash)
      })
    },
    async startUpload() {
      if (!this.file) return
      
      this.isUploading = true
      const fileHash = await this.calculateFileHash(this.file)
      
      // 1. 检查文件是否已存在(秒传功能)
      const { data } = await this.$http.post('/api/file/check', {
        fileName: this.file.name,
        fileSize: this.file.size,
        fileHash
      })
      
      if (data.exists) {
        this.$message.success('文件已存在,秒传完成')
        this.progress = 100
        return
      }
      
      this.fileId = data.fileId || Date.now()
      const chunkCount = Math.ceil(this.file.size / this.chunkSize)
      
      // 2. 分片上传
      for (let i = 0; i < chunkCount; i++) {
        const start = i * this.chunkSize
        const end = Math.min(start + this.chunkSize, this.file.size)
        const chunk = this.file.slice(start, end)
        
        const formData = new FormData()
        formData.append('file', chunk)
        formData.append('fileId', this.fileId)
        formData.append('chunkIndex', i)
        formData.append('totalChunks', chunkCount)
        formData.append('fileHash', fileHash)
        
        await this.$http.post('/api/file/upload-chunk', formData, {
          onUploadProgress: progressEvent => {
            const loaded = progressEvent.loaded + i * this.chunkSize
            this.progress = Math.min(100, Math.round((loaded / this.file.size) * 100))
          }
        })
      }
      
      // 3. 合并分片
      await this.$http.post('/api/file/merge', {
        fileId: this.fileId,
        fileName: this.file.name,
        fileHash,
        totalChunks: chunkCount
      })
      
      this.$message.success('上传完成')
      this.isUploading = false
    }
  }
}

后端实现(Java Spring Boot)
// FileTransferController.java
@RestController
@RequestMapping("/api/file")
public class FileTransferController {
    
    @Autowired
    private FileStorageService storageService;
    
    @Autowired
    private FileMetadataRepository metadataRepository;
    
    // 分片上传接口
    @PostMapping("/upload-chunk")
    public ResponseEntity uploadChunk(
            @RequestParam("file") MultipartFile file,
            @RequestParam String fileId,
            @RequestParam int chunkIndex,
            @RequestParam int totalChunks,
            @RequestParam String fileHash) {
        
        try {
            // 1. 验证分片
            if (file.isEmpty()) {
                return ResponseEntity.badRequest().body("分片内容不能为空");
            }
            
            // 2. 保存分片(使用临时目录)
            Path tempDir = Paths.get("/tmp/uploads/" + fileId);
            Files.createDirectories(tempDir);
            Path chunkPath = tempDir.resolve("chunk-" + chunkIndex);
            file.transferTo(chunkPath.toFile());
            
            // 3. 记录分片信息(可选)
            // ...
            
            return ResponseEntity.ok("分片上传成功");
        } catch (IOException e) {
            return ResponseEntity.internalServerError().body("上传失败: " + e.getMessage());
        }
    }
    
    // 合并分片接口
    @PostMapping("/merge")
    public ResponseEntity mergeChunks(
            @RequestBody MergeRequest request) {
        
        try {
            // 1. 验证文件完整性
            FileMetadata metadata = metadataRepository.findByFileHash(request.getFileHash())
                .orElseGet(() -> {
                    FileMetadata newMeta = new FileMetadata();
                    newMeta.setFileHash(request.getFileHash());
                    newMeta.setFileName(request.getFileName());
                    newMeta.setFileSize(calculateTotalSize(request.getFileId(), request.getTotalChunks()));
                    return metadataRepository.save(newMeta);
                });
            
            // 2. 合并分片(使用NIO高效合并)
            Path tempDir = Paths.get("/tmp/uploads/" + request.getFileId());
            Path outputPath = Paths.get("/storage/" + metadata.getStoragePath());
            
            try (SeekableByteChannel channel = Files.newByteChannel(
                    outputPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
                
                for (int i = 0; i < request.getTotalChunks(); i++) {
                    Path chunkPath = tempDir.resolve("chunk-" + i);
                    try (InputStream is = Files.newInputStream(chunkPath)) {
                        byte[] buffer = new byte[8192];
                        int bytesRead;
                        while ((bytesRead = is.read(buffer)) != -1) {
                            channel.write(ByteBuffer.wrap(buffer, 0, bytesRead));
                        }
                    }
                    Files.deleteIfExists(chunkPath); // 清理分片
                }
            }
            
            Files.deleteIfExists(tempDir); // 清理临时目录
            
            return ResponseEntity.ok("文件合并成功");
        } catch (IOException e) {
            return ResponseEntity.internalServerError().body("合并失败: " + e.getMessage());
        }
    }
    
    // 其他辅助方法...
}

3.3 信创环境适配方案

  1. 国产CPU优化

    • 使用JNI调用龙芯/飞腾的加密指令集加速哈希计算
    • 针对鲲鹏处理器优化NIO文件操作
  2. 国产操作系统适配

    // 检测操作系统类型并应用特定配置
    public class OSAdapter {
        public static boolean isLinux国产化() {
            String os = System.getProperty("os.name").toLowerCase();
            return os.contains("linux") && 
                  (os.contains("kylin") || os.contains("uos") || os.contains("deepin"));
        }
        
        public static FileStorageStrategy getStorageStrategy() {
            if (isLinux国产化()) {
                return new KylinFileStorageStrategy(); // 使用国产文件系统优化
            }
            return new DefaultFileStorageStrategy();
        }
    }
    
  3. 数据库适配

    • 默认使用PostgreSQL(支持信创环境)
    • 提供达梦/人大金仓数据库的方言适配

四、实施路线图

  1. 第一阶段(1个月)

    • 完成核心传输功能开发
    • 实现基础分片上传/下载
    • 完成Chrome/Firefox/Edge兼容性测试
  2. 第二阶段(2周)

    • 信创环境适配
    • 完成麒麟/统信UOS+龙芯/飞腾环境测试
    • 实现等保2.0安全要求
  3. 第三阶段(1周)

    • 性能优化与压力测试
    • 编写完整技术文档
    • 内部培训与知识转移

五、预期收益

  1. 技术自主性:完全掌握核心技术,避免开源组件停更风险
  2. 安全可控:符合等保2.0要求,通过国产操作系统认证
  3. 性能提升:预计传输效率比现有方案提升30%以上
  4. 维护成本降低:减少对外部开源社区的依赖

我司已组建专项技术团队推进此项目,预计在3个月内完成全部开发测试工作。该方案既能满足当前项目需求,又可作为独立产品进行商业化推广,具有显著的战略价值。

将组件复制到项目中

示例中已经包含此目录
image

引入组件

image

配置接口地址

接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表
参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de
image

处理事件

image

启动测试

image

启动成功

image

效果

image

数据库

image

下载示例

点击下载完整示例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值