Vue3 视频库系统:基于 WebWorker 的大文件分片上传与播放实现
前言
在现代 Web 应用中,视频内容管理是一个常见且重要的功能。本文将详细介绍如何使用 Vue3、TypeScript 和 WebWorker 技术构建一个功能完善的视频库系统,实现大文件分片上传、视频播放、缩略图懒加载等核心功能。
项目概述
核心功能
- 🎬 视频播放:支持多种格式视频在线播放
- 📤 大文件上传:基于 WebWorker 的分片上传,支持 1GB 以内视频文件
- 🔄 并发控制:智能的上传队列管理和并发控制
- 🖼️ 懒加载:缩略图懒加载优化性能
- 📊 进度跟踪:实时上传进度显示
- 🎯 文件管理:重命名、删除等基础操作
技术栈
- 前端框架:Vue3 + TypeScript
- UI 组件:Element Plus
- 状态管理:Vue3 Reactive API
- 并发处理:WebWorker
- 文件处理:原生 File API
系统架构设计
整体架构
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Vue组件层 │ │ 服务层 │ │ Worker层 │
│ │ │ │ │ │
│ VideoLibrary │───▶│ UploadService │───▶│ upload-worker │
│ UploadList │ │ UploadState │ │ │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
核心模块
1. 上传服务层(UploadService)
负责文件上传的核心逻辑,包括队列管理、Worker 通信等。
2. 状态管理层(UploadState)
使用 Vue3 的 reactive API 管理上传状态,实现响应式数据更新。
3. Worker 处理层(upload-worker)
在独立线程中处理文件分片和上传,避免阻塞主线程。
核心功能实现
1. 大文件分片上传
分片策略
// 分片配置
const UPLOAD_CONFIG = {
MAX_BATCH_SIZE: 6, // 每批次分片数量
WORKER_CONCURRENCY: 3, // Worker内并发数
BATCH_TIMEOUT: 600000, // 批次超时时间
CHUNK_SIZE: 10 * 1024 * 1024, // 分片大小:10MB
MIN_CHUNK_SIZE: 5 * 1024 * 1024 // 最小分片:5MB
}
分片计算逻辑
// 计算分片大小
const standardChunkSize = UPLOAD_CONFIG.CHUNK_SIZE // 10MB
let totalChunks = Math.ceil(file.size / standardChunkSize)
const finalChunkSizes: number[] = []
// 确保最后一个分片不小于最小限制
if (totalChunks > 1) {
const lastChunkSize = file.size % standardChunkSize
if (lastChunkSize > 0 && lastChunkSize < UPLOAD_CONFIG.MIN_CHUNK_SIZE) {
// 将最后一个分片与倒数第二个分片合并
totalChunks = totalChunks - 1
const mergedLastChunkSize = standardChunkSize + lastChunkSize
// 设置分片大小
for (let i = 0; i < totalChunks - 1; i++) {
finalChunkSizes.push(standardChunkSize)
}
finalChunkSizes.push(mergedLastChunkSize)
} else {
// 正常分片
for (let i = 0; i < totalChunks; i++) {
finalChunkSizes.push(
i === totalChunks - 1 ? file.size % standardChunkSize || standardChunkSize : standardChunkSize
)
}
}
}
2. WebWorker 并发处理
Worker 消息处理
// upload-worker.ts
self.onmessage = async (event) => {
const {type, payload} = event.data
switch (type) {
case 'UPLOAD_BATCH':
await handleBatchUpload(payload)
break
case 'COMPLETE_FILE':
handleFileComplete(payload)
break
case 'CANCEL_UPLOAD':
handleCancelUpload(payload.uploadId)
break
}
}
并发上传控制
// 并发上传实现
async function uploadChunksConcurrently(
chunks: Array<{chunk: ArrayBuffer; chunkIndex: number}>,
fileName: string,
uploadId: string,
fileId: string,
totalChunks: number,
progress: ProgressInfo
) {
// 创建所有上传Promise
const uploadPromises = chunks.map(({chunk, chunkIndex}) =>
uploadChunkWithProgress(chunk, chunkIndex, fileName, uploadId, fileId, totalChunks, progress)
)
// 并发执行所有上传任务
await Promise.all(uploadPromises)
}
3. 响应式状态管理
状态定义
// uploadState.ts
export interface FileInfo {
id: string
fileName: string
fileSize: string
status: 'queued' | 'uploading' | 'success' | 'error'
progress: number
error?: string
}
export const uploadState = reactive<UploadState>({
files: {}
})
状态更新方法
// 更新上传进度
export function updateProgressById(fileId: string, progress: number): void {
const file = uploadState.files[fileId]
if (file && file.status !== 'success') {
if (file.status === 'queued') {
file.status = 'uploading'
}
file.progress = progress
}
}
// 设置上传成功
export function setUploadSuccessById(fileId: string): void {
const file = uploadState.files[fileId]
if (file) {
file.status = 'success'
file.progress = 100
}
}
4. 视频播放功能
播放组件实现
<template>
<!-- 视频播放对话框 -->
<el-dialog
v-model="dialogVisible"
title="视频播放"
width="70%"
:before-close="closeVideoDialog"
destroy-on-close
:close-on-click-modal="false"
class="video-dialog"
:modal="false"
>
<div class="video-player-container">
<video
ref="videoPlayerRef"
:src="currentVideoUrl"
controls
class="video-player"
@contextmenu.prevent
:disablePictureInPicture="true"
controlsList="nodownload noremoteplayback noplaybackrate"
></video>
</div>
</el-dialog>
</template>
播放控制逻辑
// 播放视频
const playVideo = (item: VideoItem) => {
if (item.status === 2) {
return ElMessage.warning('视频正在加载中,请稍后再试')
}
currentVideoUrl.value = getVideoUrl(item.fileId)
dialogVisible.value = true
// 确保视频元素已渲染后播放
setTimeout(() => {
if (videoPlayerRef.value) {
videoPlayerRef.value.play().catch(() => {
// 播放失败处理
})
}
}, 100)
}
// 关闭视频对话框
const closeVideoDialog = () => {
dialogVisible.value = false
if (videoPlayerRef.value) {
videoPlayerRef.value.pause()
videoPlayerRef.value.currentTime = 0
}
}
5. 缩略图懒加载
Intersection Observer 实现
// 初始化Intersection Observer
const initIntersectionObserver = () => {
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const id = entry.target.id.split('-')[1]
const item = videoData.value.find((v) => v.id.toString() === id)
if (item) {
// 当元素进入视口时,设置为可见
item.isVisible = entry.isIntersecting
}
})
},
{
root: null,
rootMargin: '100px', // 提前100px加载
threshold: 0.1 // 当10%的元素可见时触发
}
)
}
滚动优化
// 滚动事件处理(节流)
const handleScroll = (() => {
let timer: ReturnType<typeof setTimeout> | null = null
return () => {
if (!timer) {
timer = setTimeout(() => {
// 重新检查可见性
const entries = document.querySelectorAll('[id^="thumbnail-"]')
entries.forEach((el) => {
const rect = el.getBoundingClientRect()
const isVisible =
rect.top <= (window.innerHeight || document.documentElement.clientHeight) + 100 && rect.bottom >= -100
const id = el.id.split('-')[1]
const item = videoData.value.find((v) => v.id.toString() === id)
if (item) {
item.isVisible = isVisible
}
})
timer = null
}, 200)
}
}
})()
6. 文件管理功能
重命名功能
const handleCommand = async (type: string, item: VideoItem) => {
if (type === 'rename') {
// 保存原始文件名
item.originalFileName = item.fileName
item.isEdit = true
nextTick(() => {
const inputs = document.querySelectorAll('.video-info .el-input__inner')
const lastInput = inputs[inputs.length - 1] as HTMLInputElement
if (lastInput) {
lastInput.focus()
// 选中文件名(不包含扩展名)
const fileName = item.fileName
const dotIndex = fileName.lastIndexOf('.')
if (dotIndex > 0) {
lastInput.setSelectionRange(0, dotIndex)
} else {
lastInput.select()
}
}
})
}
}
// 保存重命名
const handleSaveRename = async (item: VideoItem) => {
const data = {
id: item.id,
fileName: item.fileName
}
await api.rename(data)
ElMessage.success('重命名成功')
item.isEdit = false
query()
}
删除功能
const handleDelete = async (item: VideoItem) => {
try {
await ElMessageBox.confirm(`确定删除视频${item.fileName}吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await api.delete(item.id)
ElMessage.success('删除成功')
query()
} catch (error) {
// 用户取消操作
}
}
性能优化策略
1. 上传性能优化
分片大小优化
- 标准分片:10MB,平衡上传效率和内存占用
- 最小分片:5MB,避免过小分片影响性能
- 智能合并:最后一个分片过小时自动合并
并发控制
- 批次处理:每批次最多 6 个分片
- Worker 并发:每个批次内最多 3 个并发上传
- 队列管理:智能的上传队列,避免资源争抢
2. 渲染性能优化
懒加载策略
// 缩略图懒加载
<img v-if="item.thumbnailFileId && item.isVisible"
:src="getVideoUrl(item.thumbnailFileId)"
alt="视频缩略图" />
<img v-else-if="!item.thumbnailFileId"
src="@/assets/images/video/video-loading.png"
alt="视频缩略图" />
虚拟滚动
- 使用 Intersection Observer 监听元素可见性
- 只渲染可视区域内的缩略图
- 提前 100px 预加载,提升用户体验
3. 内存管理
Worker 内存优化
// 文件上传完成后清理
function handleFileComplete(payload: any) {
// 清理文件进度信息
fileProgress.delete(uploadId)
cancelledFiles.delete(uploadId)
// 清理AbortController
abortControllers.delete(uploadId)
}
组件卸载清理
onUnmounted(() => {
// 断开观察器连接
if (observer) {
observer.disconnect()
observer = null
}
// 移除事件监听
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('resize', handleScroll)
})
总结
本文详细介绍了基于 Vue3 和 WebWorker 的视频库系统实现,涵盖了以下关键技术点:
- 大文件分片上传:智能分片策略,支持 1GB 以内视频文件
- WebWorker 并发处理:避免阻塞主线程,提升用户体验
- 响应式状态管理:使用 Vue3 Reactive API 实现状态管理
- 性能优化:懒加载、虚拟滚动、内存管理等多重优化
- 用户体验:完善的错误处理和用户提示
该系统具有以下优势:
- 🚀 高性能:WebWorker 并发处理,不阻塞 UI
- 🛡️ 高可靠:完善的错误处理和重试机制
- 🎯 用户友好:实时进度显示,直观的操作界面
- 🔧 易扩展:模块化设计,易于维护和扩展
通过这套方案,我们成功构建了一个功能完善、性能优异的视频库系统,为用户提供了流畅的视频上传和播放体验。
参考资源
本文基于实际项目开发经验总结,如有问题欢迎交流讨论。