简介:面向微信小程序开发者的分享海报生成方案,基于原生组件与画布绘图实现二维码合成,适用于电商裂变、活动推广等需要用户转发带参二维码的场景。资源共5个文件,压缩包仅4KB,包含wxml页面结构、wxss样式、json配置、js逻辑及txt说明,文件虽精简但完整覆盖获取图片、生成二维码、绘制海报、保存相册的核心链路。其中js逻辑实现获取图片信息、下载文件等接口调用,页面与样式负责海报布局,文本说明梳理关键参数与踩坑提醒。附带的haibao模板可直接替换商家标识、商品图与推广码,降低二次开发成本。已有2655人学习下载,对急需快速上线分享功能的小程序开发者是一份轻量可用的参考实现。
1. 微信小程序生成分享海报:把链接变成一张能传播的图
做微信小程序的人迟早会遇到一个尴尬场景:用户觉得你的商品不错,想甩给朋友,结果甩过去的是一段没人愿意点开的文本卡片。分享海报就是为这个场景准备的——把卖点、价格、小程序码塞进一张图,用户长按保存、发朋友圈、发群,传播效率比转发链接高一个量级。
这份资源是一个完整的微信小程序生成分享海报项目,自带二维码生成能力。核心解决三件事:用 canvas 2d 把设计稿渲染成海报图,把用户专属二维码实时画上去,再把画布导出成图片保存到相册。适合两类人:小程序运营想把分享率做上去的产品同学,前端开发想抄一套能直接用的海报绘制方案。项目不依赖后端,二维码在前端本地生成,改改参数就能跑。
2. 解压后的第一件事:目录结构与技术选型
2.1 目录结构拆解:先分清哪些文件要动
拿到这个 .rar,解压后别急着往微信开发者工具里拖,先花两分钟把目录过一遍。这类海报项目通常是一整个完整的小程序工程,不是单文件插件,所以 app.js / app.json / app.wxss 三件套在最外层,pages 目录放演示页面,utils 目录才是这次的主角。
├── app.js ├── app.json ├── app.wxss ├── pages/ │ └── index/ │ ├── index.wxml # 页面结构:两个 canvas + 操作按钮 │ ├── index.wxss # 画布尺寸、按钮样式、隐藏画布定位 │ ├── index.js # 页面逻辑:生成、导出、保存、授权 │ └── index.json # 页面级配置 ├── utils/ │ ├── poster.js # 海报绘制核心:遍历配置、逐元素绘制 │ ├── qrcode.js # 二维码生成库(前端本地绘制,无后端依赖) │ ├── canvas-helper.js # 画布初始化、图片加载、导出封装 │ └── template.js # 海报模板配置(纯 JSON 结构) └── images/ ├── bg.png # 海报背景图(本地资源) └── default-avatar.png # 头像兜底图这个结构里,你要改布局就动 template.js,改绘制逻辑就动 poster.js,页面入口只在 index.js 里调用,分工很干净。实际开发中最容易犯的错是改需求时直接往 index.js 里堆绘制代码,几版之后 poster.js 就名存实亡了——这个项目把"数据"和"绘制"分开的做法值得保留。
2.2 技术选型:canvas 2d 和二维码方案为什么这么配
项目选的是 canvas 2d(即 WXML 里type="2d"的画布),而不是老版本的wx.createCanvasContext。这两者的区别不是兼容性小修,而是底层模型不同:旧版 canvas 拿到的是一套"命令队列"式的 context,绘制指令发给原生层异步执行,你没法同步读像素、拿节点,遇到 DPR 适配和局部重绘非常别扭;canvas 2d 直接给你canvas节点实例,canvas.getContext('2d')拿到的标准 context 有完整的measureText、drawImage能力,行为跟浏览器里的 canvas 基本一致。海报绘制这种场景要反复计算文字宽度、压图、居中,用 2d 版本几乎不用绕路。
二维码部分,项目在 utils/qrcode.js 里内置了一个前端绘制库,直接在当前画布上把二维码算出来。这个选型很实际:小程序分享海报里的码,本质就是把用户 ID 或页面参数编码成一张图,纯前端生成意味着不需要一台服务器去调接口换 token,也不受小程序码接口的次数配额限制。代价是这种码只是普通二维码,点进去打开的是小程序页面;如果你要的是那种圆形带 logo 的官方"小程序码",玩法不一样,得走后端接口wxacode.getUnlimited,下文会单独提。
2.3 基础配置:保存相册的权限与隐私声明
跑通这个项目之前,app.json 里有一处配置漏掉会直接导致保存失败,而且是那种"开发工具里正常、真机必挂"的隐性坑。
{ "pages": ["pages/index/index"], "permission": { "scope.writePhotosAlbum": { "desc": "用于保存分享海报到相册" } } }scope.writePhotosAlbum声明了相册权限的用途文案,首次调用wx.saveImageToPhotosAlbum时微信会弹出授权框,这段 desc 就是授权框里展示给用户的说明。必须写清楚用途,否则审核阶段会被打回,理由是"权限用途描述不明确"。2023 年之后微信还要求在小程序后台配置用户隐私保护指引,把"相册(写入)权限"这一项勾上,并且代码里调用前先走一遍wx.getPrivacySetting检查用户是否同意过隐私协议。这个项目在 index.js 里做了隐私检查的封装,真机调试时如果保存相册一直无响应,优先怀疑是不是这层没过。
3. 核心模块拆解:从设计稿到海报的完整链路
3.1 海报配置对象:把设计稿翻译成数据
项目把海报当成一棵元素树来画,这是最容易复用的设计。设计稿上每一个可见元素都被抽象成 JSON 配置项,类型、位置、尺寸、内容来源全部声明式描述,绘制引擎只负责遍历执行。
// utils/template.js module.exports = { width: 375, height: 667, background: { color: '#F6F3EE', image: '/images/bg.png' }, elements: [ { type: 'image', key: 'avatar', // 数据源 key,绘制时从 data 里取值 x: 28, y: 44, width: 72, height: 72, round: 36, // 圆角半径,画之前 clip 裁切 fallback: '/images/default-avatar.png' }, { type: 'text', key: 'nickname', x: 116, y: 58, fontSize: 18, color: '#333333', bold: true, maxWidth: 220, lineHeight: 24, prefix: '来自 ', // 文本前缀,比如"来自 XX 的推荐" }, { type: 'text', key: 'remark', x: 28, y: 260, fontSize: 14, color: '#666666', maxWidth: 319, lineHeight: 20, lines: 3 // 最多 3 行,超出省略 }, { type: 'qrcode', key: 'qrContent', x: 108, y: 430, size: 160, background: '#FFFFFF' } ] }每个元素用key与外部传入的动态数据对接,静态属性直接写在配置里。这样设计有两层用意:一是视觉改版时只调 JSON,不碰绘制逻辑;二是同一个绘制引擎可以喂不同模板,做出多套海报。prefix和lines这类字段是给文本扩展用的,绘制时读到lines: 3就知道要算省略号。
3.2 画布初始化:node 获取与高 DPI 适配
canvas 2d 的第一个坑是:节点只能在页面onReady之后通过wx.createSelectorQuery拿,而且canvas的宽高必须手动设置成物理像素。很多新手直接把 WXML 里的 CSS 宽高当画布尺寸用,画出来的海报在 iPhone 上糊成一团,就是漏了 DPR 这步。
// utils/canvas-helper.js function initCanvas(selector) { return new Promise((resolve) => { wx.createSelectorQuery() .select(selector) .fields({ node: true, size: true }) .exec((res) => { if (!res || !res[0] || !res[0].node) { reject(new Error('canvas 节点不存在,检查 type="2d" 和 id')) return } const { node: canvas, width, height } = res[0] const dpr = wx.getSystemInfoSync().pixelRatio // canvas 内部像素尺寸 = 逻辑尺寸 × DPR canvas.width = width * dpr canvas.height = height * dpr const ctx = canvas.getContext('2d') ctx.scale(dpr, dpr) // 坐标系缩放为逻辑尺寸 resolve({ canvas, ctx, width, height, dpr }) }) }) }关键在ctx.scale(dpr, dpr):这之后所有绘制坐标都按 CSS 逻辑尺寸写,fillRect(28, 44, 72, 72)就代表 28px 处画 72px 的方块,不用自己在每个坐标上乘系数。如果业务里需要更高清的导出图,可以在导出时用destWidth放大,而不是在画布尺寸上堆倍数,后者会拖慢绘制速度。
3.3 文字绘制:中文宽度计算与自动换行
海报上的文案是最容易翻车的地方。设计稿给的是固定字号和留白,代码里必须真实计算出每一行能放几个字,否则要么溢出画布边界,要么行数对不上设计。关键问题是ctx.measureText对中文字符测量在部分基础库版本里不准确,所以项目里先用正则把中文和西文分开算宽度。
// utils/poster.js 文字换行片段 function measureTextWidth(ctx, text, fontSize) { let width = 0 for (const ch of text) { if (/[\u4e00-\u9fa5]/.test(ch)) { width += fontSize // 中文按全角宽度算 } else { width += ctx.measureText(ch).width // 英文数字用真实测量 } } return width } function wrapText(ctx, text, maxWidth, fontSize) { const lines = [] let line = '' let lineWidth = 0 for (const ch of text) { const chWidth = measureTextWidth(ctx, ch, fontSize) if (lineWidth + chWidth > maxWidth) { lines.push(line) line = ch lineWidth = chWidth } else { line += ch lineWidth += chWidth } } if (line) lines.push(line) return lines }wrapText按字符逐个累加宽度,超过maxWidth就断行。实际绘制时再套一层行数和省略号处理:如果wrapText返回的行数大于配置里的lines,就截取前 N-1 行,最后一行末尾补...。这里有个细节——省略号本身的宽度也要参与计算,否则最后一行视觉上会顶到边界。文本绘制完记得ctx.textBaseline = 'top',这样 y 坐标直接就是文字顶部,配合行高做多行排版时心智负担小很多。
3.4 二维码生成:隐藏画布加容错率设置
二维码这块项目用了双画布方案:一个隐藏画布专门画二维码,导出成临时图片后,再贴到主海报画布上。之所以多绕一步,是因为二维码库的绘制接口是直接操作画布节点的,它会把整张画布清掉重画,没法只画一个区域;而且二维码库的坐标系不受主画布的scale(dpr, dpr)影响,混在一起画会出现尺寸错位。
// utils/qrcode.js 封装 function drawQRCode(content, size) { return new Promise((resolve, reject) => { initCanvas('#qrCanvas').then(({ canvas, ctx, dpr }) => { canvas.width = size * dpr canvas.height = size * dpr ctx.scale(dpr, dpr) // 内置二维码库:容错率取 H,码点更密但抗遮挡 QRCode.draw(content, canvas, size, size, { foreground: '#000000', background: '#FFFFFF', padding: 8, correctLevel: QRCode.CorrectLevel.H }).then(() => { wx.canvasToTempFilePath({ canvas, x: 0, y: 0, width: size, height: size, destWidth: size * 2, destHeight: size * 2, fileType: 'png', success: (res) => resolve(res.tempFilePath), fail: reject }) }) }) }) }correctLevel: QRCode.CorrectLevel.H是容错率最高档,二维码局部被贴纸、头像或者手指遮挡时依然能扫出来,海报场景下强烈建议用 H。destWidth导出两倍尺寸是为了防止二维码边缘发虚——二维码扫描是二值化识别,模糊边缘可能导致识别率下降。临时图片拿到后,在主画布上canvas.createImage()加载再drawImage贴回目标位置,整个流程是 Promise 串行的,保证主画布上的二维码永远在背景和文字之后绘制,不会被盖住。
4. 避坑指南:真机上最常见的四个翻车现场
4.1 现象:二维码画上去是空白
模拟器和真机上都可能出现二维码区域一片空白,控制台没有报错。排查后发现原因在两个方向:一是二维码库的draw是异步的,主画布在二维码还没画完时就开始导出,导出图里自然是空的;二是隐藏 canvas 被wx.canvasToTempFilePath转图片时机太早,二维码库内部还在绘制。解决办法是严格串行——drawQRCode的 Promise resolve 之后,主画布才开始绘制和导出,并且二维码要贴在最后画,遵循"背景、图片、文字、二维码"的固定顺序。
4.2 现象:iOS 保存相册失败,fail 返回 authorize no response
按钮点了没反应,fail回调里errMsg是saveImageToPhotosAlbum:fail auth deny或干脆authorize no response。原因分两层:首次调用没弹授权框,或者用户之前拒过,微信会在后续调用里直接静默失败。解决分三步:调用前用wx.getSetting主动查询scope.writePhotosAlbum状态;拒绝后用wx.showModal说明用途并引导wx.openSetting去设置页打开权限;同时在 app.json 里把 permission.desc 写清楚,很多"没弹框"其实是因为后台隐私指引没配,真机调试时在开发者工具"真机调试"面板看隐私协议的状态能快速定位。
4.3 现象:文字换行错乱,英文和数字串到下一行
配置了maxWidth: 319,实际绘制时一行放了两倍多的字符,中文倒是正常。原因是ctx.measureText默认字体没有设置,测量英文字宽时用的是 canvas 默认字体,跟你ctx.font = '14px sans-serif'设置的字体不一致,测出来的宽度偏差很大。解决方法是每次测量前先设置字体:ctx.font =${bold ? 'bold ' : ''}${fontSize}px sans-serif``,保证测量和绘制用同一套字体参数。中文按全角宽度直接加fontSize的方式不受字体影响,所以中文正常的项目往往在混合文案上才会暴露这个问题。
4.4 现象:开发工具正常,真机白屏
开发工具里海报渲染完整,预览到手机上一片白,只有背景色。这个踩坑经验基本可以锁定在 canvas 2d 的wx.canvasToTempFilePath参数上——老版 API 用canvasId传字符串,canvas 2d 要求传canvas节点对象,而且导出的x/y/width/height是画布逻辑尺寸,不是像素尺寸。白屏的另一个高发原因是 WXML 里 canvas 的样式宽高为 0,createSelectorQuery拿到的width是 0,画布实际像素被设置成 0。排查时先console.log打印fields({ node: true, size: true })返回的宽高,再确认导出参数用的是节点引用而不是 ID,基本能排除 80% 的"开发工具好真机挂"。
5. 从点击到落相册:完整跑通生成链路
5.1 页面结构:一主一副两个画布
页面里放两个 canvas,一个负责最终海报,一个专画二维码并隐藏到屏幕外。注意隐藏画布不能用display: none,节点不渲染的话createSelectorQuery会拿不到 node,正确做法是用position: fixed移出可视区域。
<!-- pages/index/index.wxml --> <view class="page"> <canvas type="2d" id="posterCanvas" class="poster-canvas"></canvas> <!-- 二维码隐藏画布:移到可视区外,保留渲染能力 --> <canvas type="2d" id="qrCanvas" class="qr-canvas"></canvas> <button class="action-btn" bindtap="generatePoster">生成分享海报</button> <button class="action-btn" bindtap="savePoster">保存到相册</button> </view>/* pages/index/index.wxss */ .poster-canvas { width: 375px; height: 667px; background: #F6F3EE; } .qr-canvas { position: fixed; left: -9999px; top: -9999px; width: 300px; height: 300px; }type="2d"是两个 canvas 都必须带的属性,少了它拿到的不是节点而是老版 context,后续canvas.createImage直接报错。CSS 里的375rpx和375px要区分清楚,海报这种固定设计稿尺寸的场景统一用px,避免 rpx 在不同屏幕宽度下把海报撑变形。
5.2 生成与保存的完整调用链
页面按钮的完整链路是三段式:先把模板和动态数据喂给绘制引擎,再把画布导出成图片文件,最后触发保存授权。项目里把这三步拆成了三个可独立测试的方法。
// pages/index/index.js 核心逻辑 Page({ data: { userInfo: null, posterPath: '' }, async generatePoster() { wx.showLoading({ title: '海报生成中', mask: true }) try { // 1. 准备动态数据:昵称、头像、二维码内容 const payload = { nickname: '某开发者', avatar: '/images/default-avatar.png', remark: '这份项目笔记我整理了一周,点击进来看看', qrContent: 'pages/index/index?uid=1001&from=poster' } // 2. 绘制:传入 canvas 节点与模板 key,内部完成全部绘制 await drawPoster('#posterCanvas', 'default', payload) // 3. 导出:canvas 2d 传节点,destWidth 双倍输出 const path = await exportPoster('#posterCanvas', 750, 1334) this.setData({ posterPath: path }) wx.showToast({ title: '生成完成,可保存或截图', icon: 'success' }) } catch (err) { console.error('[generatePoster]', err) wx.showToast({ title: err.message || '生成失败', icon: 'none' }) } finally { wx.hideLoading() } }, async savePoster() { if (!this.data.posterPath) { wx.showToast({ title: '请先生成海报', icon: 'none' }) return } try { await saveToAlbum(this.data.posterPath) // 内部处理授权与引导 wx.showToast({ title: '已保存到相册', icon: 'success' }) } catch (err) { // saveToAlbum 内部已弹出授权引导,这里只兜底 console.error('[savePoster]', err) } } })exportPoster内部对 canvas 2d 的调用是把canvas节点和destWidth一起传,width/height用逻辑尺寸,destWidth/destHeight用目标输出尺寸,比例必须一致,否则图片会被拉伸。生成和保存拆成两个按钮,是为了让用户先预览海报内容再决定是否保存,这个交互顺序比"点击即生成即保存"留存体验好得多。
5.3 验证方法:从对齐到清晰度的检查清单
海报这种功能,肉眼看得"差不多"往往就是差很多。项目里我习惯在调试阶段临时画辅助网格线和边界框:遍历模板元素时给每个元素描一个 1px 的#FF0000描边,导出图放大检查文字是否超出maxWidth、图片是否挤压变形、二维码是否被其他元素压住。网格线在最终导出前删掉,但流程建议保留。
清晰度的验证点在导出参数:海报逻辑宽度 375px,导出destWidth至少 750px,这样在 iPhone 的 3x 屏幕上长按放大才不会看到锯齿。边界情况的验证清单是:极长昵称(比如 30 个字符)会不会把头像挤掉、缺省头像存在不存在、二维码内容里带特殊字符&会不会被转义破坏。这些用例在每个模板上跑一遍,比上线后用户反馈再修要省事得多。
6. 二次开发:把海报生成器改造成配置化模板引擎
6.1 模板化:从固定布局到 JSON 驱动
项目自带一套模板配置,但实际业务通常不止一张海报。改造方向是让template.js变成模板注册表,绘制引擎按key取模板,这样运营换文案、换背景只需要改一套配置对象,发版都不需要。
// utils/template.js 扩展 const templates = { default: { background: { color: '#F6F3EE', image: '/images/bg.png' }, elements: [ /* 默认模板 */ ] }, discount: { background: { color: '#FFF4E0', image: '/images/bg-discount.png' }, elements: [ { type: 'text', key: 'discountText', x: 28, y: 300, fontSize: 32, color: '#E64340', bold: true }, // 折扣模板的其他元素 ] } } function getTemplate(name) { return templates[name] || templates.default }绘制引擎的入口同步改成drawPoster(selector, templateName, data),内部只做一件事:拿模板、遍历elements、按type分发绘制。动态数据和模板彻底解耦后,新增一张海报的成本从"改代码"降成"写 JSON"。
6.2 动态数据注入与远程模板的开关
动态数据注入的改造点是处理缺省值:模板里每个元素都应该有fallback,业务数据没传或传空时,使用兜底内容而不是直接跳过绘制。头像、昵称、分享文案这三个字段最容易缺,项目里的默认模板已经示范了缺省头像的写法,扩展模板时记住"宁可画默认图,不要留白块"这个原则就行。
远程模板是另一个可选项——做一个管理后台把模板 JSON 下发到小程序,客户端wx.request拉取后先校验 JSON 结构再缓存到本地存储,缓存未命中时用内置模板兜底。这样做的好处是运营改文案不用等发版,代价是必须处理 JSON 校验和缓存过期。背景图如果换远程地址,记得走wx.downloadFile下载成临时文件再绘制,直接canvas.createImage().src = 'https://...'会被下载域名白名单卡住,开发阶段临时关闭域名校验可以,上线前必须在后台配置合法downloadFile合法域名。
6.3 一个值得长期坚持的绘制习惯
做这个项目二次开发时,我给自己定了一条强制规则:每次绘制前必须ctx.clearRect(0, 0, width, height),每次画图前必须等image.onload,每次导出必须在所有异步绘制完成之后。这三条看着简单,但海报类需求几乎所有的"偶现白屏""图片错位"都是违反其中一条导致的。从那以后,我每次接到生成图片类需求都强制走一遍这三个检查,宁可多写两个 Promise 也不在回调里放飞时序。希望这个项目和这篇拆解能帮你少踩几个真机上的坑,把分享率这件事踏踏实实做起来。
本文还有配套的精品资源,点击获取