uniapp实战:混合使用组件与API,优雅实现图片与视频的上传与预览
1. 为什么需要混合使用组件与API?
在uniapp开发中,处理媒体文件上传是个高频需求。很多开发者一开始都会像我一样,想找个现成的组件一把梭搞定所有功能。uView的u-upload组件确实是个不错的选择,它封装了图片上传的完整流程:从相册选择、拍照、预览到上传一气呵成。但实际用起来就会发现,当涉及到视频处理时,这个组件就显得力不从心了。
最典型的问题就是视频预览功能。u-upload组件对图片的预览支持很完善,点击就能全屏查看,但换成视频就完全失效。我最初以为是自己的写法有问题,反复检查文档和示例代码,最后才确认是组件本身的限制。这时候就需要换个思路——为什么不把组件的优势和原生API结合起来用呢?
这种混合方案有几个明显好处:
- 各取所长:u-upload处理图片更高效,原生API处理视频更灵活
- 性能优化:避免用单一方案处理所有媒体类型导致的性能损耗
- 体验统一:虽然底层实现不同,但给用户的交互体验可以保持一致
2. 图片上传的最佳实践
2.1 u-upload组件的正确打开方式
先来看图片上传的实现。u-upload组件已经帮我们封装好了大部分功能,只需要简单配置就能用:
<u-upload :fileList="imgList" @afterRead="imgRead" @delete="deletePic" name="1" multiple :previewFullImage="true" ></u-upload>几个关键参数说明:
fileList:绑定已选择的图片列表afterRead:选择图片后的回调delete:删除图片的回调multiple:是否支持多选previewFullImage:开启全屏预览
这里有个小技巧:如果只需要上传单张图片,可以把multiple设为false,这时afterRead回调的参数event.file会是对象而非数组,处理起来更简单。
2.2 图片上传的完整流程
选择图片后的处理逻辑很关键,我整理了一个健壮的实现方案:
async imgRead(event) { // 统一转为数组处理 let lists = [].concat(event.file) let fileListLen = this.imgList.length // 先更新UI显示上传状态 lists.map((item) => { this.imgList.push({ ...item, status: 'uploading', message: '上传中' }) }) // 逐个上传 for (let i = 0; i < lists.length; i++) { try { const result = await this.uploadImg(lists[i], i) let item = this.imgList[fileListLen] this.imgList.splice(fileListLen, 1, Object.assign(item, { status: 'success', message: '', accessoryUrl: result })) fileListLen++ } catch (error) { // 处理上传失败 this.imgList[fileListLen].status = 'failed' this.imgList[fileListLen].message = '上传失败' } } }这个实现有几个亮点:
- 统一处理单选和多选情况
- 实时更新上传状态反馈给用户
- 加入错误处理提升健壮性
- 使用async/await让异步代码更清晰
2.3 上传服务器的关键细节
实际项目中,上传接口通常需要携带认证信息和额外参数:
uploadImg(item, index) { return new Promise((resolve, reject) => { uni.uploadFile({ url: 'https://your-api.com/upload', filePath: item.url, name: 'file', header: { 'Authorization': `Bearer ${getToken()}` }, formData: { type: 'image', filename: item.name }, success: (res) => { const data = JSON.parse(res.data) resolve(data.url) }, fail: (err) => { reject(err) } }) }) }注意几个细节:
header中传递认证tokenformData可以附加业务参数- 服务端返回的数据需要JSON.parse处理
- 使用Promise包装便于异步控制
3. 视频上传的灵活方案
3.1 为什么不用u-upload处理视频?
虽然u-upload组件在文档中说支持视频,但实际使用会发现几个问题:
- 视频预览功能无效
- 无法获取视频时长等元信息
- 上传进度反馈不准确
- 大视频文件处理性能差
这时候就需要请出uniapp的原生API——uni.chooseVideo,它专门为视频场景做了优化。
3.2 视频选择与上传实现
视频处理的整体流程与图片类似,但实现细节有所不同:
<view class="up-video-box"> <!-- 已选视频预览 --> <view v-for="(item,i) in videoList" :key="i"> <video :src="item.url"></video> <u-icon @click="deleteVideo(i)" name="close"></u-icon> </view> <!-- 选择视频按钮 --> <view @click="chooseVideo"> <u-icon name="camera-fill"></u-icon> </view> </view>对应的JS逻辑:
chooseVideo() { uni.chooseVideo({ sourceType: ['camera', 'album'], maxDuration: 60, // 限制60秒 success: (res) => { this.videoList.push({ url: res.tempFilePath, name: res.tempFilePath.split('/').pop() }) this.uploadVideo(res.tempFilePath) } }) }chooseVideo的几个实用参数:
sourceType:控制是拍照还是相册选择maxDuration:限制视频时长compressed:是否压缩视频
3.3 视频上传的注意事项
视频文件通常较大,上传时需要特别注意:
uploadVideo(filePath) { const task = uni.uploadFile({ url: 'https://your-api.com/upload', filePath, name: 'video', header: { 'Authorization': `Bearer ${getToken()}` }, formData: { type: 'video' }, success: (res) => { const data = JSON.parse(res.data) // 更新视频列表中的url为服务器返回的地址 }, fail: (err) => { console.error('上传失败', err) } }) // 监听上传进度 task.onProgressUpdate((res) => { console.log(`进度: ${res.progress}%`) }) }相比图片上传,视频上传需要额外注意:
- 使用
onProgressUpdate显示上传进度 - 处理可能的超时问题
- 考虑断点续传方案(对大视频特别重要)
- 服务端要做大小限制
4. 混合方案的架构设计
4.1 状态管理的统一处理
虽然图片和视频使用了不同技术方案,但在数据管理上应该保持统一:
data() { return { mediaList: [], // 统一管理所有媒体文件 imgList: [], // 专门用于u-upload绑定的图片列表 videoList: [] // 视频列表 } }建议采用这种混合管理方式:
mediaList存储所有媒体文件的元信息imgList和videoList分别用于对应组件的绑定
4.2 上传队列与并发控制
当用户同时选择多个文件时,需要合理控制上传并发:
class UploadQueue { constructor(maxConcurrent = 2) { this.queue = [] this.activeCount = 0 this.maxConcurrent = maxConcurrent } add(task) { this.queue.push(task) this.run() } run() { while (this.activeCount < this.maxConcurrent && this.queue.length) { const task = this.queue.shift() this.activeCount++ task().finally(() => { this.activeCount-- this.run() }) } } } // 使用示例 const uploadQueue = new UploadQueue(2) uploadQueue.add(() => this.uploadImg(img1)) uploadQueue.add(() => this.uploadVideo(video1))这个简单的队列实现可以:
- 控制同时上传的文件数量
- 避免网络拥堵
- 提供更好的用户体验
4.3 错误处理与重试机制
网络请求难免会失败,完善的错误处理很重要:
async uploadWithRetry(file, maxRetry = 3) { let retryCount = 0 while (retryCount < maxRetry) { try { return await this.uploadFile(file) } catch (error) { retryCount++ if (retryCount >= maxRetry) throw error await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)) } } }这个重试机制会:
- 最多重试3次
- 每次重试间隔时间递增(1s, 2s, 3s)
- 超过重试次数后抛出错误
5. 用户体验优化技巧
5.1 上传进度可视化
对于大文件上传,进度反馈非常重要:
<u-line-progress :percent="progress" :showText="false" ></u-line-progress>配合上传任务的进度事件:
task.onProgressUpdate((res) => { this.progress = res.progress })5.2 文件大小与类型校验
在上传前做客户端校验可以节省带宽:
// 图片校验 function validateImage(file) { const maxSize = 5 * 1024 * 1024 // 5MB const types = ['image/jpeg', 'image/png'] if (file.size > maxSize) { uni.showToast({ title: '图片不能超过5M', icon: 'none' }) return false } if (!types.includes(file.type)) { uni.showToast({ title: '仅支持JPEG/PNG格式', icon: 'none' }) return false } return true }5.3 预览功能的增强实现
对于视频预览,可以这样优化:
previewVideo(url) { uni.navigateTo({ url: `/pages/videoPlayer?src=${encodeURIComponent(url)}` }) }然后在单独页面中使用uniapp的video组件全屏播放:
<template> <view> <video :src="src" controls autoplay style="width:100vw;height:100vh" ></video> </view> </template>这种实现方式比直接在当前页面弹出播放器体验更好,还能支持全屏旋转等功能。
6. 实际项目中的踩坑记录
6.1 安卓与iOS的兼容性问题
在真机测试时发现几个平台差异:
- iOS选择视频时
tempFilePath可能不带扩展名 - 安卓部分机型上传大文件容易超时
- 各平台对视频格式的支持不一致
解决方案:
// 统一处理文件名 function getFileName(path) { let name = path.split('/').pop() if (!name.includes('.')) { // 根据文件头判断类型 const type = path.startsWith('file://') ? getFileTypeFromHeader(path) : 'mp4' name += `.${type}` } return name }6.2 内存泄漏问题
长时间使用媒体上传功能后,发现页面性能下降。经排查是以下几个原因:
- 上传完成的文件引用未释放
- 事件监听器未移除
- 大尺寸预览图缓存
解决方法:
beforeDestroy() { // 释放视频对象 this.videoList.forEach(item => { item.url = null }) // 清除图片缓存 this.imgList.forEach(item => { uni.removeSavedFile({ filePath: item.url }) }) }6.3 微信小程序特殊限制
在微信小程序环境中,有几个额外限制需要注意:
- 单次上传不能超过10MB
- 不支持多文件同时上传
- 视频选择时长默认不超过60秒
对应的适配方案:
// 微信环境下启用压缩 if (uni.getSystemInfoSync().platform === 'ios') { options.compressed = false } else { options.compressed = true }7. 性能优化进阶方案
7.1 分片上传大文件
对于超过20MB的视频文件,建议实现分片上传:
async function chunkUpload(file, chunkSize = 5 * 1024 * 1024) { const fileSize = file.size const chunks = Math.ceil(fileSize / chunkSize) const fileMd5 = await getFileMd5(file) for (let i = 0; i < chunks; i++) { const start = i * chunkSize const end = Math.min(fileSize, start + chunkSize) const chunk = file.slice(start, end) await uploadChunk({ chunk, index: i, fileMd5, chunks }) } await mergeChunks(fileMd5) }7.2 客户端压缩方案
对于图片文件,可以在上传前进行压缩:
function compressImage(file) { return new Promise((resolve) => { uni.compressImage({ src: file.url, quality: 70, success: (res) => { resolve(res.tempFilePath) } }) }) }7.3 断点续传实现
基于本地缓存实现断点续传:
// 上传前检查本地记录 const uploadedChunks = uni.getStorageSync(fileMd5) || [] if (uploadedChunks.includes(chunkIndex)) { continue // 跳过已上传分片 } // 上传成功后更新记录 uploadedChunks.push(chunkIndex) uni.setStorageSync(fileMd5, uploadedChunks)8. 完整代码结构示例
最后给出一个完整的页面结构参考:
<template> <view class="container"> <!-- 图片上传区域 --> <view class="section"> <text class="title">图片上传</text> <u-upload :fileList="imgList" @afterRead="handleImageRead" @delete="removeImage" multiple :maxCount="9" ></u-upload> </view> <!-- 视频上传区域 --> <view class="section"> <text class="title">视频上传</text> <view class="video-uploader"> <view v-for="(video, index) in videoList" :key="index" class="video-item" > <video :src="video.url"></video> <u-icon @click="removeVideo(index)" name="close"></u-icon> </view> <view @click="chooseVideo" class="add-btn"> <u-icon name="plus"></u-icon> </view> </view> </view> <!-- 提交按钮 --> <button @click="submit">提交</button> </view> </template> <script> export default { data() { return { imgList: [], videoList: [] } }, methods: { // 图片处理相关方法 async handleImageRead(event) { // 实现图片上传逻辑 }, removeImage(index) { this.imgList.splice(index, 1) }, // 视频处理相关方法 chooseVideo() { uni.chooseVideo({ success: (res) => { this.uploadVideo(res.tempFilePath) } }) }, async uploadVideo(filePath) { // 实现视频上传逻辑 }, removeVideo(index) { this.videoList.splice(index, 1) }, // 提交处理 async submit() { if (!this.imgList.length && !this.videoList.length) { uni.showToast({ title: '请至少上传一个文件', icon: 'none' }) return } try { await this.uploadAll() uni.showToast({ title: '提交成功' }) } catch (error) { uni.showToast({ title: '提交失败', icon: 'none' }) } }, async uploadAll() { const imagePromises = this.imgList.map(img => this.uploadImage(img) ) const videoPromises = this.videoList.map(video => this.uploadVideo(video.url) ) await Promise.all([...imagePromises, ...videoPromises]) } } } </script> <style> /* 样式省略 */ </style>这个实现包含了我们讨论的所有关键点:
- 图片使用u-upload组件
- 视频使用原生API
- 统一的上传状态管理
- 完整的用户交互流程
- 健壮的错误处理
在实际项目中,你可能还需要根据具体需求调整:
- 添加上传进度显示
- 增加文件大小和类型校验
- 实现更复杂的分片上传逻辑
- 优化移动端显示效果
