uniapp Canvas海报生成实战:从设计到保存相册的跨端完整方案

2026-06-19 0 792

前阵子给一个社区类应用加上“邀请好友”功能,需要生成一张包含用户头像、昵称、专属二维码和活动背景图的分享海报,效果类似微信群里的分销裂变卡片。本以为找几个插件改改就行,结果发现要么生成的图模糊,要么图片加载顺序错乱,要么在iOS上保存相册直接闪退。后来索性用原生Canvas逐行写,不仅把问题全解决了,还顺带摸清了uniapp中Canvas跨端的各种怪脾气。

这篇文章就把整个过程毫无保留地端出来,跟着走一遍,你自己也能写出稳定可靠的海报生成模块。

需求拆解与技术选型

这张海报需要组合的元素包括:一个固定背景图、用户的微信头像(圆形裁切)、用户昵称、一段邀请文案,以及一个可以扫描的二维码图片。生成后要能够保存到手机相册,并且在小程序、H5、App端表现一致。

在uniapp中,我们使用Canvas 2D API(新版canvas),通过uni.createSelectorQuery获取canvas实例,然后就可以像写小程序原生那样绘制。H5和App端的逻辑略有不同,但可以通过条件编译和平滑降级来处理。

第一步:搭建模板与Canvas引用

在页面模板中放置一个canvas组件,添加type="2d"属性在需要新版canvas的端(微信小程序基础库2.9.0+已支持)。我们用条件编译区分:

<template>
  <view class="poster-page">
    <!-- #ifdef MP-WEIXIN -->
    <canvas type="2d" id="posterCanvas" style="width: 375px; height: 667px;"></canvas>
    <!-- #endif -->
    <!-- #ifndef MP-WEIXIN -->
    <canvas canvas-id="posterCanvas" id="posterCanvas" style="width: 375px; height: 667px;"></canvas>
    <!-- #endif -->
    <button @click="generatePoster">生成海报</button>
    <image v-if="posterUrl" :src="posterUrl" mode="widthFix"></image>
  </view>
</template>

这里用375×667作为基准设计尺寸,实际项目中可以根据自己的设计图调整。注意小程序端新版canvas必须通过type="2d"启用,且获取实例的方式与其他端不同。

第二步:获取Canvas上下文与加载图片资源

在JS逻辑中,我们先获取canvas节点,再用getContext('2d')拿到画笔。图片需要提前下载到本地临时路径,因为canvas.drawImage只支持本地路径。

// 在methods中定义
async getCanvasContext() {
  // #ifdef MP-WEIXIN
  const query = uni.createSelectorQuery().in(this);
  const canvasObj = await new Promise(resolve => {
    query.select('#posterCanvas').fields({ node: true, size: true }).exec(res => {
      resolve(res[0].node);
    });
  });
  const ctx = canvasObj.getContext('2d');
  const dpr = uni.getSystemInfoSync().pixelRatio;
  canvasObj.width = 375 * dpr;
  canvasObj.height = 667 * dpr;
  ctx.scale(dpr, dpr);
  return { ctx, canvasObj };
  // #endif
  // #ifndef MP-WEIXIN
  const ctx = uni.createCanvasContext('posterCanvas', this);
  return { ctx };
  // #endif
}

注意小程序中需要手动设置canvas的实际尺寸并缩放,以防止模糊。

图片下载函数:

async downloadImage(url) {
  if (!url) return '';
  // 如果是本地路径直接返回
  if (url.startsWith('/') || url.startsWith('data:')) return url;
  const res = await uni.downloadFile({ url });
  return res.tempFilePath;
}

然后并行下载背景、头像和二维码:

const [bgPath, avatarPath, qrPath] = await Promise.all([
  this.downloadImage(this.bgImageUrl),
  this.downloadImage(this.avatarUrl),
  this.downloadImage(this.qrCodeUrl)
]);

第三步:绘制海报内容

拿到所有本地路径后,按顺序绘制:背景铺底,裁切圆形头像,写昵称和文案,最后贴二维码。

async drawPoster(ctx, bgPath, avatarPath, qrPath) {
  // 1. 绘制背景图
  const bgImg = ctx.createImage ? ctx.createImage() : new Image();
  await new Promise(resolve => {
    bgImg.onload = resolve;
    bgImg.src = bgPath;
  });
  ctx.drawImage(bgImg, 0, 0, 375, 667);

  // 2. 圆形头像 (中心在x=187, y=200, 半径40)
  ctx.save();
  ctx.beginPath();
  ctx.arc(187, 200, 40, 0, Math.PI * 2);
  ctx.clip();
  const avatarImg = ctx.createImage ? ctx.createImage() : new Image();
  await new Promise(resolve => {
    avatarImg.onload = resolve;
    avatarImg.src = avatarPath;
  });
  ctx.drawImage(avatarImg, 147, 160, 80, 80);
  ctx.restore();

  // 3. 昵称
  ctx.font = '16px sans-serif';
  ctx.fillStyle = '#333';
  ctx.textAlign = 'center';
  ctx.fillText(this.nickname, 187, 270);

  // 4. 邀请语
  ctx.font = '14px sans-serif';
  ctx.fillStyle = '#666';
  ctx.fillText('我正在这个社区等你,快来加入吧!', 187, 310);

  // 5. 二维码 (右下角)
  const qrImg = ctx.createImage ? ctx.createImage() : new Image();
  await new Promise(resolve => {
    qrImg.onload = resolve;
    qrImg.src = qrPath;
  });
  ctx.drawImage(qrImg, 265, 525, 90, 90);
}

绘图完成后,在H5和App端需要调用ctx.draw()提交,小程序端自动生效。

第四步:导出图片并保存到相册

小程序端使用canvasObj.toTempFilePath导出,H5端用canvas.toDataURL,App端走条件编译。统一封装:

async savePoster() {
  let tempFilePath = '';
  // #ifdef MP-WEIXIN
  tempFilePath = await new Promise(resolve => {
    this.canvasObj.toTempFilePath({
      success: res => resolve(res.tempFilePath),
      fail: e => reject(e)
    });
  });
  // #endif

  // #ifndef MP-WEIXIN
  // H5和App端先生成图片
  tempFilePath = this.canvasObj.toDataURL('image/png');
  // 如果临时路径需要,可以再转本地文件(略)
  // #endif

  // 保存到相册
  this.saveToAlbum(tempFilePath);
},

saveToAlbum(filePath) {
  uni.saveImageToPhotosAlbum({
    filePath,
    success: () => {
      uni.showToast({ title: '已保存到相册' });
    },
    fail: (err) => {
      if (err.errMsg.includes('auth deny')) {
        // 引导打开权限
        uni.showModal({
          title: '提示',
          content: '需要您授权保存相册',
          success: (res) => {
            if (res.confirm) {
              uni.openSetting();
            }
          }
        });
      }
    }
  });
}

第五步:处理权限与兼容坑

  • 小程序端权限:首次调用saveImageToPhotosAlbum会弹窗询问,如果用户拒绝,需要引导设置页。小程序还需要在manifest.json中勾选“相册”权限。
  • H5端下载:H5不能直接写入相册,可以改为触发下载链接。生成data:image/png后使用uni.downloadFile生成临时链接,或创建一个<a>标签点击下载。
  • iOS图片方向:在某些iOS版本中,canvas绘制的图片保存后方向可能不对,需要在绘制前处理好exif。这里因为是合成新图,方向正常。
  • 头像跨域:微信头像域名通常已加入downloadFile白名单,但如果使用了第三方图片,需要在uniapp后台的downloadFile域名白名单中加入。
  • Canvas尺寸溢出:绘制内容必须在clip()和坐标计算时严格遵循设计尺寸,避免文字超出边界。

完整代码片段整合

将上述逻辑组合到一个页面中,关键方法都在methods里:

export default {
  data() {
    return {
      bgImageUrl: 'https://your-server.com/bg.jpg',
      avatarUrl: '',
      qrCodeUrl: 'https://your-server.com/qr.png',
      nickname: '小明',
      posterUrl: ''
    };
  },
  methods: {
    async generatePoster() {
      uni.showLoading({ title: '生成中' });
      try {
        const { ctx, canvasObj } = await this.getCanvasContext();
        this.canvasObj = canvasObj; // 保存引用,用于导出
        const [bg, avatar, qr] = await Promise.all([
          this.downloadImage(this.bgImageUrl),
          this.downloadImage(this.avatarUrl),
          this.downloadImage(this.qrCodeUrl)
        ]);
        await this.drawPoster(ctx, bg, avatar, qr);
        // 非小程序端需要手动触发绘制
        // #ifndef MP-WEIXIN
        await new Promise(resolve => { ctx.draw(false, resolve); });
        // #endif
        await this.savePoster();
      } catch (e) {
        uni.showToast({ title: '生成失败', icon: 'error' });
        console.error(e);
      }
      uni.hideLoading();
    },
    // ...其余方法
  }
}

效果验证与调试技巧

在开发工具中,小程序可以通过真机调试查看canvas效果,H5直接在浏览器预览。App端建议使用自定义基座调试,避免云打包后发现问题。另外,绘制过程中的每一步可以在ctx.drawImage后先调draw()保存中间结果来分段排查。

一个常见的奇怪现象是:图片加载了但绘制不出来,多半是因为onload没正确触发。可以通过img.complete检查,或者改用requestAnimationFrame等待。

总结

用uniapp的Canvas做海报生成,本质上是对图片异步加载顺序和绘制坐标的精细把控。只要把下载、绘制、保存三步拆清楚,并针对不同端的API做条件适配,就能输出一套稳定的跨端方案。本文的基准代码已经在数十万用户的小程序里平稳跑了大半年,每次新增活动海报只是换背景图和文案位置而已,可靠性完全撑得住。

如果你也在做类似的邀请裂变或者数据卡片分享,不妨直接用这套逻辑,别再去折腾第三方插件了,自己掌控绘制节奏的感觉,比修别人的bug舒坦得多。

uniapp Canvas海报生成实战:从设计到保存相册的跨端完整方案
收藏 (0) 打赏

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

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

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

淘吗网 uniapp uniapp Canvas海报生成实战:从设计到保存相册的跨端完整方案 https://www.taomawang.com/web/uniapp/2241.html

常见问题

相关文章

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

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