nuxt3 + vue3 分片上传组件全解析(大文件分片上传)

本文将详细介绍一个基于 Vue.js 的分片上传组件的设计与实现,该组件支持大文件分片上传进度显示等功能。

组件概述

这个上传组件主要包含以下功能:

  1. 支持大文件分片上传(默认5MB一个分片)
  2. 支持文件哈希计算,用于文件唯一标识
  3. 显示上传进度(整体和单个文件)
  4. 支持自定义UI样式
  5. 提供完整的文件管理功能(添加、删除)
  6. 后端支持分片合并和临时存储

组件结构

组件由三个主要文件组成:

  1. Uploader.vue - 主组件
  2. fileChunk.ts - 文件分片和哈希计算工具
  3. uploader.post.ts - 后端API处理
  4. uploaderImg.vue - 调用示例

核心功能实现

1. 文件分片处理

fileChunk.ts 中,我们实现了文件分片功能:

import SparkMD5 from 'spark-md5';
/**
 * 创建文件分片
 * @param file 文件对象
 * @param chunkSize 每个分片的大小 (字节)
 */
export const createFileChunk = (file: File, chunkSize: number) => {
  const chunks = []
  let current = 0
  while (current < file.size) {
    const end = Math.min(current + chunkSize, file.size)
    const chunk = file.slice(current, end)
    chunks.push({
      file: chunk,
      index: chunks.length,
      start: current,
      end: end,
      size: end - current,
    })
    current = end
  }
  return chunks
}

/**
 * 计算文件hash (使用SparkMD5)
 * @param file 文件对象
 */
export const calculateHash = (file: File): Promise<string> => {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer()

    const reader = new FileReader()
    const chunkSize = 2 * 1024 * 1024 // 2MB
    const chunks = Math.ceil(file.size / chunkSize)
    let currentChunk = 0

    reader.onload = (e) => {
      spark.append(e.target?.result as ArrayBuffer)
      currentChunk++

      if (currentChunk < chunks) {
        loadNext()
      } else {
        resolve(spark.end())
      }
    }

    const loadNext = () => {
      const start = currentChunk * chunkSize
      const end = Math.min(start + chunkSize, file.size)
      const chunk = file.slice(start, end)
      reader.readAsArrayBuffer(chunk)
    }

    loadNext()
  })
}

这个方法将大文件分割成指定大小的多个小分片,便于上传和管理。

2. 文件哈希计算

使用 SparkMD5 库计算文件哈希,用于唯一标识文件:

export const calculateHash = (file: File): Promise<string> => {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer()
    const reader = new FileReader()
    const chunkSize = 2 * 1024 * 1024 // 2MB
    const chunks = Math.ceil(file.size / chunkSize)
    let currentChunk = 0

    reader.onload = (e) => {
      spark.append(e.target?.result as ArrayBuffer)
      currentChunk++

      if (currentChunk < chunks) {
        loadNext()
      } else {
        resolve(spark.end())
      }
    }

    const loadNext = () => {
      const start = currentChunk * chunkSize
      const end = Math.min(start + chunkSize, file.size)
      const chunk = file.slice(start, end)
      reader.readAsArrayBuffer(chunk)
    }

    loadNext()
  })
}

3. 上传组件实现

Uploader.vue 组件提供了完整的上传功能:






4. 后端API实现

uploader.post.ts 处理分片上传和合并:

import fs from 'fs'
import path from 'path'
import { promisify } from 'util'
import { H3Event } from 'h3'
import { randomUUID } from 'crypto'

const mkdir = promisify(fs.mkdir)
const writeFile = promisify(fs.writeFile)
const readdir = promisify(fs.readdir)
const unlink = promisify(fs.unlink)
const stat = promisify(fs.stat)
const rename = promisify(fs.rename)

const UPLOAD_DIR = path.resolve(process.cwd(), 'uploads')
const CHUNK_DIR = path.resolve(UPLOAD_DIR, 'chunks')

// 确保上传目录存在
if (!fs.existsSync(UPLOAD_DIR)) {
  fs.mkdirSync(UPLOAD_DIR, { recursive: true })
}
if (!fs.existsSync(CHUNK_DIR)) {
  fs.mkdirSync(CHUNK_DIR, { recursive: true })
}

export default defineEventHandler(async (event: H3Event) => {
  const { req, res } = event.node

  if (req.method !== 'POST') {
    res.statusCode = 405
    return { error: 'Method not allowed' }
  }

  try {
    const contentType = req.headers['content-type'] || ''

    if (contentType.includes('multipart/form-data')) {
      // 处理分片上传
      return await handleChunkUpload(event)
    } else if (contentType.includes('application/json')) {
      // 处理合并请求
      const body = await readBody(event)
      if (body.action === 'merge') {
        return await mergeChunks(body)
      }
    }

    return { error: 'Invalid request' }
  } catch (error) {
    console.error('Upload error:', error)
    res.statusCode = 500
    return { error: 'Internal server error' }
  }
})

async function handleChunkUpload(event: H3Event) {
  const formData = await readMultipartFormData(event)
  if (!formData) {
    throw new Error('Invalid form data')
  }

  const fileData = formData.find(item => item.name === 'file')
  const hash = formData.find(item => item.name === 'hash')?.data.toString()
  const filename = formData.find(item => item.name === 'filename')?.data.toString()

  if (!fileData || !hash || !filename) {
    throw new Error('Missing required fields')
  }

  // 保存分片到临时目录
  const chunkPath = path.resolve(CHUNK_DIR, hash)
  await writeFile(chunkPath, fileData.data)

  return { success: true, hash }
}

async function mergeChunks(body: any) {
  const { fileHash, filename, chunkCount } = body

  // 验证所有分片是否已上传
  const chunkFiles = await readdir(CHUNK_DIR)
  const uploadedChunks = chunkFiles.filter(name => name.startsWith(fileHash))

  if (uploadedChunks.length !== Number(chunkCount)) {
    throw new Error('Not all chunks have been uploaded')
  }

  // 按分片索引排序
  uploadedChunks.sort((a, b) => {
    const aIndex = parseInt(a.split('-').pop() || '0')
    const bIndex = parseInt(b.split('-').pop() || '0')
    return aIndex - bIndex
  })

  // 创建最终文件
  const filePath = path.resolve(UPLOAD_DIR, filename)
  const writeStream = fs.createWriteStream(filePath)

  // 合并所有分片
  for (const chunkName of uploadedChunks) {
    const chunkPath = path.resolve(CHUNK_DIR, chunkName)
    const chunkData = await fs.promises.readFile(chunkPath)
    writeStream.write(chunkData)
    await unlink(chunkPath) // 删除已合并的分片
  }

  writeStream.end()

  return new Promise((resolve, reject) => {
    writeStream.on('finish', () => {
      resolve({ success: true, path: filePath })
    })
    writeStream.on('error', (error) => {
      reject(error)
    })
  })
}

组件使用示例






你可能感兴趣的:(nuxt,javascript,开发语言,ecmascript,nuxt3,vue3,typescript)