uniapp图片压缩不丢画质:一套方案搞掂App端、H5端和小程序端

2026-08-08 0 782

上周做版本迭代,测试拿着安卓机过来跟我说,上传照片时原图太卡,服务端还报什么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图片上传的诡异差异,欢迎在评论区分享你的坑,也让我学学更好的法子。

uniapp图片压缩不丢画质:一套方案搞掂App端、H5端和小程序端
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uniapp图片压缩不丢画质:一套方案搞掂App端、H5端和小程序端 https://www.taomawang.com/web/uniapp/2507.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务