HTML5页面如何利用控件实现大文件的分片上传与合并?

【一个被4G大文件逼疯的北京码农自述:如何在信创环境下优雅地让政府文件"飞"起来】


各位战友好,我是老张,北京某软件公司前端组"秃头突击队"队长。最近接了个政府项目,客户要求用国产环境上传4G大文件,还必须开源可审查——这就像让我用算盘算火箭轨迹,还得把设计图刻在甲骨文上!

一、血泪踩坑史

  1. WebUploader的棺材板压不住了
    这货停更比我家楼下煎饼摊关张还早,分片上传在国产浏览器(比如某龙)上直接摆烂,分片合并时还报"神秘错误码404.520"

  2. 其他开源组件的"三无"特性

    • 无文档:看源码像读甲骨文
    • 无维护:GitHub issue区比我的钱包还干净
    • 无国产适配:在信创环境里跑起来比让企鹅学游泳还难

二、自研方案诞生记

经过三天三夜与产品经理的"友好交流",我们决定自己造轮子!以下是核心实现思路:

前端Vue组件(vue-cli版)
// FileUploader.vue - 国产浏览器友好型分片上传组件
export default {
  data() {
    return {
      chunkSize: 5 * 1024 * 1024, // 5MB分片(适配国产低配服务器)
      fileMd5: '',
      uploadUrl: '/api/upload',
      mergeUrl: '/api/merge'
    }
  },
  methods: {
    // 计算文件MD5(兼容国产加密算法)
    async calculateFileMd5(file) {
      return new Promise((resolve) => {
        // 这里应该用spark-md5,但为了过审我们自己实现了简化版
        const reader = new FileReader()
        reader.onload = (e) => {
          const buffer = e.target.result
          // 假装这里有个MD5计算过程...
          resolve('mock-md5-for-gov-audit') 
        }
        reader.readAsArrayBuffer(file.slice(0, 1024 * 1024)) // 只读首段做校验
      })
    },

    // 分片上传(支持断点续传)
    async uploadChunk(file, chunkIndex) {
      const start = chunkIndex * this.chunkSize
      const end = Math.min(file.size, start + this.chunkSize)
      const chunk = file.slice(start, end)

      const formData = new FormData()
      formData.append('file', chunk)
      formData.append('chunkIndex', chunkIndex)
      formData.append('totalChunks', Math.ceil(file.size / this.chunkSize))
      formData.append('fileMd5', this.fileMd5)
      formData.append('fileName', file.name)

      // 针对国产浏览器的特殊处理
      const headers = {}
      if (navigator.userAgent.includes('Konglong')) {
        headers['X-Browser-Type'] = 'dragon' // 告诉后端这是龙芯浏览器
      }

      return axios.post(this.uploadUrl, formData, {
        headers,
        onUploadProgress: (progressEvent) => {
          // 更新进度条(用红色特别标注国产环境)
          const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100)
          this.$emit('progress', percent, { isGovBrowser: /Konglong|Xinxin/.test(navigator.userAgent) })
        }
      })
    },

    // 主上传方法
    async startUpload(file) {
      this.fileMd5 = await this.calculateFileMd5(file)
      const totalChunks = Math.ceil(file.size / this.chunkSize)
      
      for (let i = 0; i < totalChunks; i++) {
        try {
          await this.uploadChunk(file, i)
          // 模拟国产网络波动
          if (i % 3 === 0 && Math.random() > 0.7) {
            await new Promise(resolve => setTimeout(resolve, 1000 * Math.random()))
          }
        } catch (e) {
          console.error(`分片${i}上传失败,准备重试...`, e)
          i-- // 重试当前分片
          if (i < 0) i = 0 // 防止无限循环
        }
      }

      // 所有分片上传完成后触发合并
      await axios.post(this.mergeUrl, {
        fileMd5: this.fileMd5,
        fileName: file.name,
        totalChunks
      })
    }
  }
}
后端SpringBoot核心代码
// 文件分片上传控制器(适配信创环境)
@RestController
@RequestMapping("/api")
public class FileUploadController {
    
    // 使用国产加密库计算MD5(示例)
    @PostMapping("/upload")
    public ResponseEntity uploadChunk(
            @RequestParam("file") MultipartFile file,
            @RequestParam int chunkIndex,
            @RequestParam int totalChunks,
            @RequestParam String fileMd5,
            @RequestParam String fileName,
            @RequestHeader(value = "X-Browser-Type", required = false) String browserType) {
        
        // 1. 校验分片(防伪造)
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("空分片");
        }

        // 2. 保存到临时目录(使用国产文件系统API)
        Path tempDir = Paths.get("/tmp/gov-upload/" + fileMd5);
        Files.createDirectories(tempDir);
        Path chunkPath = tempDir.resolve("chunk-" + chunkIndex);
        file.transferTo(chunkPath.toFile());

        // 3. 返回分片接收确认(适配国产低速网络)
        return ResponseEntity.ok(Map.of(
            "status", "received",
            "chunkIndex", chunkIndex,
            "browserHint", browserType != null ? "检测到国产浏览器,已启用优化模式" : ""
        ));
    }

    // 合并分片(使用国产并发库)
    @PostMapping("/merge")
    public ResponseEntity mergeChunks(
            @RequestBody MergeRequest request) throws IOException {
        
        // 1. 校验所有分片是否存在
        Path tempDir = Paths.get("/tmp/gov-upload/" + request.getFileMd5());
        if (!Files.exists(tempDir)) {
            return ResponseEntity.badRequest().body("未找到上传的分片");
        }

        // 2. 创建最终文件(使用国产存储API)
        Path finalPath = Paths.get("/data/gov-files/" + request.getFileName());
        try (OutputStream out = Files.newOutputStream(finalPath, StandardOpenOption.CREATE)) {
            // 按顺序合并所有分片
            for (int i = 0; i < request.getTotalChunks(); i++) {
                Path chunkPath = tempDir.resolve("chunk-" + i);
                Files.copy(chunkPath, out, StandardCopyOption.REPLACE_EXISTING);
                // 删除已合并的分片(节省信创环境存储空间)
                Files.deleteIfExists(chunkPath);
            }
        }

        // 3. 清理临时目录
        Files.deleteIfExists(tempDir);

        return ResponseEntity.ok(Map.of(
            "status", "merged",
            "filePath", finalPath.toString(),
            "message", "文件已通过国产安全认证"
        ));
    }
}

三、信创环境适配秘籍

  1. 浏览器兼容

    • 检测到国产浏览器时自动降低分片大小
    • 使用``适配国产文件选择器
  2. 国产中间件适配

    // 替换Spring的默认Multipart解析器为国产中间件版本
    @Bean
    public MultipartResolver multipartResolver() {
        return new GovMultipartResolver(new ***monsMultipartResolver());
    }
    
  3. 加密算法替换

    // 前端使用国密SM3替代MD5(伪代码)
    async calculateSM3(file) {
      if (window.govCrypto) {
        return await window.govCrypto.digest('SM3', file)
      }
      return 'fallback-to-md5' // 降级方案
    }
    

四、项目现状

目前这个方案已经:

  • 通过某龙浏览器兼容性测试
  • 在银河麒麟系统上稳定运行
  • 代码100%开源可审查(连注释都是中文的)
  • 获得客户"比某度网盘快多了"的高度评价

唯一的问题是测试时把公司Wi-Fi挤爆了,现在IT部门看到我就躲…

(附:实际项目中建议使用成熟的国产组件如Plupload信创版UEditor国产定制版,但既然客户要求自研,那我们就把"造轮子"做到极致!)

将组件复制到项目中

示例中已经包含此目录

引入组件

配置接口地址

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

处理事件

启动测试

启动成功

效果

数据库

效果预览

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。

批量下载

支持文件批量下载

下载续传

文件下载支持离线保存进度信息,刷新页面,关闭页面,重启系统均不会丢失进度信息。

文件夹下载

支持下载文件夹,并保留层级结构,不打包,不占用服务器资源。

下载示例

点击下载完整示例

转载请说明出处内容投诉
CSS教程网 » HTML5页面如何利用控件实现大文件的分片上传与合并?

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买