☰
小程序前端用 Canvas 直接生成二维码与海报的完整实践与避坑指南
2026/10/15 13:30:57 网站建设 项目流程

1. 我为什么放弃后端接口,坚持在小程序前端直接出二维码

先交代一下背景。我之前在某个微信小程序项目里做分享和营销功能,第一版图省事,二维码全部走后端生成:前端传一个参数,后端画一张二维码图片返回,前端再展示或者保存。听起来没啥问题,实际上项目一跑起来就发现了几个非常别扭的点。

首先是响应速度。用户点了"保存二维码",前端先发起请求,后端排队、生成、返回,运气好一两百毫秒,运气差走一秒钟以上,用户在手机上明显能感觉到"卡"一下。生成频率一高,服务器还要额外承担这部分计算压力,虽说单张二维码计算量不大,但营销活动一爆量,后端日志里这一块请求数量非常可观。

其次是定制化太难受。后端生成的是静态图片,前端想把二维码换个颜色、换个 logo、加个背景图,基本上每次都要重新发一个请求,交互过程中的临场感完全没了。前端虽然拿到了图片,本质还是"后端给什么就用什么",谈不上灵活的二次加工。 于是我把方案整体调整成:前端拿到内容数据之后,直接在小程序内部用 canvas 绘制二维码,绘制完成后导出成图片,再引导用户保存到相册。整套流程不依赖服务端渲染,海报合成也是一样的思路——先把背景图、头像、昵称、二维码全部画在同一张画布上,最后统一导出。这个方案上线后稳定运行,响应快、可控性强,这篇文章就是把完整实现思路和踩坑过程原原本本记录下来。 适用范围说清楚:如果你是个人小程序、企业小程序,或者任何需要用微信生态做分享裂变、活动海报的场景,这篇文章里的方案都可以直接迁移。就算你不做海报,只需要一个"保存二维码到相册"的功能,文章里从绘制到授权保存的全链路也能直接照抄。

2. 二维码生成库的选型思路:三个常用库到底有什么区别

做前端生成二维码,第一反应就是选一个"能在小程序里跑的二维码生成库"。市面上的方案确实不少,但我把常用的三个放一起比过之后发现,它们之间的差别并没有想象中那么大——底层算法都是通用的 QR Code 编解码,只是暴露给使用者的形态不太一样。

2.1 库的能力边界:核心算法一致,包装不同

先说结论:任何做前端二维码生成的库,本质都是拿一段字符串(链接、文本、JSON),通过 QR 编码算法算出一堆黑白模块的分布数据,然后把这些模块画到 canvas 上。算法本身是公开且成熟的东西,市面上流传的都是同源或同思路的实现,所以"能不能准确生成二维码"这件事,三个库基本没有差别。

它们的差别主要体现在三处:

  • 使用形态:有的库给你一个函数,你调用后拿到二维码绘制能力;有的库直接封装成小程序组件,你往组件里塞参数就行;
  • 对齐的 canvas 版本:有些库基于旧的wx.createCanvasContext接口,有些库则针对新版 Canvas 2D 接口做了适配;
  • 开放程度:有的库只支持绘制二维码本身,有的库额外提供 logo 插入、颜色自定义、边框控制等功能。

2.2 三个库的实际使用体验对比

我整理了一张对比表,方便你照着选:

对比项库 A库 B库 C
使用形态npm 包,纯函数npm 包,支持组件式小组件方案,模板式引入
底层 canvas 接口新版 Canvas 2D 与旧版均有适配新版 Canvas 2D 为主旧版wx.createCanvasContext为主
自定义颜色支持支持支持
绘制 logo需自己二次绘制支持,但埋得比较深暂未内置,需自己处理
体积中等中等偏小较小

我自己实际跑下来,最省心的还是库 A。原因是我后续要做海报合成,需要精确控制"二维码出现在画布哪个位置、大小为多少、周围留多少白边",而库 A 以函数形式暴露的绘制逻辑让我能把二维码绘制动作嵌入到整张海报的 canvas 绘画流程里,自由度最高。库 B 虽然组件化使用方便,但到后期我需要在同一个 canvas 上叠加背景图和文字,再嵌套一个组件去绘制二维码,反而绕。库 C 则因为基于旧版 canvas 接口,在真机上遇到了一些兼容性问题,后面换了。

2.3 一个扎心的建议:别急着从众

如果你去搜这类教程,会发现评论区经常有人说"某某库就是个坑,赶紧换另一个",实际上我对比完发现很多是场景不匹配导致的,不是库本身的错。所以我建议按这个逻辑挑工具:

  • 只做简单保存二维码功能,不在乎画布复杂度 → 库 B 组件化最快,两行代码跑通;
  • 要做海报、要叠加背景和文字,且想灵活控制绘制坐标 → 用函数式的库 A;
  • 只是临时应急、不打算长期维护 → 找最小体积的现成 demo 复制。

工具永远为场景服务,这是我做完项目之后最大的感受。选错库不是最可怕的,最可怕的是你选库的时候根本没想清楚自己的绘制需求,结果功能做到一半发现封装好的组件无法满足你的叠加需求,被迫中途换方案。

3. 从零开始绘制:二维码上 canvas 的完整代码与坐标换算

库定下来之后,重头戏是绘制本身。这块内容用旧版 canvas 接口和新版 Canvas 2D 接口差异巨大,我一开始用的旧接口在部分安卓机上表现不稳定,后来统一切到了新版 Canvas 2D,所以下面所有代码演示都以新版接口为准。

3.1 模板结构:先搭好画布

新版 Canvas 2D 在 WXML 里的写法跟旧版不一样,旧版是<canvas canvas-id="qrCanvas">,新版则是<canvas type="2d">,而且不再使用canvas-id去定位,而是通过id配合wx.createSelectorQuery()来获取节点。这一点坑过很多人,我这里直接把这部分写清楚。

实操中我的 WXML 长这样:

<view class="canvas-wrapper"> <canvas type="2d" id="shareCanvas" style="width: 300px; height: 300px;"></canvas> </view>

注意这里有几个细节:

  • width和height一定要显式写在 style 里,如果你只写class而 class 里用了rpx单位,后面获取节点计算尺寸时会拿到错误的值。我建议直接用 px 写死画布的逻辑尺寸。
  • 无需给 canvas 设置canvas-id,type="2d"就代表走新接口。
  • 这个 canvas 在页面上可以是隐藏的,比如用position: fixed; left: -9999px把它移出可视区域,也可以用一个半透明蒙层先把用户看到的"预览图"做出来再做导出。我个人推荐:预览和最终导出共用一张画布,不要在视觉层额外套一层 image,避免两套状态维护的心智负担。

3.2 获取画布节点与像素比处理

这是最关键的一段,很多人画出来的二维码发虚、不清楚,十有八九是像素比(dpr)没处理好。逻辑上可以这样理解:CSS 层面规定一个 300px 见方的画布,但物理屏幕上每个 CSS 像素对应多个物理像素。如果你只按 300×300 去绘制,再放大显示在手机视网膜屏幕上,就会模糊。

所以正确做法是:让 canvas 的物理尺寸等于逻辑尺寸乘以设备像素比,绘制时再把坐标系整体放大 dpr 倍,所有绘制操作使用的坐标仍然按逻辑像素来写。代码如下:

const query = wx.createSelectorQuery(); query.select('#shareCanvas') .fields({ node: true, size: true }) .exec((res) => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = wx.getSystemInfoSync().pixelRatio; canvas.width = res[0].width * dpr; canvas.height = res[0].height * dpr; ctx.scale(dpr, dpr); // 到这里,ctx 的坐标系统就和 CSS 像素保持一致了 drawQRCode(ctx, res[0].width, res[0].height); });

wx.getSystemInfoSync()在部分新版本基础库上标记为废弃,但实测在绝大多数线上版本仍然可用,如果你的项目兼容到的微信版本足够新,可以换成从wx.getWindowInfo()里取pixelRatio。

3.3 绘制二维码时的比例与白边

二维码本身有"安静区"的概念——就是二维码四周那圈空白,没有这圈空白,很多扫码设备在识别时容易出错。所以绘制二维码不能把模块贴满整张画布,一定要留边。

我实践中的一个取值习惯是:把二维码内容区(模块矩阵)控制在画布边长的 80% 左右,四周各留 10%。也就是说,假设画布宽 300px,二维码实际绘制区域的宽高为 240px,左上角起始坐标为 30px。

库的正确调用方式我贴一下,这里以某函数式库的 API 为例:

function drawQRCode(ctx, width, height, content) { const margin = width * 0.1; const qrSize = width * 0.8; // 调用库的绘制方法,把二维码画到 ctx 上 // 具体库的命名各有不同,但核心参数基本是:ctx、内容、绘制区域尺寸、边距、颜色 qrApi.draw(ctx, content, { size: qrSize, x: margin, y: margin, fore: '#000000', back: '#ffffff', }); }

这里啰嗦一句:白边必须画成不透明的白色。否则透明背景的二维码在某些深色海报底图上会被背景色渗透,扫码率大幅下降。我一开始偷懒没画背景色块,海报底图是深蓝色,结果有三分之一的用户反馈"扫不出来",排查了半小时才定位到是这个原因。加上白色背景之后,扫码成功率立刻回到正常水平。

3.4 canvas 绘制完成后如何确认画完了

新版 Canvas 2D 接口中,绘制动作通常是同步入队的,但canvasToTempFilePath(后面会讲到)必须在绘制完成之后调用。最稳妥的做法是用canvas.requestAnimationFrame或setTimeout拉一小段时延,让渲染管线处理完入队的绘制命令。我习惯用setTimeout延迟 300ms 后再导出,虽然 60-100ms 通常也够,但留点余量省得在低端安卓机上偶发空白。

4. 导出图片与保存相册:授权被拒时的完整处理链路

二维码画好了,下一步是导出成图片文件并保存到用户相册。这段是整个流程中交互细节最多的地方,处理不好,用户会卡在"保存失败"的弹窗里,而你还不知道问题出在授权这一层。

4.1 先导出 toTempFilePath

新版 Canvas 2D 接口对应的方法是canvas.toTempFilePath(),注意它跟旧版的wx.canvasToTempFilePath在调用方式上有区别。新版是 node 实例上的方法,需要先在 selector 查询里拿到 canvas node,然后调用:

canvas.toTempFilePath({ destWidth: canvas.width, destHeight: canvas.height, fileType: 'png', quality: 1, success(res) { // res.tempFilePath 就是临时图片路径 // 可以用于 <image> 预览,也可以直接传给保存 API }, fail(err) { console.error('导出失败', err); }, });

这里有个注意点:destWidth和destHeight我填的是 canvas 的物理宽高(即包含 dpr 的尺寸),而不是逻辑尺寸。这样才能保证导出的图片是高清的,而不是按 CSS 逻辑尺寸导出后发虚。fileType用png,因为二维码和海报里如果存在细小线条,jpg压缩会出现明显的锯齿和色斑,识别率受影响。如果强制要求 jpg 格式,质量参数至少给到 0.9 以上。

4.2 保存到相册的标准流程

导出成功后,tempFilePath是一张临时图片,还不能算保存到用户相册。需要调用wx.saveImageToPhotosAlbum把它真正写入系统相册。但saveImageToPhotosAlbum有一个前置条件:用户必须已授权scope.writePhotosAlbum权限。所以一个完整的流程是:

  1. 先wx.getSetting查一下当前授权状态;
  2. 如果已经授权,直接保存;
  3. 如果未授权,调用wx.authorize发起授权弹窗;
  4. 如果用户之前拒绝过,wx.authorize会直接走 fail,此时引导用户去wx.openSetting手动打开开关。

代码链路我写成这样:

function saveToAlbum(filePath) { wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { wx.saveImageToPhotosAlbum({ filePath, success: () => wx.showToast({ title: '保存成功' }), fail: (err) => showSaveError(err), }); } else if (res.authSetting['scope.writePhotosAlbum'] === undefined) { // 用户从未选择过,可以先弹授权 wx.authorize({ scope: 'scope.writePhotosAlbum', success: () => { wx.saveImageToPhotosAlbum({ filePath }); }, fail: () => showGuideModal(), }); } else { // 用户之前拒绝过,直接引导去设置 showGuideModal(); } }, }); }

4.3 授权拒绝后的引导设计,真的别省

showGuideModal()这个函数是我非常想强调的点。很多开发者在authorize的 fail 回调里直接弹一个 toast"保存失败",用户在完全没有概念的情况下就被拒绝了,后续再也不愿意点第二次。

我用的引导方案是:弹一个自定义 modal,文案写明"需要相册权限才能保存二维码,请在设置中开启",按钮上写"去设置"。点击后调用wx.openSetting,打开的小程序设置页里就有"添加到相册"这一项开关。用户打开开关后回到页面,再自动触发一次保存逻辑。代码大概这样:

function showGuideModal() { wx.showModal({ title: '需要相册权限', content: '保存二维码需要访问你的相册,请在设置中开启权限', confirmText: '去设置', success(res) { if (res.confirm) { wx.openSetting({ success: (settingRes) => { if (settingRes.authSetting['scope.writePhotosAlbum']) { // 用户开完权限,重新保存 retrySave(); } }, }); } }, }); }

4.4 iOS 和安卓在授权上的实际差异

这里有个隐蔽差异:wx.openSetting打开的是微信小程序自身的设置页,它能控制的是小程序对用户授予的"虚拟权限"。在 iOS 上,如果用户之前在系统设置里直接关闭了微信的相册权限,那么小程序的openSetting页面无法绕过系统限制,用户必须去真正的系统设置里打开微信相册权限。安卓在这块相对宽容,一般小程序内授权通过就能正常保存。

所以如果你的上线用户里有相当比例 iOS 玩家,字体处理这里要格外小心。这带来的实际问题就是:当用户反馈保存失败时,弹窗文案里要区分"微信设置页能解决"和"需要去系统设置"两种场景。我目前的做法是在失败回调里检测err.errMsg里是否包含auth denied字样,再结合系统判断用户有没有可能被系统层拦截,文案会提示"若小程序设置页中没有相册开关,请去手机系统设置里允许微信访问相册"。这一句提示在真实用户服务中至少能解决四分之一的疑难反馈。

5. 把海报说清楚:背景图、文字、二维码的画布分层方案

标题里说"海报同理",意思是海报的生成逻辑和二维码完全一致,只是画布上多画了几层内容。这一节我把海报场景完整拆开讲,因为它比单纯画二维码更典型,也更容易踩坑。

5.1 网络图片必须转本地路径:最容易被忽视的一步

画海报的第一步是画背景图,背景图通常是一张网络图片。如果你直接把https://...地址塞给 canvas 的drawImage,在小程序里是画不出来的——canvas 默认不允许直接绘制网络图片,必须先把网络图片下载到本地,转成一个临时文件路径,才能绘制。

完成这一步的方法是wx.getImageInfo:

function loadImage(url) { return new Promise((resolve, reject) => { wx.getImageInfo({ src: url, success: (res) => resolve(res.path), fail: reject, }); }); }

注意:拿到res.path只是第一步,drawImage还需要知道这张图片的宽度和高度,否则无法正确按比例绘制。所以我在loadImage里还会把res.width和res.height一并返回,这样绘制时可以精确计算横纵比例。这个方法有两个天然属性:会读取图片的wxfile://临时目录,也意味着图片一旦绘制成功,临时文件在本次会话内是稳定的,不会画到一半失效。

5.2 绘制顺序决定最终效果:背景、元素、文字

海报的本质是一张合成图,绘制顺序就是图层的从底到顶。以我做的活动海报为例,绘制顺序是这样的:

  1. 背景图:铺满整张画布。这里有个细节,不能盲目拉伸铺满,如果背景图比例和画布比例不一致,拉伸后会变形。我的做法是「覆盖填充」:按画布比例裁剪图片,多出来的部分裁掉。这需要一段简单的比例计算,后续我在代码里给出。
  2. 半透明遮罩:很多海报会在背景上加一层半透明黑色,让前景内容更清晰。这个就是ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'然后fillRect整个画布,一气呵成。
  3. 装饰元素:比如活动名称底板、品牌 logo、个性头像。头像如果是圆的,需要先画圆形裁剪区域再drawImage。
  4. 二维码:放在海报右下角是常见设计,这里直接把第 3 节画二维码的函数搬过来,传入相应的坐标即可。
  5. 文字:活动标题、副标题、日期、地点等等。绘制文字时注意ctx.textBaseline = 'middle'和ctx.textAlign = 'center'这两个属性,它们决定了文字相对坐标的锚点,不设置的话,文字位置经常比预期偏上或偏下。

5.3 海报绘制代码实例:带覆盖填充的背景图

完整代码我给出一个精简版本,方便直接挪到项目里改改就用:

async function drawPoster(canvas, ctx, width, height, posterData) { // 1. 背景图 const bg = await loadImage(posterData.bgUrl); const bgRatio = bg.width / bg.height; const canvasRatio = width / height; let drawWidth = width; let drawHeight = height; let offsetX = 0; let offsetY = 0; if (bgRatio > canvasRatio) { // 图片偏宽:以高度为基准,宽度超出的部分裁剪 drawHeight = height; drawWidth = height * bgRatio; offsetX = (width - drawWidth) / 2; } else { // 图片偏高:以宽度为基准,高度超出的部分裁剪 drawWidth = width; drawHeight = width / bgRatio; offsetY = (height - drawHeight) / 2; } ctx.drawImage(bg, offsetX, offsetY, drawWidth, drawHeight); // 2. 半透明遮罩 ctx.fillStyle = 'rgba(0, 0, 0, 0.25)'; ctx.fillRect(0, 0, width, height); // 3. 昵称 ctx.fillStyle = '#ffffff'; ctx.font = 'bold 20px sans-serif'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(posterData.nickname, 24, 60); // 4. 二维码(右下角,预留白边) const qrMargin = 24; const qrSize = 120; drawQRCode(ctx, width - qrSize - qrMargin, height - qrSize - qrMargin, qrSize, posterData.qrContent); // 5. 提示文字 ctx.fillStyle = '#ffffff'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('扫码查看详情', width - qrSize / 2 - qrMargin, height - qrSize / 2 - qrMargin + 76); }

绘制完成后再调用第 4 节的canvas.toTempFilePath导出并保存,整体流程完全一致。

5.4 头像裁剪成圆形:小技巧但高频出现

头像这块单独拎出来说,因为做分享海报几乎必用。canvas 默认的drawImage是矩形的,要做圆头像需要先声明圆形裁剪区域:

function drawRoundAvatar(ctx, x, y, radius, avatarPath) { ctx.save(); ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.closePath(); ctx.clip(); ctx.drawImage(avatarPath, x - radius, y - radius, radius * 2, radius * 2); ctx.restore(); }

关键点是ctx.save()和ctx.restore()一对调用。如果不做 save/restore,裁剪区域会残留并影响后续所有绘制内容,比如下一个方形图形会被莫名裁剪。大多数海报绘制出现"某一块不见了、某一块变圆了"的诡异现象,十有八九是裁剪状态没恢复。

6. 我在真实项目里踩过的六个坑,附排查思路

前面内容偏正向流程,但真正让开发时间翻倍的永远是那些隐藏很深的坑。我把整个项目里踩过的坑按「现象-排查过程-根因-修复」的方式记录下来,每一个都值得你在自己的项目里提前规避。

6.1 导出图片全白:绘制命令还没入队就导出了

现象:二维码页面打开后马上点保存,导出的图片是一张纯白图。过几秒再点,又正常。

排查过程:先怀疑canvas.toTempFilePath的参数配错,检查后没问题;再怀疑 selector 查询时机不对,调整后仍偶发;后来在draw之后和toTempFilePath之间打印ctx状态,发现绘制命令确实执行了,但导出时渲染管线还没消费完。

根因:canvas 的绘制和导出是异步管线,绘制命令入队后需要一小段时间才能反映到最终渲染结果里。

修复:在绘制函数完成后,用setTimeout延迟 300ms 再执行导出。如果绘制的是大尺寸海报(超过 1000px 宽),我建议延迟增加到 500ms。

6.2 二维码存在但扫不出来:白边被透明背景腐蚀

现象:海报深色背景下,二维码整体看起来很完整,但扫码时识别极慢,经常"无法识别"。

排查过程:一开始以为是二维码内容太长导致密度太高,后来单独把二维码画在白色画布上,识别立刻恢复正常。对比深色背景和白色背景两种场景,发现问题出在背景色对二维码模块区域的干扰。

根因:二维码的安静区必须是纯色且颜色与模块区对比度足够。我绘制的二维码背景是透明色,深色海报底图从透明区域渗透进来,干扰了解码器的边界识别。

修复:绘制二维码时强制填充白色矩形底,再画模块矩阵。就是在drawQRCode内部先fillStyle = '#ffffff'画一个覆盖二维码区域的矩形。

6.3 同一张画布二次绘制内容残留:clearRect 才是正解

现象:第一次生成的海报没问题,第二次换了一组数据重新生成,旧海报的某些图形残留在新图边缘。

排查过程:一开始以为是 canvas 复用导致状态混乱,尝试销毁重建画布节点,太麻烦且不稳定。后来检查发现是绘制前没有清空画布。

根因:canvas 上的所有绘制是累积的,第二次绘制如果没清空底层,旧内容不会被自动覆盖。特别是透明区域,旧图形会从上一次的位置透出来。

修复:每次绘制海报之前,统一在入口处执行ctx.clearRect(0, 0, width, height)。如果不需要透明底,直接fillRect一个白色底也可以。

6.4 图片导出后发虚:dpr 处理缺失

现象:同一张二维码,页面上预览非常清晰,保存到相册后放大看有锯齿和模糊感。

排查过程:没太怀疑是 canvas 本身的问题,因为预览是清晰的。但保存图是物理像素级输出,我对比了保存图和预览图的尺寸,发现保存图只有 300×300 物理像素,远低于 iPhone 屏幕 750×750 或更高的物理像素需求,所以放大必糊。

根因:canvas 的物理尺寸只设了 CSS 逻辑尺寸(300px),没有乘以dpr。导出的destWidth也用了逻辑尺寸,导致输出分辨率不足。

修复:按 3.2 节的做法,canvas.width = width * dpr,canvas.height = height * dpr,同时ctx.scale(dpr, dpr)。导出时destWidth用canvas.width,destHeight用canvas.height。

6.5 部分安卓机上导出纯黑块:fileType 用错了

现象:安卓某机型保存的海报是一整块黑色,iOS 完全正常。

排查过程:对比日志发现toTempFilePath的 success 回调正常返回了路径,但图片内容异常。后来把fileType从jpg改成png之后问题消失。

根因:在部分安卓 WebView 实现里,canvas 的 alpha 通道与 jpg 压缩格式存在兼容性问题,透明区域被渲染成黑色。

修复:统一使用fileType: 'png',同时避免在海报中使用大面积透明区域。如果你业务上必须输出 jpg,至少要在导出前给画布刷一层纯色背景。

6.6 Canvas 组件在部分低端机上不触发 touch 事件相关异常

现象:用户在页面滚动时偶发"卡顿 + 页面闪烁",尤其在带 canvas 长列表页。

排查过程:用性能面板看渲染耗时,发现 canvas 重绘和列表滚动交织,导致掉帧。后来把 canvas 移出滚动区,放在固定抽屉里,问题消失。

根因:canvas 是一个相对重的渲染单元,长列表滚动时如果 canvas 频繁参与合成,在低端机上会成为瓶颈。

修复:凡是主要用途为"生成图片"的 canvas,一律固定在页面底部或独立抽屉中,不在列表内复用。列表里只显示导出后的临时图片,canvas 只承担绘画职责。

7. 性能、清晰度与体验优化:上线前必须要做的检查清单

代码能跑通只是第一步,真正上线前的性能调优和体验细节,是我在反复折腾后总结出来的,这里按优先级高低列出来。

7.1 大图生成的卡顿如何缓解

海报场景里,很多人把画布尺寸设置成 750×1334(对应设计稿 750px 宽),再乘上 dpr,物理尺寸直接变成 1500×2668 甚至更高。这么大的画布绘制起来非常吃内存,低端机轻则卡顿,重则白屏。

我的做法是把画布逻辑尺寸控制在「够用就好」,屏幕预览和最终图片共用一张画布的方案,在大多数手机上 300px 宽预览足够清晰了。如果你确实需要 750px 宽的高清图,建议用离屏 canvas 分步绘制:第一层画背景,第二层画内容,最后一层合成。碎片化绘制能降低单帧渲染压力,虽然代码上多了一次 canvas 创建,但体验提升明显。

7.2 图片预加载:解决"第一张海报画不出背景"

现象很有代表性:海报功能上线第一天,后台收到几十条反馈,说保存的图片没有背景图,只有文字和二维码。排查之后发现,报告问题的用户基本都是首次打开海报页就立刻点击保存。因为wx.getImageInfo下载网络图片需要时间,首次请求还没返回,drawImage就拿不到合法路径,绘制失败。第二次进入页面时图片有缓存,所以正常。

修复方案是两步:一是从页面初始化就开始预下载背景图,把wx.getImageInfo提前执行;二是在用户点击保存之前,检查背景图资源是否就绪,未就绪时先展示 loading,避免用户过早操作。

7.3 文明引导用户等待:canvas 绘制进度怎么反馈

canvas 绘制过程是不可见的,用户点击"保存海报"后如果等待 1-2 秒没反馈,会觉得应用卡死。我的做法是在按钮点击后立刻弹一个半透明 loading 蒙层,文案写"正在生成海报",然后在toTempFilePath的 success 回调里关掉蒙层并走保存流程。这套交互虽然简单,但实际效果远好于直接调用showLoading然后再换 toast 的处理。

7.4 归档一张自检清单

上线前我习惯对整个链路做一次完整自检,这里干脆把我用的清单也放出来:

检查项通过标准
画布调用链新版 Canvas 2D 接口,id定位,宽高显式声明
dpr 处理canvas 物理尺寸 = 逻辑尺寸 × dpr,绘制前ctx.scale(dpr, dpr)
绘制前清屏每次绘制入口先clearRect或铺底色
网络图片处理全部经wx.getImageInfo转本地临时路径
二维码安静区二维码四周有 10%-15% 的纯色白边/浅色底
导出参数fileType: 'png',destWidth/destHeight取物理尺寸
授权链路getSetting → authorize → openSetting 三段处理完整
双端验证iOS 和至少 3 台不同安卓机型真机验证
首帧体验背景图预加载,绘制前有 loading 反馈
资源释放页面 onUnload 时清理临时文件路径,避免占满缓存

7.5 最后补一个顺手的工具函数

做海报时坐标计算很繁琐,我封装了一个rpx2px函数:设计稿按 750rpx 出图,代码里按屏幕宽度换算成 px。这个函数在多个项目里反复用到,直接分享:

function rpx2px(rpx) { const windowWidth = wx.getSystemInfoSync().windowWidth; return (rpx / 750) * windowWidth; }

因为小程序生态里设计稿基本都是 750px 宽,rpx2px帮我省掉了大量手算工作。你在实际开发中也可以把你的换算公式固定下来,避免每张海报都从头算一遍坐标。说实话,这套方案里最花时间的往往不是 API 本身,而是那些散落在代码角落的像素计算和状态管理细节,多沉淀几个工具函数,后面每个项目都能受益。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询