拓冰建站拓冰建站
首页 / 资讯中心 / 正文

微信小程序Canvas 2D海报生成:原理、实战与避坑指南

简介这是一份基于微信小程序新版 canvas2d 的海报绘制封装组件主要面向需要在小程序内实现商品分享海报、邀请海报等场景的开发者。针对官方文档零散、老版 canvas 不再维护、开发门槛高等痛点作者将完整实现整理成开箱即用的组件调用时仅需传入参数并支持按业务需要调整样式兼顾生成效率与性能表现。资源包共 4 个文件分别为 js 逻辑、wxml 结构、wxss 样式及 json 配置整体仅 4KB轻量易集成适合有一定小程序基础、希望快速产出高质量分享海报的开发者参考使用。目前已有 605 人学习下载社区反馈该方案与淘宝、京东、拼多多等主流电商的分享海报实现思路一致具备较强的实用与借鉴价值。1. 为什么分享海报成了电商小程序的硬需求先聊个现象。你现在打开淘宝、京东、拼多多随便点开一个商品点“分享”出来的那张带二维码、带商品图、带价格、带平台logo的长图基本全是前端用canvas现场画出来的。为什么不做成一张静态图片放服务器上因为每个用户的二维码不同、商品价格会变、活动文案要实时替换真要后端合成图服务器扛不住那个并发而且每次改版都要后端联动效率太低。在小程序里用canvas2d画海报把图片、文字、二维码拼在一张画布上再导出成图片让用户保存或转发是当前电商类小程序的主流做法。这篇文章就围绕微信小程序的canvas2d海报生成方案来写从基础API讲起到富文本排版、圆角裁剪、保存相册再到我实际开发中踩过的坑。想给小程序做分享海报功能的或者想把旧版canvas代码迁移到新版canvas2d的都可以直接参考这里的思路和代码。2. 为什么要从旧版canvas迁移到canvas2d2.1 旧版canvas接口的局限微信小程序早期的canvas是原生组件用wx.createCanvasContext操作这套接口最大的问题是绘制状态容易错乱。举个例子你画完一张图想给下一张图重新设置字体大小如果不小心漏了setFontSize它会沿用上一次的样式。在多页面复用的场景下这种“状态残留”非常难排查。还有一个更头疼的问题旧版canvas的绘制结果在不同机型上差异很大尤其文字渲染。同样的fontSize: 28在iOS和安卓上画出来高度能差好几个像素做多行文本截断的时候特别容易踩坑。另外旧版canvas的draw回调时机不稳定经常出现“图片还没画上去就导出”的情况导致生成的图片缺块。2.2 canvas2d带来了什么改变新版canvas2d用的是Web标准接口跟浏览器里的canvasAPI基本一致。你拿到的CanvasRenderingContext2D对象在H5和各类小程序框架里都通用这意味着你在别的端上写过的绘图代码稍微改改就能搬到小程序里学习成本低很多。更重要的是canvas2d支持Promise风格的图片加载能精确控制“等图片加载完再绘制”的顺序。官方还提供了Canvas.createImage()接口配合Image.onload可以彻底解决之前那种“图片没加载完就急着画”的问题。实测下来canvas2d在渲染性能和稳定性上都要优于旧接口这也是各大电商小程序陆续把它作为基础设施的原因。2.3 dpr适配清晰度的关键直接说结论canvas2d画出来的海报糊不糊取决于你有没有做设备像素比dpr适配。// 获取canvas节点 const query wx.createSelectorQuery() query.select(#posterCanvas) .fields({ node: true, size: true }) .exec((res) { const canvas res[0].node const ctx canvas.getContext(2d) const dpr wx.getSystemInfoSync().pixelRatio const width res[0].width // 样式宽度比如 300 const height res[0].height // 样式高度比如 500 // 关键canvas的实际像素尺寸要乘以dpr canvas.width width * dpr canvas.height height * dpr ctx.scale(dpr, dpr) // 后续绘图都按逻辑像素(width/height)来不用再关心dpr })很多新手在自定义组件里拿不到canvas节点是因为没有在组件中开启options: { virtualHost: true }或者样式选择器写错了。调试时可以在s successful回调里先console.log(res)确认有没有拿到node字段拿不到就检查type2d属性有没有加。注意canvas的width和height属性指的是画布实际像素尺寸CSS里写的width: 300px; height: 500px只是它显示出来的大小。两者不匹配轻则画出的内容模糊重则导出图片是空白的。2.4 项目结构规划代码不要全堆在页面js里海报逻辑会越来越复杂。我是这样组织的utils/draw-text.js封装多行文本绘制、自动换行、省略号处理utils/draw-image.js封装圆角图片绘制、等比缩放裁剪utils/poster.js对外暴露createPoster(canvas, options)方法内部按顺序绘制背景、商品图、文案、二维码这样页面代码只需要拿到canvas节点调用createPoster传入数据源逻辑清晰也方便复用。3. canvas2d海报生成的核心原理3.1 绘制流程的整体认知海报本质是一张按坐标堆叠元素的图。写代码之前先在纸上画一张草稿图标出每个元素的x、y坐标、宽高再考虑元素之间的间距。这比边写边调坐标高效得多。一个典型电商海报的绘制顺序背景填充纯色or渐变色背景装饰图如果有商品图通常需要圆角裁剪文案标题可能两行超出省略价格信息“¥199”价格用大号红色字体单位用通用字体二维码固定大小放在右下角提示文字“长按识别小程序码”顺序的原则是先画的元素会被后画的覆盖。装饰性元素先画核心信息后画。3.2 绘制文字的两个关键问题第一中英文混排时宽度计算不准确。ctx.measureText(str).width可以测出字符串总宽度但如果你想实现“文字超出宽度就换行”需要逐字累加宽度。这里有个优化点数字和英文字母可以按整词推进不用逐字符减少计算量。第二canvas2d不支持直接画富文本文案里的关键词要单独设置颜色或字号就必须拆开绘制。比如“仅需199元立即抢购”如果想把“199”标红就分三段画先画“仅需”再画“199”最后画“元立即抢购”。绘制时注意段与段之间要动态计算偏移因为前一段的宽度可能和预估不同。我封装的多行文本函数核心逻辑如下function drawMultiLineText(ctx, text, options) { const { x, y, maxWidth, lineHeight, fontSize, color, textAlign left } options ctx.setFontSize(fontSize) ctx.setFillStyle(color) ctx.setTextAlign(textAlign) let currentLine let currentY y for (let i 0; i text.length; i) { const testLine currentLine text[i] const testWidth ctx.measureText(testLine).width if (testWidth maxWidth i 0) { // 先画已攒完的一行 ctx.fillText(currentLine, x, currentY) currentLine text[i] currentY lineHeight } else { currentLine testLine } } // 画最后一行 if (currentLine) { ctx.fillText(currentLine, x, currentY) } return currentY // 返回结束位置方便后续元素定位 }注意ctx.setTextAlign会影响fillText里x坐标的语义left时x是文字左边界center时x是文字中心线。多行文本居中对齐时x要传固定中心坐标而不是每行去算偏移。3.3 圆角图片裁剪的实现canvas2d里没有现成的“画圆角矩形图片”API需要自己画路径。原理就是拿ctx.arcTo或者ctx.arc组合出四段圆弧形成圆角矩形路径然后clip()裁剪再画图。基础版本function drawRoundRect(ctx, x, y, width, height, r) { ctx.beginPath() ctx.moveTo(x r, y) ctx.arcTo(x width, y, x width, y height, r) ctx.arcTo(x width, y height, x, y height, r) ctx.arcTo(x, y height, x, y, r) ctx.arcTo(x, y, x width, y, r) ctx.closePath() }画图片时先ctx.save()然后drawRoundRect再ctx.clip()最后ctx.drawImage()画图画完ctx.restore()恢复。坑点在这儿如果不save/restoreclip是永久生效的画完这张圆角图后续所有元素都会被裁剪。另外ctx.drawImage接收的图片源必须是canvas.createImage()创建的Image对象而且要先onload确认加载完成。如果你直接传一个网络url给drawImage会静默失败屏幕上什么都不显示也不报错。等比缩放裁剪是另一个重点。商品图可能是800x800的正方形海报里需要300x300的圆角图如果原图不是正方形就要先按“覆盖”模式裁剪再绘制而不是直接把原图拉伸否则图片会变形。function drawCoverImage(ctx, img, x, y, targetW, targetH, radius) { // 计算裁剪参数保持原图比例填满目标区域居中裁剪 const scale Math.max(targetW / img.width, targetH / img.height) const sourceW targetW / scale const sourceH targetH / scale const sourceX (img.width - sourceW) / 2 const sourceY (img.height - sourceH) / 2 ctx.save() drawRoundRect(ctx, x, y, targetW, targetH, radius) ctx.clip() ctx.drawImage( img, sourceX, sourceY, sourceW, sourceH, // 原图裁剪区域 x, y, targetW, targetH // 目标绘制区域 ) ctx.restore() }这里有9个参数很多人会记混。前4个是“从原图的哪个位置、取多大区域”后4个是“画到画布的哪个位置、多大尺寸”。中间那个scale的计算逻辑如果你想要“含边”而不是“裁剪”就把Math.max改成Math.min这个细节会导致图片是居中裁剪还是留白。3.4 二维码海报的核心二维码怎么放进去小程序码或者二维码不能直接用wx.downloadFile下载后当普通图片绘制。因为小程序码的域名不一定在downloadFile合法域名里尤其是用户自己生成的小程序码通常存在云存储域名已经配置过的没这个问题。更稳的方案让后端返回一个临时文件路径wxfile://或http://前端用canvas.createImage()加载后绘制。如果后端给的是Base64字符串可以先把Base64转成ArrayBuffer再用fs.writeFileSync写入用户目录把临时路径给Image对象加载。还有一个细节二维码区域最好加一个白色底图垫底。因为有些用户把海报保存到相册后在深色背景上直接放小程序码码的边缘容易被背景色干扰导致扫不出来。垫一个白色圆角矩形磨平这个风险。3.5 保存图片到相册的完整链路绘制完成只是前半场后半场是导出和保存。wx.canvasToTempFilePath({ canvas, // 注意新版要传canvas实例老版本传canvasId x: 0, y: 0, width: canvas.width, height: canvas.height, destWidth: canvas.width * 2, // 导出图尺寸建议2倍保证清晰 destHeight: canvas.height * 2, fileType: jpg, quality: 0.9, success(res) { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () { wx.showToast({ title: 已保存到相册, icon: success }) }, fail(err) { handleAuthDenied(err) // 处理用户拒绝授权的情况 } }) } })保存相册之前要检查授权状态。首次调用wx.saveImageToPhotosAlbum会弹授权框用户拒绝之后再调用会直接走fail回调这时候要引导用户去设置页打开相册权限。function handleAuthDenied(err) { if (err.errMsg.includes(auth deny) || err.errMsg.includes(authorize)) { wx.showModal({ title: 提示, content: 需要您授权保存图片到相册才能保存海报, confirmText: 去设置, success(res) { if (res.confirm) { wx.openSetting() } } }) } }注意在iOS上如果用户开启了“仅允许添加照片”权限iOS 14saveImageToPhotosAlbum的报错信息是saveImageToPhotosAlbum:fail不带具体的deny描述。这种情况建议把错误归类为权限问题处理引导用户去设置页。4. 完整绘制流程与代码实战4.1 先搭一个基础的海报组件以原生小程序为例我通常把海报做成一个自定义组件好处是多个页面可以共用而且可以用Canvas的type2d特性在组件内自己管理canvas节点不需要页面关心。poster.js的核心部分Component({ properties: { // 外部传入的海报数据 data: { type: Object, value: null } }, data: { canvasVisible: false, generating: false }, observers: { data: function (newData) { if (newData newData.imageUrl) { this.initAndRender(newData) } } }, lifetimes: { ready() { // 组件初始隐藏canvas需要展示时再显示 } }, methods: { initAndRender(postData) { const query this.createSelectorQuery() query.select(#posterCanvas) .fields({ node: true, size: true }) .exec((res) { if (!res || !res[0] || !res[0].node) { console.error(canvas节点获取失败) return } const canvas res[0].node const ctx canvas.getContext(2d) const dpr wx.getSystemInfoSync().pixelRatio const width res[0].width const height res[0].height canvas.width width * dpr canvas.height height * dpr ctx.scale(dpr, dpr) this.renderPoster(ctx, canvas, width, height, postData) }) }, async renderPoster(ctx, canvas, width, height, postData) { // 1. 加载所有需要的图片 const bgImg await loadImage(canvas, postData.backgroundUrl) const productImg await loadImage(canvas, postData.productUrl) const qrImg await loadImage(canvas, postData.qrUrl) // 2. 按顺序绘制 ctx.clearRect(0, 0, width, height) ctx.drawImage(bgImg, 0, 0, width, height) // 3. 画商品图 drawCoverImage(ctx, productImg, 30, 80, 300, 300, 16) // 4. 画标题 drawMultiLineText(ctx, postData.title, { x: 30, y: 410, maxWidth: width - 60, lineHeight: 34, fontSize: 28, color: #333333 }) // 5. 画价格 // ... // 6. 画二维码 drawCoverImage(ctx, qrImg, width - 130, height - 160, 100, 100, 8) // 绘制完成触发导出 this.exportCanvas(canvas) }, exportCanvas(canvas) { wx.canvasToTempFilePath({ canvas, success: (res) { this.setData({ posterTempPath: res.tempFilePath, canvasVisible: false }) this.triggerEvent(success, { tempFilePath: res.tempFilePath }) } }) } } })4.2 图片加载的完整封装loadImage封装是新手最容易出问题的点。直接用wx.getImageInfo拿到local路径再赋给Image对象是一个方案但在iOS上偶尔出现兼容问题。我更推荐直接给canvas的Image对象赋值网络urlfunction loadImage(canvas, url) { return new Promise((resolve, reject) { if (!url) { reject(new Error(图片url为空)) return } const img canvas.createImage() img.onload () resolve(img) img.onerror (err) { console.error(图片加载失败, url, err) reject(err) } img.src url }) }要注意网络图片需要在小程序后台配置downloadFile合法域名。如果在开发者工具里一切正常真机上图片不出来第一反应就该去查域名白名单。请求是https的开发工具可以勾选“不校验合法域名”来临时调试但上线前一定要配好。4.3 动态计算海报高度需求里海报高度经常不固定标题长了要换行图片尺寸比例变了高度跟着变。我通常会在绘制前先预估所有元素的高度再确定canvas的高度这样就避免内容画出去或者底部留大片空白。function calcPosterHeight(postData) { let height 0 height 20 // 上边距 height 300 // 商品图固定高度 height 20 // 商品图与标题的间距 const titleLines estimateTextLines(postData.title, 300 - 60, 28) height titleLines * 34 // 标题总高度 height 20 // 标题与价格的间距 height 50 // 价格区域高度 if (postData.showQr) { height 20 height 120 // 二维码区域高度 } height 20 // 下边距 return height }这一步的关键是estimateTextLines它模拟了绘制时的换行逻辑function estimateTextLines(text, maxWidth, fontSize) { const ctx wx.createOffscreenCanvas ? // 用离屏canvas估算宽度 wx.createOffscreenCanvas({ type: 2d, width: 1, height: 1 }).getContext(2d) : null if (!ctx) return 1 ctx.font normal ${fontSize}px sans-serif let lines 1 let currentWidth 0 for (let i 0; i text.length; i) { const charWidth ctx.measureText(text[i]).width if (currentWidth charWidth maxWidth) { lines currentWidth charWidth } else { currentWidth charWidth } } return lines }用离屏canvas计算文字宽度不用等待实际canvas节点渲染速度更快。但离屏canvas在部分安卓机上有兼容问题稳妥起见也可以直接用实际canvas的ctx来调用measureText。4.4 生成分享图的完整调用方式页面里的调用方式以点击“生成海报”按钮为例handleShare() { if (this.isGenerating) return this.isGenerating true wx.showLoading({ title: 海报生成中, mask: true }) this.setData({ canvasVisible: true, posterData: { backgroundUrl: https://xxx.com/bg.png, productUrl: productInfo.mainImage, qrUrl: this.getQrCodeUrl(productInfo.id), title: productInfo.title, price: 199, originalPrice: 299 } }, () { // 等canvas显示出来再开始绘制 setTimeout(() { // 组件内部observers触发绘制 }, 50) }) }这里有个小细节canvas组件如果一开始是隐藏的display: none或者visibility: hidden在真机上可能导致节点尺寸为0绘制出来全是空白。我实测的规律是组件先渲染到页面上等一帧再操作canvas。有时候不得不用setTimeout这个是脏但有效的办法。4.5 性能优化不要重复绘制海报的核心数据一般30秒内不会变比如商品信息、二维码没有必要每次分享都重新下载图片、重新绘制。我的方案是在内存里做缓存const posterCache {} function getCachedPoster(key) { if (posterCache[key]) { return Promise.resolve(posterCache[key]) } return generatePoster(key).then((tempFilePath) { posterCache[key] tempFilePath return tempFilePath }) }缓存key用商品ID 活动ID 用户openid组合。如果图片资源或者二维码过期比如券到期后端接口里加一个时间戳参数前端检测到变化就强制刷新缓存。5. 常见问题与排查技巧实录5.1 canvas节点获取失败现象SelectorQuery.exec回调里res[0]是null或没有node字段。原因canvas设置的是type2d但写成了type2D或者组件内部没启用virtualHost又或者canvas在自定义组件里没有在组件方法内用this.createSelectorQuery()而是用wx.createSelectorQuery()。排查先检查wxml有没有type2d和canvas-id再确认选择器能选到节点。把查询结果打印出来如果res[0].node存在而res[0].width为0基本上就是canvas没展示出来。5.2 导出图片空白现象生成的海报图片一片空白或者只有背景没有任何文字和图片。原因最典型的是图片还没加载完成就调用canvasToTempFilePath。异步操作没有同步等待。上面写的loadImage用Promise串起来就不会出现这个问题。另一个原因canvasToTempFilePath传的canvas必须是canvas实例如果你是用旧版方式传canvasId在基础库2.9以上会直接报错或者导出空图。确认你的写法是wx.canvasToTempFilePath({ canvas: canvas, // 这是canvas2d的实例 success(res) { } })5.3 图片绘制变形现象商品图被拉伸或压缩比例不对。原因没有用drawCoverImage那种等比裁剪逻辑直接drawImage(img, x, y, width, height)原图比例跟目标宽高比不一致就会被拉伸。解决统一用上面的drawCoverImage方法传目标宽高和圆角半径。唯一要注意的是cover模式的图片会居中裁剪如果原图重要内容在边缘可能会被切掉。这种情况可以把图片先缩放成“含边”模式Math.min但会留白我个人更偏好加一层半透明蒙版解决。5.4 真机文字模糊现象开发者工具里很清晰真机导出的图片文字发虚。原因导出时的destWidth和destHeight设置太小。比如canvas真实宽度是750px用dpr铺过导出时只设了width: 375, height: 667图片当然糊。解决导出参数里的width/height填写canvas的逻辑宽高destWidth/destHeight填实际像素宽高或者直接填canvas.width的一到两倍。5.5 安卓机偶发绘制不全现象同一段代码iOS上每次都正常有些安卓机偶尔图片缺了一角或者二维码没画上去。原因canvas的绘制是异步执行的绘制过程中如果页面发生重绘或者canvasToTempFilePath在绘制还没提交时就被调用了就会丢内容。解决绘制完后加一个短暂延时再导出或者使用canvas.requestAnimationFrame确保当前帧绘制完成。真机上实测加setTimeout(export, 100)能让问题大幅减少。5.6 自定义组件下canvas层级问题现象canvas盖住了弹窗、按钮等其他元素。原因在微信小程序里canvas属于原生组件同层渲染在某些老机型上没生效导致canvas永远在最上层。解决较新的基础库2.9同层渲染已经比较完善可以先升级基础库试试。如果仍然有层级问题请确认你在wxml里加的是type2d的canvas而不是旧版原生canvas旧版canvas/canvas不带type才有严重的层级问题。5.7 权限拒绝后的二次点击处理现象用户第一次拒绝授权后再点“保存海报”无论怎么调wx.saveImageToPhotosAlbum都直接fail不弹窗。原因微信对于已经被拒绝的权限不会再次弹出系统授权框必须用户主动去设置页打开。解决捕获fail后弹Modal引导wx.openSetting。但如果用户之前还没被拒绝过不要一上来就调openSetting会浪费一次交互机会。一个通用方案是先尝试保存如果失败再判断错误信息是权限问题才引导设置。以下是我常用的一套错误分类逻辑function judgeSaveError(err) { const msg err.errMsg || if (msg.includes(cancel)) { return 用户取消 } if (msg.includes(auth deny) || msg.includes(authorize)) { return 用户已拒绝授权 } if (msg.includes(fail)) { // iOS14权限受限场景 return 需要去设置开启相册权限 } return 未知错误 }5.8 小程序码识别率低现象海报生成成功用户长按没法识别小程序码。原因二维码太小、二维码被背景色干扰、或者二维码没有垫白底。小程序官方对小程序的码有最小尺寸限制建议生成时尺寸不要低于100x100px。绘制时先在二维码位置画一个白色圆角矩形再画二维码图片。// 在画二维码之前先画白底 ctx.save() ctx.setFillStyle(#FFFFFF) drawRoundRect(ctx, qrX - 8, qrY - 8, qrSize 16, qrSize 16, 16) ctx.fill() ctx.restore() // 再画二维码 drawCoverImage(ctx, qrImg, qrX, qrY, qrSize, qrSize, 8)不要小看这几行很多“能扫出来但偶尔扫不出来”的问题都是因为少了这个白底。6. 更多进阶场景与一线踩坑心得6.1 多canvas实例的性能问题如果页面同时存在多个canvas比如一个用于海报一个用于小程序码生成内存占用会明显上升。低端安卓机上尤其明显。我的做法是海报画完立刻把canvas的宽高重置为1释放内存。function releaseCanvas(canvas) { canvas.width 1 canvas.height 1 }在canvasToTempFilePath成功之后调用。这个小操作在列表页上能极大减少卡顿。6.2 远程背景图的加载策略电商海报经常需要换主题背景图如果每次都是网络图用户弱网场景下加载会很慢。我一般把背景图尽可能压缩到100KB以内并且先行下载到本地缓存海报绘制时优先读本地缓存。另外一个细节是不要把所有图片都串行加载用Promise.all并行加载多个图片通常能把绘制前的等待时间缩短一半。async function loadAllImages(canvas, urls) { const tasks urls.map(url loadImage(canvas, url)) return Promise.all(tasks) }6.3 在业务中处理图片过期很多开发者在做”分享海报“时会忽略一个风险二维码会过期。比如营销活动的短链二维码生成时有一个有效期如果用户把海报保存到相册过几天再分享出去扫出来可能是失效页。至少要做两件事给二维码拼一个expires_at时间戳海报生成时记录有效期用户扫了过期码后H5落地页主动提示“该活动已结束”6.4 不要忽略canvas的宽高和绘制坐标的关系再强调一次canvas标签的CSS宽高和canvas内部像素宽高是两套体系。绘制时所有坐标都基于逻辑尺寸CSS宽高如果canvas内部像素是750x1334CSS只显示375x667那么你要在CSS坐标下绘制必须先ctx.scale(2, 2)。打个比方你在一个画板上用真实尺寸画图然后用相机拍了一张2倍像素的照片收藏起来。scale(2,2)相当于把“1个画板厘米”映射到“2个像素点”后续你不需要关心像素只管按画板厘米画就行。6.5 真机调试时保存到相册的路径问题wx.canvasToTempFilePath返回的tempFilePath是临时文件路径在iOS和安卓上格式不同。有朋友说Android上保存出来的图旋转了90度这是canvas的物理像素和屏幕方向不匹配导致的排查时要先确认是否所有机型都有这个问题还是只有部分机型。如果只是部分机型旋转可以在success里判断res.tempFilePath的后缀比如Android的某些ROM会自动旋转jpg这时候可以用wx.compressImage重新生成一份路径可能把旋转的畸形照片矫正过来。6.6 团队协作中的代码维护经验海报绘制代码一长维护起来很容易乱。我后来总结了一套约定推荐给团队使用所有绘制坐标都定义在文件顶部的config对象里不要散布在函数中间。改版时只需要动config不用去逐行读绘图逻辑绘制函数只接收ctx和data不要直接依赖页面数据。这样方便单测也方便后续迁移到其他端日志规范图片加载失败、canvas获取失败这类关键错误统一上报到监控平台。很多线上问题比如用户反馈海报空白靠日志能快速定位const LAYOUT { background: { x: 0, y: 0, width: 375, height: 667 }, product: { x: 30, y: 80, width: 300, height: 300, radius: 16 }, title: { x: 30, y: 410, maxWidth: 315, lineHeight: 34 }, qrcode: { x: 245, y: 507, size: 100, radius: 8 } }改版只动这个对象绘制函数一行都不用改。亲测这个方案在需求频繁变动的项目里能省下大量时间。6.7 canvas2d与跨端框架的适配如果你用的是Taro、uni-app这类跨端框架canvas2d的写法略有不同。Taro里要通过Taro.createSelectorQuery()拿节点uni-app里则是uni.createSelectorQuery()。核心的CanvasRenderingContext2D API是一致的所以本文讲的绘制原理、坑点都通用。唯一要注意的是框架内部对canvas节点的生命周期管理有时候需要等组件onReady再拿节点不能直接在onLoad里操作。7. 给新手的最后几条忠告做海报分享这个功能说难不难说简单也不简单。难的地方集中在异步时序和坐标规划上多写几次就有感觉了。这里分享几个我踩过多次坑之后的习惯操作动手写代码前先把海报草图画出来标好每个元素的坐标和尺寸再用代码实现。很多人喜欢边写边调最后代码改得一塌糊涂。图片加载流程和绘制流程一定要分开。所有图片都加载成功后再统一绘制千万不要在onload回调里直接绘制这样代码可读性会很差也很难排查问题。每个机型都值得测一下dpr适配。苹果机型像素比普遍是2或3安卓机则五花八门。同一个canvas在不同dpr下要保证导出的图片清晰scale那行代码不能省。最后应用商店对分享海报的审核要求不同如果海报里包含二维码务必确认二维码跳转的目标页面合规避免因为“诱导分享”或“违规外链”被驳回。我印象很深的一次是上线前才发现安卓低端机上二维码偶尔画不上去查了半天发现是图片加载完成前就开始绘制导致drawImage传了一个未加载完成的Image对象。换用createImage加onload串行等待之后问题彻底消失。这类时序问题在canvas开发里非常典型建议像我一样固定使用异步封装不要边写边现拼。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门