nuxt3 + vue3 分片上传组件全解析(大文件分片上传)
本文将详细介绍一个基于 Vue.js 的分片上传组件的设计与实现,该组件支持大文件分片上传进度显示等功能。
组件概述
这个上传组件主要包含以下功能:
- 支持大文件分片上传(默认5MB一个分片)
- 支持文件哈希计算,用于文件唯一标识
- 显示上传进度(整体和单个文件)
- 支持自定义UI样式
- 提供完整的文件管理功能(添加、删除)
- 后端支持分片合并和临时存储
组件结构
组件由三个主要文件组成:
Uploader.vue
- 主组件fileChunk.ts
- 文件分片和哈希计算工具uploader.post.ts
- 后端API处理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
组件提供了完整的上传功能:
{{ fileInfo.file.name }}
{{ (fileInfo.file.size / 1024 / 1024).toFixed(2) }} MB
{{ Math.round(fileInfo.progress) }}%
✓
{{ Math.round(totalProgress) }}%
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)
})
})
}
组件使用示例
默认调用
自定义调用
选择文件
-
{{ fileInfo.file.name }}
{{ Math.round(fileInfo.progress) }}%
本文地址:https://www.vps345.com/15659.html
上一篇:【闲谈】对于c++未来的看法