HTML5结合Vue3实现百万文件分块上传的思路是什么?

大文件上传方案探索:从WebUploader到自定义分片上传的实践

作为一名前端开发工程师,最近遇到了一个颇具挑战性的需求:需要在Vue项目中实现4GB左右大文件的稳定上传,且要兼容Chrome、Firefox、Edge等主流浏览器,后端使用PHP接收。此前我们采用了百度开源的WebUploader组件,但在实际使用中遇到了几个难以解决的问题:

  1. 分片上传过程中偶尔会出现断点续传失效的情况
  2. 对新版浏览器的兼容性不够理想
  3. 缺乏官方技术支持,社区活跃度下降
  4. 自定义UI的灵活性不足

方案选型思考

经过技术调研,我评估了以下几个主流方案:

  1. Plupload:功能全面但文档不够友好,对Vue集成支持一般
  2. Uppy:现代感强但体积较大,学习曲线较陡
  3. Resumable.js:专注分片上传但UI较为基础
  4. 自定义实现:基于XMLHttpRequest/Fetch API实现核心分片逻辑

最终决定采用自定义分片上传方案,主要基于以下考虑:

  • 完全控制上传流程
  • 可以针对业务需求深度优化
  • 减少第三方依赖,降低维护成本
  • 与Vue生态无缝集成

核心实现思路

1. 前端分片策略

javascript 复制代码
// 文件分片工具函数
const chunkFile = (file, chunkSize = 5 * 1024 * 1024) => {
  const chunks = []
  let current = 0
  while (current < file.size) {
    chunks.push({
      file: file.slice(current, current + chunkSize),
      chunkIndex: chunks.length,
      totalChunks: Math.ceil(file.size / chunkSize),
      fileName: file.name,
      fileSize: file.size,
      fileType: file.type,
      fileLastModified: file.lastModified,
      identifier: generateFileIdentifier(file) // 生成唯一标识用于断点续传
    })
    current += chunkSize
  }
  return chunks
}

// 生成文件唯一标识(基于文件内容)
const generateFileIdentifier = (file) => {
  return new Promise((resolve) => {
    const reader = new FileReader()
    reader.onload = (e) => {
      const arr = new Uint8Array(e.target.result)
      const hashArray = Array.from(arr).map(b => b.toString(16).padStart(2, '0'))
      resolve(hashArray.join('').substring(0, 16))
    }
    reader.readAsArrayBuffer(file.slice(0, 1024 * 1024)) // 取前1MB计算哈希
  })
}

2. Vue组件实现

vue 复制代码
export default {
  data() {
    return {
      file: null,
      chunks: [],
      uploadStatus: 'idle', // idle, uploading, paused, completed, error
      progress: 0,
      error: null,
      currentChunk: 0,
      abortController: null
    }
  },
  methods: {
    async handleFileChange(e) {
      this.file = e.target.files[0]
      if (!this.file) return
      
      // 生成文件标识(简化版,实际项目应使用更可靠的算法)
      const identifier = await this.generateSimpleIdentifier(this.file)
      
      // 检查服务器是否有未完成的上传记录
      const res = await this.checkUploadStatus(identifier)
      if (res.exists) {
        if (confirm('检测到未完成的上传,是否继续?')) {
          this.currentChunk = res.uploadedChunks
        } else {
          // 清除服务器记录(实际项目应实现)
        }
      }
      
      this.chunks = this.chunkFile(this.file)
      this.progress = Math.round((this.currentChunk / this.chunks.length) * 100)
    },
    
    async startUpload() {
      if (!this.file) return
      
      this.uploadStatus = 'uploading'
      this.error = null
      this.abortController = new AbortController()
      
      try {
        for (let i = this.currentChunk; i < this.chunks.length; i++) {
          if (this.uploadStatus !== 'uploading') break // 处理暂停情况
          
          const chunk = this.chunks[i]
          const formData = new FormData()
          formData.append('file', chunk.file)
          formData.append('chunkIndex', chunk.chunkIndex)
          formData.append('totalChunks', chunk.totalChunks)
          formData.append('fileName', chunk.fileName)
          formData.append('fileSize', chunk.fileSize)
          formData.append('fileType', chunk.fileType)
          formData.append('identifier', chunk.identifier)
          
          await this.uploadChunk(formData)
          
          this.currentChunk = i + 1
          this.progress = Math.round(((i + 1) / this.chunks.length) * 100)
        }
        
        if (this.uploadStatus === 'uploading') {
          await this.mergeChunks(this.chunks[0].identifier, this.chunks[0].fileName)
          this.uploadStatus = 'completed'
          this.$emit('upload-complete')
        }
      } catch (err) {
        console.error('上传失败:', err)
        this.error = err.message || '上传过程中出现错误'
        this.uploadStatus = 'error'
      }
    },
    
    async uploadChunk(formData) {
      return fetch('/api/upload-chunk.php', {
        method: 'POST',
        body: formData,
        signal: this.abortController.signal
      })
    },
    
    async mergeChunks(identifier, fileName) {
      return fetch('/api/merge-chunks.php', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ identifier, fileName })
      })
    },
    
    // 简化版标识生成(实际项目应使用更可靠的算法)
    generateSimpleIdentifier(file) {
      return `${file.name}-${file.size}-${file.lastModified}`
    },
    
    async checkUploadStatus(identifier) {
      // 实际项目应实现与后端的交互
      return { exists: false, uploadedChunks: 0 }
    },
    
    pauseUpload() {
      if (this.uploadStatus === 'uploading') {
        this.abortController.abort()
        this.uploadStatus = 'paused'
      }
    },
    
    resumeUpload() {
      if (this.uploadStatus === 'paused') {
        this.startUpload()
      }
    }
  }
}

3. PHP后端实现要点

php 复制代码
 $chunkIndex,
            'totalChunks' => $totalChunks,
            'fileName' => $fileName,
            'uploaded' => time()
        ]));
    
    echo json_encode(['status' => 'success']);
} else {
    http_response_code(500);
    echo json_encode(['status' => 'error', 'message' => 'Failed to save chunk']);
}

// merge-chunks.php - 合并分片
header('Content-Type: application/json');

$uploadDir = '/path/to/upload/dir/';
$tempDir = $uploadDir . 'temp/';

$data = json_decode(file_get_contents('php://input'), true);
$identifier = $data['identifier'] ?? '';
$fileName = $data['fileName'] ?? '';

// 检查标识符和文件名
if (empty($identifier) || empty($fileName)) {
    http_response_code(400);
    echo json_encode(['status' => 'error', 'message' => 'Invalid parameters']);
    exit;
}

// 检查上传状态文件
$statusFile = $tempDir . $identifier . '.upload';
if (!file_exists($statusFile)) {
    http_response_code(404);
    echo json_encode(['status' => 'error', 'message' => 'Upload not found']);
    exit;
}

$status = json_decode(file_get_contents($statusFile), true);
$totalChunks = $status['totalChunks'] ?? 0;

// 合并文件
$finalPath = $uploadDir . $fileName;
if ($fp = fopen($finalPath, 'wb')) {
    for ($i = 0; $i < $totalChunks; $i++) {
        $chunkPath = $tempDir . $identifier . '_' . $i;
        if (!file_exists($chunkPath)) {
            fclose($fp);
            unlink($finalPath); // 删除已创建的部分文件
            http_response_code(400);
            echo json_encode(['status' => 'error', 'message' => 'Missing chunk ' . $i]);
            exit;
        }
        
        $content = file_get_contents($chunkPath);
        fwrite($fp, $content);
        unlink($chunkPath); // 删除已合并的分片
    }
    fclose($fp);
    
    // 删除状态文件
    unlink($statusFile);
    
    echo json_encode(['status' => 'success', 'path' => $finalPath]);
} else {
    http_response_code(500);
    echo json_encode(['status' => 'error', 'message' => 'Failed to create final file']);
}
?>

方案优势与改进点

优势

  1. 完全可控:从分片策略到上传逻辑完全自主实现
  2. 深度优化:可以根据网络状况动态调整分片大小
  3. 良好兼容:基于标准Web API实现,兼容所有现代浏览器
  4. 断点续传:通过文件标识实现可靠的断点续传
  5. 进度可视化:精确计算上传进度

可改进方向

  1. 并发上传:当前实现是顺序上传,可优化为并发上传提高速度
  2. 文件校验:增加MD5/SHA校验确保文件完整性
  3. 更可靠的标识生成:当前简化版标识可能存在冲突风险
  4. 服务端清理:实现自动清理未完成上传的临时文件
  5. 拖拽上传:增强用户体验,支持拖放文件上传

实施建议

  1. 渐进式实现:先实现基本分片上传,再逐步添加断点续传、并发上传等功能
  2. 充分测试:在不同网络环境和浏览器下进行全面测试
  3. 监控上报:添加上传失败监控和错误上报机制
  4. 性能优化:根据实际测试结果调整分片大小和并发数

通过这种自定义实现方式,我们成功解决了WebUploader带来的各种问题,同时获得了更好的性能和更灵活的控制能力。目前该方案已在我们项目中稳定运行数月,处理了数百个4GB+文件的上传,未出现重大故障。

将组件复制到项目中

示例中已经包含此目录

引入组件

配置接口地址

接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表

参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de

处理事件

启动测试

启动成功

效果

数据库

效果预览

文件上传

文件刷新续传

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

文件夹上传

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

批量下载

支持文件批量下载

下载续传

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

文件夹下载

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

下载示例

点击下载完整示例

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax