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 后续优化方向
- 分片大小动态调整
- 上传速度智能限制
- 云存储服务集成
- 分布式文件存储
热门推荐
朝天门喧闹的市井里竟藏个4A级景区,最后的山城棒棒军依然在坚守
古乐器修复:重新拾起散落在时间缝隙中的音符
股市休市的原因有哪些?股市休市对投资者有何影响?
精选内容集|健康美味吃出好身材 低卡低脂餐灵感分享
巨大垂体瘤致小伙暴瘦血糖飙升,华山医院“经鼻联合开颅”新技术根除祸根
剪切后丢失的文件怎么恢复?推荐4个方法,轻松易操作
做胃肠镜前需要做什么准备
《新华字典》是如何收词的?编辑室编审揭秘
成都美食全攻略:从火锅到小吃,尽享舌尖上的成都
好吃不胖的秘密武器——凉拌平菇
汽车散热器清洗的特殊技巧
安徽合肥发出首张“全程网办”出入境证件
南京即将成为“双机场”城市,机场扩容将如何影响区域经济?
中国古代雅称大全:敬辞、谦辞与生活雅语
温度如何进行准确换算?这种换算方法在实际中有哪些应用?
这是预防老年痴呆最简单的方式,有嘴都能做,很多人不知道
中国科学院主办香山科学会议:探讨地球系统科学研究新范式
低价+等效,仿制药,各国的“主流”
长期服用泮托拉唑会导致多种副作用,补充5种营养,改善机体状态
Perthes病:症状、诊断与治疗全解析
当代心理战“取胜于无形”
二甲双胍每天吃多少效果最佳?什么时候吃最好?看看你吃对了吗
中外师生走进北京同仁堂 感受中医文化传承与创新
科技赋能监管,为食品安全装上“智慧眼”
隔水蒸肉时间攻略,肉种大小决定烹饪时长,科学调控锁住营养美味
消防员享受什么待遇?一个月工资多少钱?
绿色供应链管理:推动企业可持续发展新路径
这种特殊的肠道细菌,竟可能是长寿的秘诀?
哪些食物会导致孕妇腹胀气?胀气怀孕应该如何处理?
如何选择合适的减速电机