1. Vue3 视频上传组件开发实战在移动端H5应用开发中视频上传功能是常见的需求场景。不同于简单的图片上传视频处理涉及更多技术细节和性能考量。本文将基于Vue3Vite技术栈从零构建一个健壮的视频上传组件并深入解析其中的关键技术点。1.1 核心功能设计我们的视频上传组件需要实现以下核心功能支持从本地设备选择视频文件实时预览选中的视频内容对视频文件进行类型校验移动端适配和性能优化内存管理释放临时URL这些功能看似简单但在实际开发中会遇到各种边界情况和兼容性问题。接下来我们将逐步实现并优化这些功能。1.2 技术选型分析在Vue3生态中我们有多种实现文件上传的方案原生input方案最基础但兼容性较差Vant Uploader移动端优化API友好vue-filepond功能强大但体积较大Element Plus Upload适合PC端场景考虑到我们的应用场景是移动端H5Vant Uploader是最佳选择。它不仅体积小巧而且针对移动设备做了大量优化能够解决原生input在移动端的各种兼容性问题。2. 基础实现原生input方案虽然我们最终会选择Vant方案但理解原生实现原理非常重要。让我们先实现一个基础版本。2.1 模板结构设计template div classvideo-upload-container !-- 美化后的文件选择按钮 -- label classcustom-file-btn 选择本地视频 input typefile acceptvideo/* changehandleVideoSelect classfile-input / /label !-- 视频预览区域 -- div classvideo-preview-wrapper v-ifvideoUrl video :srcvideoUrl controls classvideo-player 您的浏览器不支持视频播放 /video /div !-- 无视频时的占位提示 -- div v-else classvideo-placeholder 请选择本地视频文件 /div /div /template这个模板包含三个主要部分美化后的文件选择按钮视频预览区域有视频时显示占位提示无视频时显示2.2 逻辑实现script setup langts import { ref, onUnmounted } from vue const videoUrl refstring() const handleVideoSelect (e: Event) { const target e.target as HTMLInputElement const file target.files?.[0] if (!file) { videoUrl.value return } // 校验视频类型 if (!file.type.startsWith(video/)) { alert(请选择有效的视频文件) target.value return } // 校验文件大小示例限制为100MB const MAX_SIZE 100 * 1024 * 1024 if (file.size MAX_SIZE) { alert(视频文件大小不能超过100MB) target.value return } try { // 生成临时URL用于预览 videoUrl.value URL.createObjectURL(file) } catch (error) { console.error(视频预览失败, error) alert(视频预览出错请重试) target.value } } // 组件卸载时释放临时URL onUnmounted(() { if (videoUrl.value) { URL.revokeObjectURL(videoUrl.value) } }) /script这段代码实现了文件选择处理文件类型校验文件大小限制生成预览URL内存管理释放临时URL2.3 样式优化style scoped /* 美化文件选择按钮 */ .custom-file-btn { display: inline-block; padding: 12px 24px; background-color: #409eff; color: white; border-radius: 8px; cursor: pointer; font-size: 15px; transition: background-color 0.3s; margin-bottom: 20px; } .custom-file-btn:hover { background-color: #66b1ff; } /* 隐藏原生文件选择框 */ .file-input { display: none; } /* 视频预览容器 */ .video-preview-wrapper { width: 100%; max-width: 400px; aspect-ratio: 16/9; border-radius: 8px; overflow: hidden; border: 1px solid #eee; margin: 0 auto; } /* 视频播放器样式 */ .video-player { width: 100%; height: 100%; object-fit: contain; background-color: #000; } /* 占位提示样式 */ .video-placeholder { width: 100%; max-width: 400px; aspect-ratio: 16/9; display: flex; align-items: center; justify-content: center; text-align: center; color: #999; border: 1px dashed #eee; border-radius: 8px; background-color: #f9f9f9; margin: 0 auto; } /* 移动端适配 */ media (max-width: 480px) { .custom-file-btn { width: 100%; text-align: center; } } /style样式设计要点使用aspect-ratio保持16:9比例响应式设计适配不同屏幕美化文件选择按钮优化视频播放器显示3. 进阶优化使用Vant Uploader原生input方案在移动端存在诸多问题我们需要使用更专业的解决方案。3.1 Vant Uploader集成首先安装Vantnpm install vant然后实现视频上传组件template van-uploader v-modelfileList acceptvideo/* :before-readbeforeRead capturealbum multiplefalse :max-size100 * 1024 * 1024 oversizeonOversize van-button iconvideo-o typeprimary选择视频/van-button /van-uploader div v-ifvideoUrl classvideo-preview video :srcvideoUrl controls classvideo-player /video van-button typedanger sizesmall clickclearVideo 清除视频 /van-button /div /template script setup langts import { ref } from vue import { showToast } from vant const fileList refany[]([]) const videoUrl ref() const beforeRead (file: File) { if (!file.type.startsWith(video/)) { showToast(请选择视频文件) return false } return true } const onOversize () { showToast(视频大小不能超过100MB) } watch(fileList, (newVal) { if (newVal.length 0) { videoUrl.value URL.createObjectURL(newVal[0].file) } else { videoUrl.value } }) const clearVideo () { fileList.value [] } /script3.2 Vant方案的优势更好的移动端体验统一的UI风格优化的交互体验内置加载状态提示更强的类型控制精确控制可选择的文件类型提供before-read钩子进行前置校验丰富的功能多选控制文件大小限制上传进度显示更好的兼容性解决各移动端浏览器的差异正确处理文件类型识别3.3 高级功能扩展我们可以进一步扩展组件的功能// 添加视频时长限制 const beforeRead async (file: File) { // 类型检查 if (!file.type.startsWith(video/)) { showToast(请选择视频文件) return false } // 大小检查 if (file.size 100 * 1024 * 1024) { showToast(视频大小不能超过100MB) return false } // 时长检查 try { const duration await getVideoDuration(file) if (duration 300) { // 5分钟限制 showToast(视频时长不能超过5分钟) return false } } catch (e) { console.error(获取视频时长失败, e) return false } return true } // 获取视频时长 const getVideoDuration (file: File): Promisenumber { return new Promise((resolve, reject) { const video document.createElement(video) video.preload metadata video.onloadedmetadata () { URL.revokeObjectURL(video.src) resolve(video.duration) } video.onerror () { reject(new Error(无法获取视频信息)) } video.src URL.createObjectURL(file) }) }4. 常见问题与解决方案4.1 移动端兼容性问题问题1在iOS Safari上acceptvideo/*可能无法正确过滤文件类型。解决方案van-uploader acceptvideo/mp4,video/quicktime :captureisIOS ? environment : album /问题2安卓设备上选择视频后无法预览。解决方案const handleVideoSelect async (file: File) { // 安卓设备可能需要延迟创建URL await nextTick() videoUrl.value URL.createObjectURL(file) }4.2 性能优化大文件处理添加文件大小限制提供压缩选项分片上传大文件内存管理及时释放不再使用的ObjectURL使用WeakRef管理大型媒体对象预览优化限制预览视频的分辨率使用缩略图代替完整视频预览4.3 安全性考虑文件类型验证不要仅依赖文件扩展名或type使用文件头信息进行验证const isVideoFile (file: File) { return new Promise((resolve) { const reader new FileReader() reader.onload (e) { const arr new Uint8Array(e.target?.result as ArrayBuffer) let header for (let i 0; i 4; i) { header arr[i].toString(16) } // 检查文件头信息 const videoHeaders [66747970, 00000020, 0000001c] resolve(videoHeaders.includes(header)) } reader.readAsArrayBuffer(file.slice(0, 4)) }) }XSS防护对用户提供的文件名进行转义使用CSP策略限制不安全的内容5. 完整实现与最佳实践结合以上所有优化点我们得到最终的视频上传组件实现template div classvideo-uploader van-uploader v-modelfileList :acceptacceptTypes :before-readbeforeRead :max-count1 :max-sizemaxSize oversizeonOversize click-uploadresetUploader template #default van-button iconvideo-o typeprimary :loadingisChecking {{ buttonText }} /van-button /template /van-uploader div v-ifvideoUrl classpreview-container video refvideoPlayer :srcvideoUrl controls classvideo-preview loadedmetadataonVideoLoaded /video div classvideo-info span时长: {{ formatDuration(duration) }}/span span大小: {{ formatFileSize(fileSize) }}/span /div van-button typedanger sizesmall clickclearVideo 清除视频 /van-button /div /div /template script setup langts import { ref, computed, watch } from vue import { showToast } from vant const props defineProps({ maxSize: { type: Number, default: 100 * 1024 * 1024 // 100MB }, maxDuration: { type: Number, default: 300 // 5分钟 }, acceptTypes: { type: String, default: video/mp4,video/webm,video/quicktime } }) const fileList refany[]([]) const videoUrl ref() const duration ref(0) const fileSize ref(0) const isChecking ref(false) const buttonText computed(() { return fileList.value.length 0 ? 重新选择 : 选择视频 }) const beforeRead async (file: File) { isChecking.value true try { // 验证文件类型 if (!props.acceptTypes.split(,).includes(file.type)) { showToast(不支持该视频格式) return false } // 验证文件大小 if (file.size props.maxSize) { showToast(视频大小不能超过${formatFileSize(props.maxSize)}) return false } // 验证视频时长 const videoDuration await getVideoDuration(file) if (videoDuration props.maxDuration) { showToast(视频时长不能超过${formatDuration(props.maxDuration)}) return false } fileSize.value file.size return true } catch (error) { console.error(视频验证失败:, error) showToast(视频验证失败) return false } finally { isChecking.value false } } const onOversize () { showToast(视频大小不能超过${formatFileSize(props.maxSize)}) } const resetUploader () { if (fileList.value.length 0) { fileList.value [] } } const clearVideo () { if (videoUrl.value) { URL.revokeObjectURL(videoUrl.value) videoUrl.value } fileList.value [] duration.value 0 fileSize.value 0 } const onVideoLoaded () { if (videoPlayer.value) { duration.value videoPlayer.value.duration } } // 工具函数 const formatDuration (seconds: number) { const mins Math.floor(seconds / 60) const secs Math.floor(seconds % 60) return ${mins}:${secs.toString().padStart(2, 0)} } const formatFileSize (bytes: number) { if (bytes 1024) return ${bytes} B if (bytes 1024 * 1024) return ${(bytes / 1024).toFixed(1)} KB return ${(bytes / (1024 * 1024)).toFixed(1)} MB } const getVideoDuration (file: File): Promisenumber { return new Promise((resolve, reject) { const video document.createElement(video) video.preload metadata video.onloadedmetadata () { URL.revokeObjectURL(video.src) resolve(video.duration) } video.onerror () { reject(new Error(无法获取视频信息)) } video.src URL.createObjectURL(file) }) } // 清理 onBeforeUnmount(() { clearVideo() }) watch(fileList, (newVal) { if (newVal.length 0) { const file newVal[0].file videoUrl.value URL.createObjectURL(file) fileSize.value file.size } else { clearVideo() } }) /script style scoped .video-uploader { display: flex; flex-direction: column; gap: 16px; } .preview-container { display: flex; flex-direction: column; align-items: center; gap: 8px; } .video-preview { width: 100%; max-width: 400px; aspect-ratio: 16/9; background-color: #000; border-radius: 8px; overflow: hidden; } .video-info { display: flex; gap: 16px; font-size: 12px; color: #666; } /style这个最终实现包含以下特性完整的类型校验和大小限制视频时长检测友好的用户提示内存管理响应式设计详细的视频信息展示在实际项目中你可以根据具体需求进一步调整参数和功能。例如添加上传进度显示、视频压缩、封面生成等高级功能。
阅读完成 · 觉得有帮助?