上周做版本迭代,测试拿着安卓机过来跟我说,上传照片时原图太卡,服务端还报什么413文件太大。我打开自己iPhone试了下,并没问题。后来才发现,小程序里传图会自动压缩,但App端和H5端拿到的是本地原路径,压根没压过。
那会儿其实挺尴尬的,同一个uni.chooseImage接口,三端行为竟然不一样。网上搜了一圈,有人说用uni.compressImage,有人说canvas画一遍再导出,还有人说直接改后端限制。最后决定自己封装一套通用的压缩方案,在H5、App和微信小程序上都能稳定跑。
先搞清楚各端的图片路径差异
uniapp开发里,使用chooseImage拿到的tempFilePaths看起来就是一个本地路径,但不同平台区别很大:
- 微信小程序:拿到的是“本地临时文件”,可以直接传给uni.compressImage用。
- App端(HBuilderX基座):可能是file:///开头的绝对路径,也可以用uni.compressImage,不过有些Android机型压缩后还是很大。
- H5端:拿到的是一个blob或object URL,根本没法用uni.compressImage(官方说H5不支持)。
所以核心痛点是怎么做到一套代码三端统一处理。我最终用的是canvas重绘方案。它有一个额外的好处:能把图片按照需要的大边长等比缩放,同时还能通过导出参数控制图片质量,压缩效果比uni.compressImage更可控。
先设计一个完整的压缩函数
这个函数接收一个本地路径,返回一个压缩后的新路径。在H5端返回的是base64或blob URL,在小程序/App端返回一个临时文件路径。为了保持后续调用一致,可以统一返回一个临时路径。
先把代码贴出来,然后逐段解释。
function compressImage(src, opts = {}) {
const maxWidth = opts.maxWidth || 1280;
const quality = opts.quality || 0.7;
const outputType = opts.outputType || 'jpg';
return new Promise((resolve, reject) => {
// #ifdef H5
compressOnH5(src, maxWidth, quality, outputType).then(resolve).catch(reject);
// #endif
// #ifndef H5
compressOnNative(src, maxWidth, quality, outputType).then(resolve).catch(reject);
// #endif
});
}
下面分别实现两个平台的处理逻辑。
H5端的canvas压缩实现
H5端有个好处,浏览器里Image对象可以直接加载blob或object URL。我们只需要把Image画到canvas上,然后调用canvas.toDataURL()或canvas.toBlob()生成新图。
function compressOnH5(src, maxWidth, quality, outputType) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function () {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
let { width, height } = img;
if (width > maxWidth) {
height = height * (maxWidth / width);
width = maxWidth;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
const mime = outputType === 'png' ? 'image/png' : 'image/jpeg';
if (canvas.toBlob) {
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error('canvas.toBlob 生成失败'));
return;
}
const compressedUrl = URL.createObjectURL(blob);
resolve(compressedUrl);
}, mime, quality);
} else {
// 老浏览器回退用 toDataURL
resolve(canvas.toDataURL(mime, quality));
}
};
img.onerror = () => reject(new Error('图片加载失败'));
img.src = src;
});
}
这里有个注意点:H5端如果src是blob:http地址,直接给Image.src是没问题的。如果传过来的是base64,也没问题。
另外,H5端压缩后得到的blob URL,要记得在文件上传完成后用URL.revokeObjectURL释放掉,不然会占内存。
App端和小程序端的canvas压缩
在App端和小程序里,不能直接操作HTML Image对象,但uniapp提供了uni.createImageBitmap和uni.createCanvasContext(小程序)以及plus.io(App)。我一开始被这个搞晕了,差点写了两套。
经过测试,App端其实是支持uni.getImageInfo,并且也支持canvas的。但canvas的使用方式和H5不太一样,所以只能用uniapp的api来画图。
分享一个我在实际项目里的做法,使用uni.getImageInfo获取图片尺寸,然后用uni.createOffscreenCanvas(App端支持,小程序基础库2.16.1+也支持)绘制。
function compressOnNative(src, maxWidth, quality, outputType) {
return new Promise(async (resolve, reject) => {
try {
const info = await uni.getImageInfo({ src });
let { width, height } = info;
if (width > maxWidth) {
height = height * (maxWidth / width);
width = maxWidth;
}
// 使用离屏canvas,避免影响当前页面
const canvas = uni.createOffscreenCanvas({ type: '2d' });
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const img = await uni.createImageBitmap(info.path);
ctx.drawImage(img, 0, 0, width, height);
// 决定输出格式
const format = outputType === 'png' ? 'png' : 'jpg';
const qualityNum = quality;
// 对于App端和部分小程序,canvas.toDataURL 可能不支持
// 我们改用 canvasToTempFilePath 来导出图片路径
await uni.canvasToTempFilePath({
canvas,
destWidth: width,
destHeight: height,
fileType: format,
quality: qualityNum,
success: (res) => {
resolve(res.tempFilePath);
},
fail: (err) => reject(err)
});
} catch (err) {
reject(err);
}
});
}
先别急着复制,这段代码在实际运行中会踩几个坑。
坑1:uni.createImageBitmap可能不存在
在某些webview版本较老的App端,uni.createImageBitmap是undefine。如果遇到这个情况,可以用uni.getImageInfo后再用canvas的drawImage。但你得先把图片加载到canvas里。传统写法是使用HTML Image,但在非H5环境下不行。
我最后用的老办法:先获取Image对象,不过这里的Image不是window.Image,而是plus.io.resolveLocalFileSystemURL读取出来的路径。这会让代码复杂很多。
所以后来我干脆换了一种思路:先用uni.compressImage压缩一次,如果压缩后还不满足大小要求,再用canvas兜底。这样其实更保险。不过呢,如果你只是想要一个能跑的通用方案,上面的离屏canvas在微信开发者工具和真机上大部分都能工作。
坑2:canvasToTempFilePath需要导出临时文件
在小程序里,canvasToTempFilePath可以在canvas组件中使用,但如果创建的是离屏canvas,需要传入canvas参数(就是我上面那样传)。如果你用的是老的canvas id方式,还要传canvasId。这个细节容易报错。
另外,App端对canvasToTempFilePath不支持canvas对象参数,只支持组件id。所以上面的代码在App端可能失效。为了解决App端的兼容,我有一个替代方案:直接用plus.zip.compressImage。这个API可以压缩图片并返回新的路径。
实战中我最终的封装思路
索性不追求一个函数吃遍所有平台。我写了一个可配置的方法,内部根据平台分支处理,每个平台用自己最可靠的方式。
- H5:用canvas.toBlob
- 小程序:用canva画布导出tempFilePath
- App:优先用plus.zip.compressImage,如果失败再用canvas兜底
这样写虽然代码长一点,但每个平台都是稳定的。说一下plus.zip怎么用:
function compressByPlus(src, maxWidth, quality) {
return new Promise((resolve, reject) => {
// 先用uni.compressImage压一次质量
uni.compressImage({
src,
quality: Math.round(quality * 100),
success(res) {
const compressedPath = res.tempFilePath;
// 再判断尺寸是否超过 maxWidth,如果超过再用 plus.zip
uni.getImageInfo({
src: compressedPath,
success(info) {
if (info.width > maxWidth) {
plus.zip.compressImage({
src: compressedPath,
dst: compressedPath + '_resize.jpg',
width: maxWidth,
height: maxWidth * (info.height / info.width),
quality: Math.round(quality * 100),
overwrite: true
}, resolve, reject);
} else {
resolve(compressedPath);
}
},
fail: reject
});
},
fail: reject
});
});
}
这样在App端先做质量压缩,再做尺寸缩小,效果很好。关键是plus.zip只有App能用,所以我的封装函数里加了条件编译。
把压缩函数封装成可复用的uploadImage方法
接下来把它整理成模块化,以后不管在哪个页面调用都方便。
function uploadImage(chooseOptions = {}, compressOptions = {}) {
return new Promise((resolve, reject) => {
uni.chooseImage({
count: 1,
...chooseOptions,
success: async (res) => {
const tempFilePath = res.tempFilePaths[0];
try {
const finalPath = await compressImage(tempFilePath, compressOptions);
// 接下来你可以用 uni.uploadFile 上传 finalPath
resolve(finalPath);
} catch (err) {
reject(err);
}
},
fail: reject
});
});
}
使用时只需要:
const filePath = await uploadImage({ count: 1 }, { maxWidth: 800 });
拿到filePath之后,再传给uni.uploadFile即可。
上传时还要注意的几个细节
压缩后的图片如果是一个blob URL(H5端),uni.uploadFile可以直接上传吗?实测在H5端uni.uploadFile支持blob路径。小程序端如果返回的是临时路径也没问题。
如果你在H5端返回的是base64,那需要转成blob再上传。我为了偷懒,返回blob URL。
还有一个小经验:压缩后的图片质量不要调太低,0.7左右在手机上看清晰度几乎没差别,但体积能减少70%以上。如果把长边限制在1280px,头像和普通照片都够用。
写在最后
多端开发的乐趣就在这里,很多api表面上是同样的名字,底下的实现却千差万别。踩过一次坑之后,我意识到不能只知道某个工具能干什么,还要知道它在哪个平台不能干什么。
这套压缩方案已经在我的项目里跑了一个多月,三端都没再出过大图上传的问题。如果你们也有类似的图片上传功能,可以直接拿过去用。但建议你在自己项目里多做几个真机测试,特别是Android低端机和iOS旧版本,canvas的导出格式可能会略有不同。
技术的本质是解决问题,如果你也碰到过uniapp图片上传的诡异差异,欢迎在评论区分享你的坑,也让我学学更好的法子。

