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 关键技术点
文件分片策略
- 固定大小分片(如5MB)
- 动态分片(根据网络状况调整)
断点续传实现
并发控制
三、前端完整实现
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 # 文件存储目录
八、效果演示
- 选择5GB视频文件上传
- 模拟网络中断后恢复上传
- 查看实时上传进度
- 验证文件完整性(MD5对比)
九、常见问题解答
Q1:分片大小如何选择?
Q2:大文件哈希计算卡顿?
- 使用Web Worker后台计算
- 抽样计算(取文件头尾+中间部分)
Q3:如何实现暂停功能?
const controller = new AbortController()
axios.post('/api/upload', formData, {
signal: controller.signal
})
// 暂停时调用
controller.abort()
十、总结与展望
10.1 实现成果
- 完整大文件分片上传方案
- 断点续传与秒传功能
- 生产级错误处理机制
- 可扩展的架构设计
10.2 后续优化方向
- 分片大小动态调整
- 上传速度智能限制
- 云存储服务集成
- 分布式文件存储
热门推荐
西安市第三医院张世荣主任推荐:丘脑出血术后康复锻炼法
熊出没新作定档大年初一:再勇敢一次!
光头强成“背锅侠”,《熊出没·重启未来》角色大揭秘!
克里奥佩特拉的珍珠项链:从古埃及传奇到现代时尚
仙女必备!五款项链让你秒变精致女神
婺源赏秋几月份去最好
环保科普 | 关于大气污染的基本知识你了解多少?
金融安防 | 数据中心园区安全防范及管理实践
揭秘詹姆斯4万分背后的秘密:科学训练与严格自律的完美结合
解密詹姆斯:百万美元打造的养生体系
40岁詹姆斯创历史!他与乔丹的传奇之争,谁将笑到最后?
勒布朗·詹姆斯:40岁后的华丽转身
詹姆斯42+17+8炸裂表现!湖人再胜勇士
共享经济大时代:机遇、挑战与未来路径
出国留学如何选择适合自己专业和兴趣的学校?
九夜茴教你打造动人文案:从细节到共鸣的写作技巧
黄庭坚与秦观:北宋文坛的双璧
青春期女生饰品选择的心理密码
珍珠还是翡翠?青春期女儿项链大PK!
综艺节目高质量发展迎来机遇和挑战
2025年春晚科技元素盘点:机器人、航空器、AI技术闪耀舞台
幼儿园生均面积国家标准
钱小豪再演“秋生”:经典重现还是创新突破?
钱小豪:从武打新星到演技派的传奇人生
雨雪天将至,骨科专家给您“防滑秘籍”,请注意查看!
对方微信账号已停用显示什么?一文读懂识别与应对策略的全面解析
解决微信登录问题的常见原因及应对方法总结
朋友圈里的心理学:你的每条文案都在诉说什么?
从古代国道、“茶马古道”再到南亚廊道
青少年腹痛:应重视心身同调