ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序分享海报生成:Canvas 2D与二维码合成实践

微信小程序分享海报生成:Canvas 2D与二维码合成实践 简介面向微信小程序开发者的轻量级示例资源围绕“生成带二维码的分享海报”这一高频场景解决用户在小程序内快速制作海报、引导扫码跳转的需求适用于产品推广、活动引流或社交分享等场景适合学习Canvas绘图及微信API的初级开发者。压缩包体积仅4KB共5个文件涵盖wxml页面骨架、wxss样式、js逻辑、json配置及txt说明其中wxml与wxss负责海报展示和样式js承担生成流程的核心逻辑json用于页面配置txt则给出关键接口的使用说明结构清晰便于对照学习。目前已超过2651人学习资源热度印证了其实际参考价值。示例代码完整覆盖了从网络图片下载转为本地文件、借助qrcode库生成二维码、通过canvas绘制合并海报再到调用wx.saveImageToPhotosAlbum保存到相册的完整链路同时txt说明中还给出了关键API的调用要点与错误处理思路。开发者可基于附带的页面模板快速替换素材并集成到实际项目中节省从零搭建的时间也能根据示例扩展更多个性化分享功能。1. 微信小程序生成分享海报核心不在 UI在 Canvas 与二维码合成微信小程序生成分享海报这个需求在电商、活动和裂变页里出现频率极高用户打开小程序点一下生成海报把图和二维码一起存相册、转发给朋友。这份资源就是把「海报绘制」和「二维码生成」打包好的实现后端不参与合成也能跑通完整链路。很多开发者把精力花在搭 UI 上结果真机一跑要么导出白图要么二维码扫不出来要么 iOS 相册保存失败——问题全出在 Canvas 的使用方式和图片加载时机上。适合正在搭营销组件、做用户邀请页的开发者也适合用 uniapp 写小程序、想看看 Canvas 2D 做法的朋友。2. 选对画布Canvas 2D 与旧接口背后藏着海报清晰度与白屏的根因2.1 为什么我不推荐再用旧版绘图接口早期小程序海报基本都是wx.createCanvasContextcanvas canvas-id...这种写法。接口本身不难但有三处硬伤一是绘图指令通过draw()异步提交到渲染层绘制完成的时机很难精确捕捉二是wx.canvasToTempFilePath要传canvasId一旦页面里多个 canvas 就经常取错节点三是它不会按设备像素比自动缩放同样一个 300px 宽的海报在 iPhone 和 Android 上导出的清晰度天差地别。我后来把项目都迁移到了canvas type2d方案。2d 接口直接返回 canvas 节点绘制是同步的导出时能显式控制输出像素尺寸。旧接口也不是完全不能用但涉及到海报这种对清晰度和导出稳定性都有要求的场景2d 是唯一省心的选项。这也是为什么资源包里的完整示例都跑在 2d 接口上。2.2 用 2d 接口搭画布节点获取、dpr 与缩放第一步是拿到 canvas 节点并设置物理尺寸。注意样式尺寸和节点尺寸是两个概念style 里的宽高决定布局占位node.width/node.height决定导出图的真实像素。常见错误是只设置了 style导出图永远是模糊的。canvas type2d idposter stylewidth: 300px; height: 420px;/canvasconst query wx.createSelectorQuery().in(this) query.select(#poster).fields({ node: true, size: true }).exec((res) { const canvas res[0].node const ctx canvas.getContext(2d) // 新基础库用 wx.getWindowInfo老项目还在用 getSystemInfoSync const dpr wx.getWindowInfo ? wx.getWindowInfo().pixelRatio : wx.getSystemInfoSync().pixelRatio canvas.width res[0].width * dpr canvas.height res[0].height * dpr ctx.scale(dpr, dpr) // 画布准备好之后再开始绘制 renderPoster(canvas, ctx) })这段代码有两点要注意。第一fields({ node: true, size: true })是 2d 接口的固定搭配node 是 canvas 实例size 提供样式尺寸两个都要拿。第二ctx.scale(dpr, dpr)之后后面所有drawImage、fillText仍然按 300 x 420 的逻辑尺寸写但导出时每个逻辑像素会对应 dpr 个物理像素图片自然就清晰了。真机上如果发现文字边缘发虚优先检查这一节是不是只设了 style或者 scale 之前就开始了绘制。我遇到过的翻车案例十个里有八个是漏了 dpr 换算。2.3 离屏 canvas 与页面内 canvas海报生成该用哪个页面里直接放一个 canvas 并不可见地绘制有一个隐藏问题它占布局受页面滚动、自定义导航栏偏移影响调试时还容易误触。更稳妥的做法是用离屏 canvas。const canvas wx.createOffscreenCanvas({ type: 2d, width: 300 * dpr, height: 420 * dpr }) const ctx canvas.getContext(2d) ctx.scale(dpr, dpr)离屏 canvas 不挂载在页面上绘制过程用户完全看不到生成完直接拿tempFilePath显示在普通 image 标签里交互上更干净。资源包里的示例默认走这条路线。它的代价是部分基础库版本对离屏 canvas 的自定义字体支持不完整如果海报用了非系统字体需要先wx.loadFontFace再绘制另外createOffscreenCanvas在小程序端的实现和 H5 端有差异跨端项目里要加条件编译。我一般建议是小程序原生项目优先离屏 canvasuniapp 项目为了统一三端还是用页面上挂载的 2d canvas 更省事。2.4 图片素材加载canvas.createImage 与网络图白名单海报背景、用户头像、二维码都要先转成 canvas 能识别的 Image 对象才能 drawImage。2d 接口里创建图片不能用浏览器那套new Image()得用canvas.createImage()。网络图片还有一道坎小程序的 canvas 不允许直接绘制没有配置合法域名的图片。模拟器居然不拦真机才报错这是最容易踩的延时炸弹。我一般先把图片加载封装成一个 Promise 函数保证所有素材就位后再开始画function loadImage(canvas, url) { return new Promise((resolve, reject) { const img canvas.createImage() img.onload () resolve(img) img.onerror () reject(new Error(image load failed: url)) img.src url }) }调用方同时跑多个 loadImage然后用 Promise.all 汇总。好处是绘制函数永远不需要处理“图片还没加载完”的状态。要注意img.src直接放网络地址前先确认地址已经在微信公众平台的 downloadFile 合法域名里本地图片路径、base64 不受这个限制优先级更高。头像这类域名经常变动我习惯先wx.getImageInfo拿本地临时路径再喂给canvas.createImage省得在开发阶段被域名白名单反复折腾。这一节做完画布和素材都就绪了下一节开始真正往画布上画内容。3. 从背景到用户信息一张海报的绘制顺序、裁剪与文字换行实践3.1 绘制顺序就是图层顺序Canvas 本质是即时绘制系统后画的压先画的。海报的正确绘制顺序是背景图 → 用户头像与昵称 → 活动文案 → 二维码。如果先把二维码画上背景图再盖一层生成的图片里就没有二维码了。这个顺序别指望靠 z-index 之类控制canvas 里没有层级概念代码执行顺序就是最终图层顺序。另一个顺序问题是导出时机。导出的命令必须放在所有绘制完成之后尤其是 drawImage 的图片如果还没 onload画布上是空的导出的结果就是白图。合理做法是把整张海报的绘制包成一个函数素材加载、绘制、导出三个环节串成一条链避免在 onload 回调里层层嵌套。3.2 用户头像圆形裁剪的 save / clip / restore 三件套海报里的头像一般要裁成圆形。做法是先画一个圆再 clip然后把头像图片按方形画进去最后 restore 恢复画布状态。function drawAvatar(ctx, avatarImg, x, y, radius) { ctx.save() ctx.beginPath() ctx.arc(x, y, radius, 0, Math.PI * 2) ctx.closePath() ctx.clip() // 头像图片按直径 2 * radius 绘制长方形会被裁掉多余部分 ctx.drawImage(avatarImg, x - radius, y - radius, radius * 2, radius * 2) ctx.restore() }这里最容易被忽略的是 save / restore 配对。clip 之后如果不 restore后面的文字和二维码都会被限制在圆形区域内轻则文字显示不全重则二维码缺角。我整理这份资源时专门把这个坑写进了注释ctx.save()和ctx.restore()必须成对出现被裁剪的区域只影响这一步绘制。参数上radius 不要小于 20逻辑像素否则头像细节全丢。头像图如果本身很小drawImage 放大后边缘会发虚我一般要求上传头像不小于 200px到这步就是纯缩放。3.3 文字处理font 设置与按字符换行小程序 canvas 2d 里设置字号和颜色的方式是ctx.font normal bold 16px sans-serif然后ctx.fillText(text, x, y)。这里有个隐蔽点font 必须在 fillText 前设置每次重新设置都会重置前面的样式颜色用ctx.fillStyle。文字内容超过海报宽度时要手动换行canvas 没有自动换行能力。function wrapText(ctx, text, maxWidth) { const chars text.split() const rows [] let row for (let i 0; i chars.length; i) { const test row chars[i] if (ctx.measureText(test).width maxWidth row) { rows.push(row) row chars[i] } else { row test } } rows.push(row) return rows }measureText在小程序 canvas 2d 里可用返回的是当前 font 下的真实宽度。按字符逐个累加判断比按字节截断靠谱得多中文场景下每个字宽度差不多但遇到数字、英文大小写混排就差异极大。换行函数算出的 rows 数组循环 fillText行高取 fontSize 的 1.4 倍左右比较合适太挤会像被压扁太松又显得海报空洞。3.4 导出与保存canvasToTempFilePath 的参数别乱填绘制完成后先导出临时图片再保存到相册。2d 接口的导出方法和旧接口写法不同必须把 canvas 实例作为参数传入。wx.canvasToTempFilePath({ canvas, x: 0, y: 0, width: 300, height: 420, destWidth: 300 * dpr, destHeight: 420 * dpr, fileType: png, success(res) { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () wx.showToast({ title: 已保存到相册, icon: none }) }) } })参数含义x/y/width/height 是截取画布上哪个区域destWidth/destHeight 是输出图片的像素尺寸。如果只截海报主体就改前四个参数如果想输出 2 倍图给运营做高清素材就调后两个参数。fileType 我固定用 png原因和二维码直接相关jpg 压缩会折损二维码的边缘对比度扫码时容易失败。3.5 rpx 与 px 换算为什么海报尺寸要写 px 而不是 rpx设计稿通常按 750rpx 宽度出但 canvas 2d 的坐标系统只认 px。如果在绘制代码里直接写 rpx真机上不同宽度的设备会得到完全不同的效果。我一般先在工具函数里做一次转换function rpx2px(rpx) { const windowWidth wx.getWindowInfo().windowWidth return rpx / 750 * windowWidth }注意取整问题rpx2px(2)在某些机型上会得到 1.07 这类小数canvas 绘制时四舍五入会导致两张图之间出现 1px 缝隙。背景图用ceil普通元素用Math.round这是我在排查海报细线时发现的血泪经验。到这一步一张带背景、头像、文案的海报已经能存到用户相册。但别忘了二维码还没合成进去这是下一章的重点。4. 二维码生成的两条路线前端画码和后端取码选错会白白丢渠道数据4.1 前端直接画二维码是布尔矩阵在 canvas 上的点阵二维码的本质是一组黑白点阵。成熟的 qrcode 实现会把文本编码成 modules 二维数组每个元素是 true 或 falsetrue 代表黑点。小程序端把这段算法放进包体再用 canvas 逐格绘制就能不依赖后端生成二维码。市面上的 qrcode 实现很多选一个不带 DOM 依赖的版本放进 utils 目录就能用这也是这份资源包“附带二维码生成”的落地方式。function drawQrCode(ctx, qr, x, y, size) { const count qr.modules.length const cell size / count const padding 8 // 白底垫底避免透明背景叠加到海报上 ctx.fillStyle #ffffff ctx.fillRect(x, y, size padding * 2, size padding * 2) ctx.fillStyle #000000 for (let row 0; row count; row) { for (let col 0; col count; col) { if (qr.modules[row][col]) { ctx.fillRect(x padding col * cell, y padding row * cell, cell, cell) } } } }参数上size 指的是二维码有效区域的边长建议不小于 120 逻辑像素我一般用 150。padding 留 8 个点位的白边这是扫码识别的基本要求二维码没有留白很难扫出来。纠错级别选 M 是性价比最高的配置既能容忍部分遮挡画出来的码又不会太密。绘制前如果 qr.modules 还没生成要等文本编码完成再调这个函数否则 canvas 上只会出现一个白框。前端画码的缺点也要说清楚内容就是一段文本或链接扫码后打开的是普通网页或小程序页面但无法携带 scene 参数做渠道归因。如果运营需要区分“A 用户拉来的是 B 还是 C”这个方案做不到。4.2 后端取小程序码wxacode 接口与请求封装另一个常见做法是走后端wxacode.getUnlimited接口拿小程序码。小程序码长得和普通二维码不同本身就能携带 scene 参数扫码直接进入小程序指定页面并且在后台能看到扫码来源。这一步必然涉及请求封装把 access_token 管理、超时、失败重试收敛到一个模块里。function request(opt) { return new Promise((resolve, reject) { wx.request({ url: opt.url, method: opt.method || GET, data: opt.data || {}, header: { content-type: application/json }, success: resolve, fail: reject }) }) } async function getWxCode(scene, page) { const accessToken await getAccessToken() // access_token 由后端下发 const res await request({ url: https://api.weixin.qq.com/wxa/getwxacodeunlimit, method: POST, data: { scene, page, width: 430, check_path: false, env_version: release } }) // res.data 是图片 Buffer转 base64 交给前端绘制 const base64 wx.arrayBufferToBase64(res.data) return data:image/png;base64, base64 }注意 access_token 必须由后端获取并下发appsecret 无论如何不能写进小程序前端代码。scene 最长 32 个可见字符page 指向的页面必须先发布否则接口直接报错。请求封装是这套流程里的标准件——统一处理成功、失败、token 过期重试核心目的是让业务代码不要到处散落 wx.request。前端拿到 base64 图片后还是走 canvas.createImage 加载再 drawImage 到海报的二维码区域。和前端画普通二维码不同小程序码本身就有官方白边绘制时不需要额外加 padding但尺寸仍然建议不小于 120 逻辑像素。4.3 两条路线怎么选这个选择直接影响后续运营数据能不能拿到不是纯技术偏好。对比项前端画普通二维码后端取小程序码是否需要后端不需要需要能否带 scene 参数否是扫码落点普通链接或小程序首页指定页面直接带参渠道归因基本无微信后台可看来源实现成本低中我的建议是单页活动、临时裂变、验证玩法时用前端画码最快跑通需要长期统计拉新数据、给不同渠道用户不同落地页的直接用 getwxacodeunlimit。这份资源包里两条路线的代码都有默认配置走前端画码改一行就能切到后端模式。5. 保存与分享避坑白图、扫码失败、相册授权失灵的五个记录5.1 真机导出是白图模拟器正常现象模拟器里海报显示完美一上真机导出成白图或者只出来半张。原因网络图片还没触发 onload绘制函数就开始执行了。模拟器渲染快图片下载就像本地资源掩盖了竞态真机网络有延迟canvas 在图片到位前已经完成了整张图的绘制和导出。解决把所有图片加载收敛到 Promise.all再进入绘制流程。我给这套流程加了一道检查绘制开始前打印一次素材加载状态凡是走了 fail 分支的直接 toast 提示用户重试而不是拿一张残缺海报去导出。5.2 二维码贴在海报上扫不出来现象二维码看着很清楚但微信扫两次才能出结果或者直接识别失败。原因三个叠加因素——二维码绘制边长小于 100 逻辑像素、用 jpg 导出导致黑色点阵边缘被压缩、没有留白边。解决二维码区域边长下限设为 150导出 fileType 固定 png绘制函数里保留 padding。真机验证的标准动作是把生成的海报图片在微信对话框里长按识别能一次出结果才算通过。5.3 保存相册接口一直失败现象wx.saveImageToPhotosAlbum走 fail 回调用户已经点了授权弹窗还是失败。原因用户历史上拒绝过一次保存相册授权后scope.writePhotosAlbum是 false之后调用接口不会再弹系统弹窗而是直接失败。很多开发者以为接口坏了其实是被授权状态卡住了。解决在 fail 回调里读取 authSetting判断是否是授权问题然后引导用户去设置页手动打开wx.saveImageToPhotosAlbum({ fail(err) { if (err.errMsg.includes(auth) || err.errMsg.includes(authorize)) { wx.showModal({ title: 需要相册权限, content: 请在设置页开启保存到相册权限, success(res) { if (res.confirm) wx.openSetting() } }) } } })多说一句errMsg 里的文案在不同基础库版本有变化不要精确匹配整段字符串用 includes 判断关键词更稳。5.4 iOS 导出图发虚或内容偏移现象同一份代码Android 导出正常iOS 导出后文字边缘发虚右下角内容被截掉。原因iOS 上逻辑像素和物理像素的比例更高canvas 节点没按 dpr 扩容就直接导出输出的是逻辑分辨率另外有些机型对 canvas 区域的尺寸精度有偏差截取区域和绘制区域不完全一致。解决照第 2 章的做法node.width和node.height统一乘 dprctx.scale(dpr, dpr)。导出时的 width/height 也尽量和绘制坐标保持一致不要写死一个大范围再靠 dest 裁切。每改一次尺寸参数就在 iOS 真机上重新导一张验证。5.5 自定义导航栏下海报入口按钮位置偏移现象页面用了自定义导航栏海报生成按钮在不同机型上忽高忽低甚至被状态栏盖住。原因自定义导航栏后没有计算胶囊按钮的位置不同机型的状态栏高度和胶囊位置差异很大。微信小程序顶部导航栏高度没有固定值硬编码必定在某台机器上翻车。解决动态计算。先拿胶囊按钮位置再推导航栏高度const menuRect wx.getMenuButtonBoundingClientRect() const statusBarHeight wx.getWindowInfo().statusBarHeight const navHeight (menuRect.top - statusBarHeight) * 2 menuRect.heightnavHeight 就是自定义导航栏的实际高度海报按钮的 top 用这个值去算。这套公式我在多个项目里复用连 iPad 小程序都能对上。6. 进阶把海报组件化配置文件驱动绘制再用三连验证收尾6.1 配置文件驱动绘制海报画了三次以后我意识到一个问题运营隔三差五换背景图、改文案每次都要动绘制代码成本太高。后来的做法是把海报拆成配置层和渲染层绘制函数只认配置对象。// poster-config.js module.exports { width: 300, height: 420, layers: [ { type: image, src: /assets/bg.png, x: 0, y: 0, w: 300, h: 420 }, { type: avatar, src: user.avatar, x: 40, y: 60, radius: 24 }, { type: text, content: user.name, x: 76, y: 86, fontSize: 16, color: #333333 }, { type: qrcode, text: inviteUrl, x: 180, y: 280, size: 96 } ] }渲染层遍历 layers按 type 分发到对应绘制函数。运营要换图只需要替换 src要调整二维码位置改 qrcode 那一行的坐标。这里注意 user.name、inviteUrl 是运行时数据要在生成配置时注入绘制函数保持无状态。6.2 多端提示uniapp 里 canvas 接口不通用如果你用 uniapp 同时出小程序和 AppCanvas 2D 的接口在小程序端是 wx.createSelectorQuery在 App 端要换 uni 提供的 canvas 上下文H5 端又是浏览器原生的 canvas。不要在公共组件里直接调 wx 前缀用条件编译区分平台否则 App 端一启动就报 wx 未定义。海报这类功能尽量隔离在单独组件里不要散落在页面各处的生命周期中。6.3 发布前的三连验证我从那次白图事故之后每次发布海报相关功能都在真机上强制走一遍三连验证第一生成图在微信对话框里长按能直接识别二维码第二把图片放大到 200%文字边缘不能发虚四角内容不能有裁切第三iOS 和 Android 各导出一次对比两张图的二维码占比和留白。三条都过才敢把版本提审。那段时间被真机问题反复折磨后来干脆把这套流程写进了团队的海报自检清单。做这类资源型功能模拟器只是开发工具真机才是裁判。希望帮到你。本文还有配套的精品资源点击获取
返回列表