在移动端开发(App/小程序/H5)中, 图片上传 是一个极其高频且容易产生性能瓶颈的场景。直接上传原图往往会带来以下问题:
我们的目标是封装一个通用组件 MyUpload ,实现以下流程:
我们基于 u-upload 进行二次封装,同时引入压缩插件。
// 上传核心逻辑
async uploadFilePromise(file, lists) {
let OriginalUrl = file.url
let afterCompressFile = null
let ifcompress = false
// 1. file.type 判空保护:防止部分安卓机型或特殊场景下 type 丢失导致报错
// 2. 模糊匹配 'image':覆盖 image/png, image/jpeg, image/gif 等所有图片类型
// 3. 大小阈值:只有超过 1MB (1024KB) 才压缩,小图直接上传,阈值可自行调整
if (file.type && file.type.indexOf('image') != -1 && file.size / 1024 > 1024) {
// 标记为需要压缩
ifcompress = true
// 调用压缩插件,返回值是压缩后的 Base64 字符串
let afterCompressBase64 = await this.$refs.helangCompress.compress({
src: OriginalUrl,
maxSize: 1024, // 限制最大分辨率
fileType: 'jpg', // 统一输出为 jpg 减少体积
quality: 0.8, // 压缩质量
minSize: 640 // 最小尺寸保护
})
// uni.uploadFile 不支持直接传 Base64,必须转为 File 对象
afterCompressFile = await base64ToFile(afterCompressBase64, file.name)
}
return new Promise((resolve, reject) => {
uni.uploadFile({
url: config.upLoadUrl,
name: 'file',
// 如果压缩了,filePath 传 null(或根据平台差异调整),file 传转换后的对象
// 如果没压缩,直接用原路径
filePath: !ifcompress ? file.url : file.name,
file: !ifcompress ? null : afterCompressFile,
header: {
'Authorization': 'Bearer ' + uni.getStorageSync('Token') ?? '',
},
success: (res) => {
// 处理服务端返回
let data = JSON.parse(res.data);
if(data.code == 200){
resolve(data.url)
} else {
uni.$u.toast(data.message)
reject(data)
}
},
fail: (err) => {
console.log("Upload failed", err)
reject(err)
}
});
})
}
实时更新 UI 的 loading 状态,并在失败时自动清理,这点蛮重要的,很多时候上传失败但是组件上展示是有图片的(这是本地的blob图片,并不是真正上传服务器后的图片)。
async afterRead(event) {
// 1. 预处理:将新选择的文件加入列表,状态设为 'uploading'
let lists = [].concat(event.file)
let fileListLen = this[`fileList${event.name}`].length
lists.map((item) => {
this[`fileList${event.name}`].push({
...item,
status: 'uploading',
message: '上传中'
})
});
// 2. 串行上传(也可以改为 Promise.all 并行,视服务器压力而定)
for (let i = 0; i < lists.length; i++) {
try {
// 等待单个文件上传(含压缩耗时)
const result = await this.uploadFilePromise(lists[i], lists)
// 3. 成功回调:更新列表状态为 success,并回填 URL
let item = this[`fileList${event.name}`][fileListLen]
this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
status: 'success',
message: '',
url: result
}))
fileListLen++
} catch(e) {
// 4. 失败回滚:移除该项,避免 UI 显示错误的占位
this[`fileList${event.name}`].splice(fileListLen, 1)
uni.$u.toast('上传失败,请重试')
}
}
// 5. 通知父组件更新数据
this.emitInput(this[`fileList${event.name}`])
}
/* File Info
* 二次封装上传图片组件
*/
/* File Info
* 封装压缩图片的canvas
*/
/* File Info
* 转换base64方法
*/
export function base64ToFile(base64Data, filename='xxx1.jpg') {
// 将base64的数据部分提取出来
const parts = base64Data.split(';base64,');
const contentType = parts[0].split(':')[1];
const raw = window.atob(parts[1]);
// 将原始数据转换为Uint8Array
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
// 使用Blob创建一个新的文件
const blob = new Blob([uInt8Array], {type: contentType});
// 创建File对象
const file = new File([blob], filename, {type: contentType});
// console.log('创建File对象==',file,blob)
return file;
}
通过这次封装,我们不仅解决了一个具体的业务需求,更重要的是提升了代码的 复用性 和 健壮性 。