问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Vue3大文件分片上传与断点续传实战(万字长文详解)

创作时间:
作者:
@小白创作中心

Vue3大文件分片上传与断点续传实战(万字长文详解)

引用
CSDN
1.
https://blog.csdn.net/qq_16242613/article/details/145939644

在Web开发中,大文件上传是一个常见的需求场景,但同时也面临着诸多挑战,如网络波动导致的上传失败、服务器内存压力过大、重复传输浪费带宽等问题。为了解决这些问题,本文将详细介绍如何使用Vue3实现大文件分片上传与断点续传功能,包括前端组件开发、核心逻辑实现、服务端接口设计等多个方面。

一、需求背景与方案选型

1.1 大文件上传痛点

  • 网络波动导致上传失败
  • 服务器内存压力过大
  • 重复传输浪费带宽
  • 用户无法暂停/恢复上传

1.2 技术方案对比

方案
优点
缺点
普通上传
实现简单
不适合大文件
分片上传
支持断点续传
实现复杂度高
云存储直传
服务端压力小
需要云服务支持

1.3 最终技术栈

  • 前端:Vue3 + Element Plus + Axios
  • 后端:Node.js + Express
  • 核心库:SparkMD5(文件哈希)、Web Workers(分片计算)
  • 存储方案:本地临时存储(生产环境建议使用OSS)

二、实现原理详解

2.1 分片上传流程

2.2 关键技术点

  1. 文件分片策略

    • 固定大小分片(如5MB)
    • 动态分片(根据网络状况调整)
  2. 断点续传实现

  3. 并发控制

三、前端完整实现

3.1 文件选择组件

<template>
  <el-upload
    :auto-upload="false"
    :show-file-list="false"
    @change="handleFileChange"
  >
    <el-button type="primary">选择大文件</el-button>
  </el-upload>
</template>

3.2 文件分片核心逻辑

// FileUploader.ts
import SparkMD5 from 'spark-md5'

export class FileUploader {
  private file: File
  private chunkSize: number
  private chunks: Blob[]
  private hash: string

  constructor(file: File, chunkSize = 5 * 1024 * 1024) {
    this.file = file
    this.chunkSize = chunkSize
    this.chunks = []
    this.hash = ''
  }

  // 生成文件哈希(Web Worker方式)
  async calculateHash(): Promise<string> {
    return new Promise((resolve) => {
      const worker = new Worker('/hash-worker.js')
      worker.postMessage({ file: this.file })
      
      worker.onmessage = (e) => {
        this.hash = e.data
        resolve(e.data)
        worker.terminate()
      }
    })
  }

  // 文件分片
  splitFile(): void {
    let offset = 0
    while (offset < this.file.size) {
      const chunk = this.file.slice(offset, offset + this.chunkSize)
      this.chunks.push(chunk)
      offset += this.chunkSize
    }
  }

  // 获取待上传分片
  async getPendingChunks(): Promise<number[]> {
    const { data } = await axios.post('/api/check', {
      hash: this.hash,
      total: this.chunks.length
    })
    return data.pendingChunks
  }

  // 上传分片
  async uploadChunks(pendingChunks: number[]): Promise<void> {
    const requests = pendingChunks.map(async (index) => {
      const formData = new FormData()
      formData.append('file', this.chunks[index])
      formData.append('hash', this.hash)
      formData.append('index', index.toString())
      formData.append('total', this.chunks.length.toString())

      return axios.post('/api/upload', formData, {
        onUploadProgress: (progressEvent) => {
          this.updateProgress(index, progressEvent.loaded)
        }
      })
    })

    await Promise.all(requests)
  }

  // 合并请求
  async mergeFile(): Promise<void> {
    await axios.post('/api/merge', {
      hash: this.hash,
      filename: this.file.name,
      total: this.chunks.length
    })
  }
}

3.3 Web Worker计算哈希

// public/hash-worker.js
self.importScripts('https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js')

self.onmessage = async (e) => {
  const { file } = e.data
  const spark = new self.SparkMD5.ArrayBuffer()
  const reader = new FileReader()
  
  reader.readAsArrayBuffer(file)
  reader.onload = (event) => {
    spark.append(event.target.result)
    self.postMessage(spark.end())
  }
}

3.4 上传进度管理

<template>
  <div class="progress-container">
    <el-progress 
      :percentage="totalProgress" 
      :stroke-width="20"
      status="success"
    />
    <div class="action-buttons">
      <el-button @click="handlePause">暂停</el-button>
      <el-button @click="handleResume">恢复</el-button>
    </div>
  </div>
</template>

<script setup lang="ts">
const progressMap = ref<Record<number, number>>({})

const totalProgress = computed(() => {
  const loaded = Object.values(progressMap.value).reduce((a, b) => a + b, 0)
  const total = uploader?.chunks.length || 0 * uploader?.chunkSize || 0
  return Math.round((loaded / total) * 100)
})
</script>

四、服务端实现(Node.js)

4.1 接口设计

接口
方法
功能
/api/check
POST
检查上传状态
/api/upload
POST
上传分片
/api/merge
POST
合并分片

4.2 核心代码实现

// server.js
const express = require('express')
const multer = require('multer')
const fs = require('fs')
const path = require('path')

const app = express()
const UPLOAD_DIR = path.resolve(__dirname, 'uploads')

// 检查分片状态
app.post('/api/check', (req, res) => {
  const { hash, total } = req.body
  const chunkDir = path.resolve(UPLOAD_DIR, hash)
  
  if (!fs.existsSync(chunkDir)) {
    return res.json({ shouldUpload: true, pendingChunks: Array.from({ length: total }, (_, i) => i) })
  }

  const uploadedChunks = fs.readdirSync(chunkDir)
  const pendingChunks = Array.from({ length: total }, (_, i) => i)
    .filter(i => !uploadedChunks.includes(i.toString()))

  res.json({ 
    shouldUpload: pendingChunks.length > 0,
    pendingChunks
  })
})

// 上传分片
const upload = multer({ dest: 'tmp/' })
app.post('/api/upload', upload.single('file'), (req, res) => {
  const { hash, index } = req.body
  const chunkDir = path.resolve(UPLOAD_DIR, hash)
  
  if (!fs.existsSync(chunkDir)) {
    fs.mkdirSync(chunkDir, { recursive: true })
  }

  fs.renameSync(req.file.path, path.resolve(chunkDir, index))
  res.json({ success: true })
})

// 合并分片
app.post('/api/merge', async (req, res) => {
  const { hash, filename, total } = req.body
  const chunkDir = path.resolve(UPLOAD_DIR, hash)
  const filePath = path.resolve(UPLOAD_DIR, filename)
  
  // 检查分片数量
  const chunks = fs.readdirSync(chunkDir)
  if (chunks.length !== total) {
    return res.status(400).json({ error: '分片数量不匹配' })
  }

  // 创建写入流
  const writeStream = fs.createWriteStream(filePath)
  
  // 按顺序合并分片
  for (let i = 0; i < total; i++) {
    const chunkPath = path.resolve(chunkDir, i.toString())
    const buffer = fs.readFileSync(chunkPath)
    writeStream.write(buffer)
    fs.unlinkSync(chunkPath) // 删除分片
  }

  writeStream.end()
  fs.rmdirSync(chunkDir) // 删除分片目录
  
  res.json({ success: true, url: `/files/${filename}` })
})

五、高级优化策略

5.1 并发控制

// 限制并发数
async function concurrentUpload(
  tasks: (() => Promise<void>)[],
  maxConcurrent = 3
) {
  const executing = new Set()
  
  for (const task of tasks) {
    const p = task().then(() => executing.delete(p))
    executing.add(p)
    
    if (executing.size >= maxConcurrent) {
      await Promise.race(executing)
    }
  }
  
  await Promise.all(executing)
}

5.2 错误重试机制

const retryWrapper = async (
  fn: () => Promise<any>,
  retries = 3
) => {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn()
    } catch (error) {
      if (i === retries - 1) throw error
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
    }
  }
}

5.3 秒传功能实现

// 服务端检查接口优化
app.post('/api/check', (req, res) => {
  const { hash } = req.body
  const filePath = path.resolve(UPLOAD_DIR, req.body.filename)

  // 已存在完整文件
  if (fs.existsSync(filePath)) {
    return res.json({ shouldUpload: false, url: `/files/${filename}` })
  }

  // 检查分片状态...
})

六、生产环境注意事项

6.1 安全防护

  • 限制文件类型(MIME类型检查)
  • 病毒扫描(集成ClamAV等)
  • 权限验证(JWT鉴权)

6.2 性能优化

  • 使用Stream处理大文件
  • 采用Redis记录分片状态
  • 部署到CDN加速下载

6.3 存储方案选型

方案
适用场景
本地存储
小型项目快速验证
云存储OSS
生产环境推荐方案
分布式文件系统
超大规模文件存储

七、完整项目结构

/vue-frontend
  ├─src
  │  ├─components
  │  │  └─FileUpload.vue   # 上传组件
  │  ├─utils
  │  │  ├─FileUploader.ts  # 上传核心类
  │  │  └─concurrency.ts   # 并发控制
  │  └─api
  │     └─upload.ts        # 接口封装

/node-server
  ├─src
  │  ├─middleware          # 鉴权中间件
  │  ├─routes              # 接口路由
  │  └─utils               # 文件处理工具
  └─uploads                # 文件存储目录

八、效果演示

  1. 选择5GB视频文件上传
  2. 模拟网络中断后恢复上传
  3. 查看实时上传进度
  4. 验证文件完整性(MD5对比)

九、常见问题解答

Q1:分片大小如何选择?

Q2:大文件哈希计算卡顿?

  • 使用Web Worker后台计算
  • 抽样计算(取文件头尾+中间部分)

Q3:如何实现暂停功能?

const controller = new AbortController()

axios.post('/api/upload', formData, {
  signal: controller.signal
})

// 暂停时调用
controller.abort()

十、总结与展望

10.1 实现成果

  • 完整大文件分片上传方案
  • 断点续传与秒传功能
  • 生产级错误处理机制
  • 可扩展的架构设计

10.2 后续优化方向

  • 分片大小动态调整
  • 上传速度智能限制
  • 云存储服务集成
  • 分布式文件存储
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号