
简介面向微信小程序开发者的分享海报生成示例包针对社交裂变、活动推广等场景解决在微信生态内快速绘制带二维码的海报并保存到用户相册的问题。整个压缩包仅4KB共含5个文件js逻辑脚本负责获取网络图片、调用二维码生成库并借助canvas合成海报wxml与wxss定义页面结构和样式json提供组件配置txt使用说明则梳理了wx.getImageInfo、wx.downloadFile、wx.saveImageToPhotosAlbum等关键API的调用步骤并给出参数设置与常见错误处理思路。开发者可据此直接套用或改造实现网络图片转本地临时文件、通过Base64编码传递二维码数据、绘制到canvas并授权保存到相册的完整链路附带的海报模板结构清晰方便自定义文案和背景。资源已吸引2652人学习适合刚接触小程序Canvas绘图或需要快速上线分享功能的初中级开发者参考也可作为理解小程序文件协作与API组合的迷你案例。1. 微信小程序生成分享海报不是截图是一张带二维码的画布图小程序里做“分享海报”最常见的诉求是让用户把商品、活动或名片转发到朋友圈、微信群时带的是一张完整好看的图片而不是小程序卡片。但这个功能用页面截图是行不通的——页面里有按钮、导航栏、胶囊截出来的图看起来就像测试截图运营方不收。标题里这个方案的本质是前端把商品图、活动背景、用户头像和信息文案画到 canvas 上再叠一个带参数的二维码导出后保存进相册。它适合电商返佣、活动邀约、企业名片这类场景用户分享动机强而且平台上还要能追踪到“谁带来的访问”。技术栈上原生小程序、Taro、uniapp 都能做差异只在取 Canvas 节点的写法。下面按一条完整链路讲先定二维码和画布的选型再写绘制代码接着导出保存最后把最常见的坑一次性说透。2. 先定生成链路二维码来源、Canvas 版本与尺寸换算动手写绘制代码之前有三件事必须先定下来二维码用什么方式生成、canvas 用哪套 API、设计稿和画布尺寸怎么换算。这三件不定清楚后面无论代码怎么写都会反复返工。2.1 二维码从哪来官方小程序码、普通二维码、前端生成三选一不做选型就直接在前端引一个 qrcode.js 生成二维码贴到海报上等上线测试时才会发现微信内长按不一定能识别这种普通二维码分享闭环根本跑不通。二维码的来源直接决定传播链路常见的就三条路。来源怎么生成微信内识别带参数落地适用场景官方小程序码接口getwxacodeunlimit后端拿 access_token 调微信接口图片存 CDN 后给前端 URL长按、扫一扫都稳定支持 scene 参数落地页 onLoad 可拿主推裂变分享首选普通二维码后端 qrcode 库生成链接后端生成二维码图片扫码先进 H5 再跳小程序微信内长按识别不稳定要经过 H5 中转带参线下物料、短信、外部 App 分享前端 qrcode.js 生成 base64 再画纯前端生成不需要后端接口只能当普通链接识别基本不带参落地要额外处理本地联调、demo 演示主推第一种因为微信生态内传播时识别率最高。接口调用形式是https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_tokenACCESS_TOKENPOST 请求体里带 page、scene、width 等参数返回的是图片二进制后端需要先把图片存到 CDN 或者转成临时文件给前端用。这个接口的 scene 参数限制 32 个可见字符所以多参数场景不要硬塞一堆 keyvalue常见做法是只放一个业务 id落地页拿 id 再查详情既绕开长度限制也方便后端埋点。2.2 用新版 Canvas 2D 还是旧版 CanvasContext看这两点再决定社区里大量老文章和绘制组件比如 Painting 库都基于旧版 wx.createCanvasContext。旧版的特点是命令式ctx.draw() 之后才真正提交渲染拿不到原生节点做圆角裁剪和复杂合成时 API 绕得厉害。新版 Canvas 2D 拿到的是 canvas 节点加 ctx node.getContext(2d)API 和 HTML5 canvas 几乎一致图片对象、clip、圆角裁剪的处理方式都更直观。判断依据主要看两点一是项目基础库版本和现有依赖如果老项目已经基于 Painting 类封装做了一堆海报模板迁移到 Canvas 2D 的改造成本高可以保留旧版二是新项目没有历史包袱直接用 Canvas 2D。我一般在新项目里一律走 Canvas 2D理由很实际后面要画圆形头像、要调圆角二维码Canvas 2D 里一套 save / clip / restore 就完成旧版 setClip 的坐标边界判断很容易翻车。如果你用 uniapp 开发微信小程序这里有个隐藏细节uni.createCanvasContext 走的是旧版 API要覆写 Canvas 2D 必须用 uni.createSelectorQuery().select(#canvas).fields({ node: true }).exec() 拿节点再 getContext(2d)和原生小程序的写法同源但很多 uniapp 教程不会讲这一步照搬原生代码经常查不到节点。2.3 海报尺寸与像素比换算设计稿怎么映射到 canvas 画布很多团队直接拿设计稿 750x1334 设置 canvas 的 width 和 heightcanvasToTempFilePath 时 destWidth 又写 750生成的图在电脑上看没问题一上手机就发虚。原因是 canvas 元素的 CSS 显示尺寸和绘图缓冲区的物理像素尺寸没有对应必须乘上设备的 pixelRatio。常见做法是设计稿固定宽度 750高度按比例来例如 1334。canvas 标签的 style 宽高是“显示尺寸”它的作用是预览一般按屏幕宽度等比缩小我习惯设成 375px 宽而 canvas.width 和 canvas.height 是“绘图缓冲区尺寸”要设置为设计稿宽度乘以 pixelRatio。绘制前先 ctx.scale(pixelRatio, pixelRatio)之后所有绘制坐标都按设计稿 750 的坐标系写不用自己在每个 drawImage 里乘一遍。这里要注意海报高度一旦超过屏幕可视高度canvas 元素在手机上看不全。生成过程中画布不能被遮挡还要避开“微信小程序顶部导航栏高度”和底部 tabbar 的区域否则某些机型上导出时会出现难排查的渲染问题。这个坑在第 5 章会展开讲做尺寸规划时就要先把画布在页面里的落位想好。3. 把海报画出来初始化画布、加载远程图、画头像和二维码选型定了进入核心实现。这一章的代码按原生小程序写Taro、uniapp 的读者把取节点的方式对应替换即可。3.1 初始化 Canvas 2D拿到节点就成功了一半WXML 端先放一个 canvas 标签注意 type2d 不能漏漏了就走旧版逻辑了。view classposter-preview canvas type2d idposterCanvas stylewidth:375px;height:667px;/canvas /viewCSS 尺寸 375x667 只是预览尺寸实际生成分辨率的逻辑在 JS 里控制。初始化脚本initCanvas() { // 自定义组件内必须用 this.createSelectorQuery()页面里用 wx.createSelectorQuery() const query this.createSelectorQuery(); query .select(#posterCanvas) .fields({ node: true, size: true }) .exec((res) { const data res[0]; if (!data || !data.node) { console.error(canvas 节点未找到检查 id 是否写错或 canvas 是否处于隐藏状态); return; } const { pixelRatio } wx.getWindowInfo(); // 老基础库可用 wx.getSystemInfoSync() 兜底 const designWidth 750; // 设计稿宽度 const designHeight 1334; // 设计稿高度 // 关键绘图缓冲区尺寸 设计稿尺寸 * 像素比 data.node.width designWidth * pixelRatio; data.node.height designHeight * pixelRatio; const ctx data.node.getContext(2d); // 缩放之后所有绘制命令都用设计稿坐标系写 ctx.scale(pixelRatio, pixelRatio); this.canvas data.node; this.ctx ctx; this.design { width: designWidth, height: designHeight }; }); }这段代码里最容易被忽略的是 pixelRatio。iPhone 上是 2 或 3安卓碎片化更明显如果漏了这步drawImage 的文字和图标边缘一定发虚。fields({ node: true, size: true })返回的 size.width 和 size.height 是 canvas 的 CSS 尺寸不是绘图缓冲区尺寸不要用它去计算导出分辨率。3.2 下载并绘制远程图片createImage 与域名校验Canvas 2D 里加载远程图片不要用 wx.getImageInfo 拿临时路径再去画直接用 canvas.createImage() 更干净。封装成一个 Promise方便统一等资源加载完成。loadImage(url) { return new Promise((resolve, reject) { const img this.canvas.createImage(); img.onload () resolve(img); img.onerror (err) reject(new Error(图片加载失败: ${url})); img.src url; }); }背景图绘制async drawPoster() { if (!this.ctx) await this.initCanvas(); const bg await this.loadImage(https://cdn.example.com/poster-bg.jpg); this.ctx.drawImage(bg, 0, 0, 750, 1334); // 背景图尺寸要和设计稿严格一致否则会被拉伸变形 }远程图片的域名必须配置在小程序后台的 downloadFile 合法域名里。本地开发可以勾选“不校验合法域名”但上线不配的话图片加载就是黑匣子用户手机上随机空白后台还看不到报错。另外一些 CDN 有防盗链直接 img.src 加载会 403此时需要给 CDN 加跨域响应头或者换一个不带防盗链的图床地址。3.3 用户头像的圆角裁剪save/clip/restore 的配合用户头像通常要裁成圆形Canvas 2D 的标准做法是先用 arc 画一个圆形路径clip 之后 drawImage最后 restore。drawRoundImage(ctx, img, x, y, radius) { ctx.save(); ctx.beginPath(); ctx.arc(x radius, y radius, radius, 0, Math.PI * 2); ctx.closePath(); ctx.clip(); ctx.drawImage(img, x, y, radius * 2, radius * 2); ctx.restore(); }参数说明x 和 y 是圆形左上角坐标radius 是半径。界面设计稿里常见的头像位置在左上角比如 x50, y100半径 50那么圆心就在 (100, 150)绘制时头像图片从 (50, 100) 开始画宽高都是 100。ctx.save()的作用是把当前裁剪状态存下来ctx.restore()恢复否则 clip 之后所有后续绘制文案、二维码都会被这个圆形区域裁剪这是最容易翻车的点。3.4 二维码落位与安全边距画上去之前先想清楚分享链路二维码图片从后端拿回来通常是一个 CDN 地址。画到海报上前先想清楚用户拿到这张图后会怎么操作长按识别、微信扫一扫、还是打印出来线下扫。不同传播方式对二维码周围净空的要求不一样背景太花、码太小都会降低识别率。// qrImg 已经通过 loadImage 加载完成 const qrSize 160; // 二维码边长设计稿 750 下建议不小于 120 const margin 40; // 右边距 const bottomSpace 80; // 底部留白放提示文字 this.ctx.drawImage(qrImg, 750 - qrSize - margin, 1334 - qrSize - bottomSpace, qrSize, qrSize); // 提示文案 this.ctx.fillStyle #8a8a8a; this.ctx.font 22px sans-serif; this.ctx.textAlign center; this.ctx.fillText(长按识别小程序码, 750 - qrSize / 2 - margin, 1334 - bottomSpace 26);绘制入口建议用 Promise.all 把所有图片资源先收齐再开始画async drawPoster() { if (!this.ctx) await this.initCanvas(); const [bg, avatar, qr] await Promise.all([ this.loadImage(bgUrl), this.loadImage(avatarUrl), this.loadImage(qrUrl), ]); // 绘制顺序背景 - 文案 - 头像 - 二维码 // 全部绘制完成后再调导出不要一边加载一边画 }某个图片加载失败时不要整个流程抛错。常见做法是给每个资源一个默认兜底图比如头像加载失败就画一个灰色圆加一个“用户”两字二维码加载失败就重试一次仍失败再提示用户稍后再试。失败的兜底逻辑写在 loadImage 的调用方而不是封装函数里这样每张图的兜底策略可以不同。4. 从 Canvas 到相册导出图片、保存授权与参数回跳画布画好了接下来三步走导出临时文件、保存相册、然后确保二维码扫进来能带参数回到正确页面。这三步每一步都有版本差异和授权边界值得单独拆开说。4.1 导出临时图片canvasToTempFilePath 的新旧版参数差异Canvas 2D 环境下wx.canvasToTempFilePath 必须传 canvas 节点旧版代码在这里传的是 CanvasContext直接会失败。更隐蔽的坑是 x/y/width/height 这套参数Canvas 2D 的绘图缓冲区尺寸是 designWidth * pixelRatio如果你按设计稿宽度 750 去截在 pixelRatio2 时会只截到画布左上角一半生成出来的图缺一大块。exportToTempFile() { return new Promise((resolve, reject) { wx.canvasToTempFilePath({ canvas: this.canvas, // 新版 Canvas 2D必须传节点 // 不传 x/y/width/height默认按整个绘图缓冲区取图 destWidth: this.design.width * 2, // 导出宽度750 * 2 1500 destHeight: this.design.height * 2, // 导出高度 fileType: jpg, quality: 0.92, success: (res) resolve(res.tempFilePath), fail: (err) reject(err), }); }); }destWidth 和 destHeight 决定导出图片的物理分辨率。设计稿 750 宽导出按 2 倍就是 1500px这个清晰度在朋友圈和微信群足够用没有必要拉到 3 倍安卓低端机导出大图时内存容易爆。fileType 选 jpg 还是 png 要看设计稿。jpg 体积小但画布没铺满的区域会变成黑色。所以用 jpg 时绘制前先刷一层白色底this.ctx.fillStyle #ffffff; this.ctx.fillRect(0, 0, this.design.width, this.design.height);如果海报底部本身是透明设计用 png但 png 体积通常比 jpg 大 3 到 5 倍加载和保存都会慢一些。我的习惯是海报背景一定是满幅的直接 jpg省体积也省内存。4.2 保存相册的授权流程拒绝之后怎么把流程拉回来wx.saveImageToPhotosAlbum 必须拿到 scope.writePhotosAlbum 授权。第一次调用会自动弹授权框用户一旦点拒绝第二次调用直接走 fail不会再弹。所以流程必须做两段处理检查授权状态拒绝后引导去设置页。async saveToAlbum(filePath) { const setting await wx.getSetting(); if (setting.authSetting[scope.writePhotosAlbum] false) { const res await wx.showModal({ title: 需要相册权限, content: 拒绝后无法保存海报去设置里打开相册权限, confirmText: 去设置, }); if (res.confirm) { const openRes await wx.openSetting(); if (openRes.authSetting[scope.writePhotosAlbum] true) { return this.doCopySave(filePath); } wx.showToast({ title: 未获得相册权限, icon: none }); } return; } return this.doCopySave(filePath); } doCopySave(filePath) { return new Promise((resolve, reject) { wx.saveImageToPhotosAlbum({ filePath, success: (res) resolve(res), fail: (err) { if (err.errMsg err.errMsg.includes(auth deny)) { wx.showToast({ title: 需要去设置里打开相册权限, icon: none }); } reject(err); }, }); }); }逻辑说明wx.getSetting 返回的 authSetting 里如果这个权限字段不存在说明用户还没被弹过授权框直接走 doCopySave由系统弹第一次框。如果字段是 false说明之前拒绝过此时再调 saveImageToPhotosAlbum 没有意义弹窗引导去设置页。注意 wx.openSetting 必须由用户点击行为触发放在 showModal 的 confirm 回调里没问题但不要再包一层 setTimeout有些基础库版本会直接不响应。4.3 scene 参数回跳二维码是怎么把用户带进具体页面的小程序码的 scene 参数是官方做分享追踪的标准入口但很多人第一次用都会踩编码坑。场景是用户 A 生成海报用户 B 扫码进入小程序落地页要拿到 A 的邀请关系。后端的生成逻辑Node 示意// 后端 Node 示例前端调用此接口拿到二维码图片 URL const resp await axios.post( https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token accessToken, { page: pages/goods/detail, scene: gid${goodsId}, // scene 只放业务 id不要拼一堆参数 width: 430, check_path: false, env_version: release, // 体验版调试时用 trial }, { responseType: arraybuffer } ); // 把 resp.data图片二进制上传到 CDN返回 URL 给前端前端落地页解析onLoad(options) { // 微信会把 scene 做一次 url 解码这里再解一次更稳 const scene decodeURIComponent(options.scene || ); const params scene.split(); const map {}; params.forEach((item) { const [key, value] item.split(); map[key] value; }); const goodsId map.gid; if (goodsId) { this.loadGoods(goodsId); } }场景参数的限制是 32 个可见字符page 不能带 query也不能是 tabBar 页面。scene 里如果包含特殊符号务必在后端拼参时先 encodeURIComponent 一次落地页再做一次 decodeURIComponent。用 split 而不是 URLSearchParams是为了兼容老安卓机的基础库实现。5. 避坑生成海报最常见的 5 个翻车现场这一章是血泪经验集中地。下面每个问题都按“现象 → 原因 → 解决”写都是我见过不止一次的真实事故。5.1 画布藏起来之后导出全是黑图现象为了不让画布干扰页面布局把 canvas 放到屏幕外left: -9999px或者 display: none生成后拿到的图片全黑或者一片空白。原因小程序 canvas 的渲染依赖视图层真正绘制画布不可见时底层合成器不会执行绘制这是基础库的渲染机制不是代码逻辑问题。解决画布必须放在可视区域内。常见做法是生成海报时盖一个全屏遮罩把 canvas 放在遮罩层上正常显示导出完成后再移除。画布位置还要避开“微信小程序顶部导航栏高度”下方的胶囊按钮尽量居中或偏下部避免导出时被系统 UI 干扰。5.2 文字和图片边缘发虚像素比没处理对现象生成的图在电脑上看很清晰发到手机上糊放大更明显。原因canvas 元素的 CSS 尺寸和绘图缓冲区尺寸没对应或者导出 destWidth 没有按设计稿倍数给。解决统一按 3.1 节的初始化方式处理node.width designWidth * pixelRatio绘制命令放在 ctx.scale(pixelRatio, pixelRatio) 之后导出时 destWidth 用 designWidth * 2。不要直接拿 node.width 当 destWidth因为 node.width 已经是物理像素再导出容易得到 3000px 以上超大图安卓机内存压力大。5.3 海报上莫名其妙多了一块空白图片还没加载完就开始画现象海报其他元素正常只有背景或头像那块区域是白色时有时无刷新多次才出现一次。原因图片是异步加载绘制时 onload 还没触发drawImage 画了一个空对象进去。很多老代码用 setTimeout 等 200ms 来赌资源加载完网络慢就翻车。解决用 canvas.createImage Promise 封装一次 Promise.all 把背景、头像、二维码全部收集完再开始绘制。单张图片失败时用兜底图不让整个流程挂掉。5.4 临时文件导出失败新版 Canvas 2D 传错了 canvas 参数现象canvasToTempFilePath 的 fail 回调返回参数错误或者 success 拿到图之后发现只有左上角一部分。原因新版接口要求传 canvas 节点传了旧版 CanvasContext 会直接报错另一个原因是 x/y/width/height 按设计稿 750 传但绘图缓冲区实际是 750 * pixelRatio导致只截取画布左上角的区域。解决确认 this.canvas 来自 createSelectorQuery 的 node 字段导出时不传 x/y/width/height让默认值按整个画布取图只设置 destWidth 和 destHeight。旧项目从 CanvasContext 迁到 Canvas 2D 时这一条是必须检查的。5.5 用户第一次拒绝授权后第二次怎么都弹不出框现象用户第一次点保存时选了拒绝之后每次点保存按钮都没有反应既不报错也不弹窗。原因小程序授权机制是一次性确认拒绝后 scope.writePhotosAlbum 被置为 false再次调用 saveImageToPhotosAlbum 直接走 fail系统不会重复弹授权框。解决保存按钮点击后先 wx.getSetting 检查权限状态false 就弹窗引导用户去 wx.openSetting 手动打开。注意 openSetting 回来之后要再执行一次保存不能只给 toast 就结束否则用户设置完还要再点一次按钮体验很差。6. 三个让海报稳定上线的验证技巧6.1 真机自测清单把屏幕和画布位置当第一项海报功能必须在真机上测模拟器上的表现和真机差很多。自测时覆盖三类机型iPhone 刘海屏、安卓全面屏、以及微信 PC 版PC 端 canvas 渲染行为不同至少确认不会崩。画布位置避开胶囊按钮、避开导航栏导出前保证它在可视区域内。如果海报高度超过屏幕设计稿可以缩短高度或者在导出完成后直接跳到一个预览页展示成品图预览页里再放保存按钮这样画布在生成阶段始终在屏幕内又不会挤占正常页面布局。6.2 给海报打印版本角标调试期的最快验证方法我调试海报时习惯在画布右下角画一行小字内容是一个版本常量比如v20250101.1。这样拿手机相册里的图一眼就能确认生成的是不是刚改的版本不用反复清缓存。上线前把角标那段代码去掉就行。这个动作成本极低但排查线上问题时非常有用用户反馈“二维码扫不出来”看图上版本号就能判断是旧版缓存还是真的生成逻辑出问题。6.3 把生成耗时和成功率当成上线指标海报生成链路里图片加载最耗时CDN 抖动、用户头像加载慢都会拖慢整体。我习惯在关键节点记录耗时并上报一个简单的事件const start Date.now(); // 绘制、导出、保存全流程 const cost Date.now() - start; const payload { cost, success: 1, errMsg: };有了耗时和成功率数据才能区分是 CDN 的问题、授权流失的问题还是 canvas 兼容性的问题。之前排查过宣传图二维码区域偶尔空白的线上问题最终定位就是二维码图片 CDN 回源慢超时后走了 onerror 兜底分支。现在做这个功能我第一件事永远是先钉死画布节点和图片资源这两层再谈样式细节希望今天写到的这些细节能帮你少走一次弯路。本文还有配套的精品资源点击获取