前端生成海报图技术选型与问题解决
作者:vivo 互联网大前端团队 - Tian Yuhan
本篇文章主要聚焦海报图分享这个形式,探讨纯前端在 H5&小程序内,合成海报到下载到本地、分享至社交平台整个流程中可能遇到的问题,以及如何解决。
一、引言
绝大多数的电商平台都会设计分享裂变的功能,激励用户进行分享,这是一种拉新促活的常见措施。提到分享裂变,就免不了需要生成用户专属的分享链接或者专属海报。当然分享推广的形式多种多样,有文本链接、网页链接、图片邀请码、小程序、音视频等等。
本篇文章主要聚焦海报图分享这个形式,探讨纯前端在 H5&小程序内,合成海报到下载到本地、分享至社交平台整个流程中可能遇到的问题,以及如何解决。
二、选型参考
2.1 实现方式
根据海报图生成渠道的差异,可以将海报图生成分为:客户端生成、前端生成和服务端生成,以下是从技术实现、兼容性、生成速度和功能受限四个维度进行的对比:
![](https://static001.geekbang.org/infoq/55/55c2ee738c5a2d73f97c9c37ef90b99d.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
对于排版复杂的场景: 建议使用服务端 Node.js 渲染的方式【如 Puppeteer 图片生成】;
对于排版简单,但用户请求并发大的场景:建议使用前端或客户端生成海报图;
对于需多端投放,样式动态更新的场景:建议使用前端生成海报图;
对于叠加用户交互的场景:建议使用客户端原生绘制的方式生成。
本篇文章着重聚焦前端 H5 & 小程序内生成海报图的技术实现,如果大家对服务端、客户端生成海报图的方案感兴趣,文末有相关链接可供参考。
2.2 H5 生成海报图技术选型
H5 生成海报图常推荐的方案有三种:
(1)html2canvas:基于页面中存在且可用的 DOM 结构、样式,构建“截图”,绘制到 canvas 画布中。
下面是 html2canvas 的技术解析,详细讲解可以参考文末链接。
![](https://static001.geekbang.org/infoq/c3/c3fac1a5179655a1e445e358ab876127.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
(2)dom-to-image:将 DOM 节点序列化为 XML,包装到 SVG 中,再转为图片。
下面是 dom-to-image 的技术解析,详细讲解可以参考文末链接。
![](https://static001.geekbang.org/infoq/2f/2f9bec2c92c3808cf1a9ae72860c7126.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
(3)canvas 纯原生绘制
以下是示例代码,这里不做赘述
![](https://static001.geekbang.org/infoq/47/47add78c97b6ccad5f4c046fa9b7c4ad.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
html2canvas 和 dom-to-image 都是用于在 canvas 的基础上封装的库,由于底层实现方式的不同,它们各有优缺点,以下是从几个维度进行的优缺点对比。
![](https://static001.geekbang.org/infoq/69/699a96201c0cac83a7ad28b2084716db.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
那么如何快速的确认当前的项目应该用哪个组件库呢,有没有必要逐个去验证,这里给到几点建议:
如果需要处理的页面比较复杂或者需要支持 SVG 图片渲染,在 html2canvas 中可能更好一些。
如果需要稳定的文字、图片渲染能力或者处理结构化数据的能力,在 dom-to-image 中可能更好一些。
如果页面基本是图片资源,那么使用原生 canvas 性能是最好的。
2.3 小程序生成海报图技术选型
与 H5 类似,小程序生成海报图在项目中常用的也有三种方案:
(1)wxml-to-canvas:利用微信小程序提供的 canvas 组件,将指定的 wxml 转化成 canvas 绘图,然后再将其导出为图片
![](https://static001.geekbang.org/infoq/7c/7c5ee1fc392a4bfc1dabb8bd1963a112.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
(2)Painter:利用 JSX 语法,将绘图的操作转化成绘制命令,并生成对应的 canvas 绘图
下图来源于Painter 2.0官方介绍
![](https://static001.geekbang.org/infoq/72/7212eec2bdd246f09a9c835833cdcc14.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
(3)Canvas/Canvas2D
以下是小程序 2D Canvas 官方示例代码,详见微信小程序基础能力/画布
![](https://static001.geekbang.org/infoq/58/5805b2431096d1b77cdca28e30c9b8df.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
从技术实现上看,wxml-to-canvas 是基于 2d 类型的 Canvas 组件能力,对基础库有要求,需在 2.7.0 以上。在 canvas2d 的基础上,封装了绘制 text、image 和 view 的能力,提供 wxml 绘制到 canvas 和导出图片的方法。Painter 不仅支持 2d 类型的 canvas,同时兼容了低版本 canvas 组件。同时支持更多复杂样式的绘制,对于网络素材实现了一套 LRU 存储机制,无需重复下载图片,并增加了容错机制。
从使用方法上看,wxml-to-canvas 使用相对较简单,只需要在小程序中引入 wxml-to-canvas 库,然后传入需要生成海报图的 wxml 代码和画布的宽高即可。而 Painter 需要在小程序中使用类 JSX 的语法绘制所有图形,覆盖小程序页面中相应的 canvas 组件。
针对两种不同库,wxml-to-canvas 更适合将已有的 wxml 结构转换为图片,对于复杂的定制化设计需要在 wxml 中拼接、排版的海报来说,并不是最佳选择。Painter 则更适合在小程序中进行复杂绘制操作。
但从另一个角度看,如果对绘制性能要求比较高的业务来说,wxml-to-canvas 相比 Painter 更符合诉求。当然,如果仅涉及图片间的快速组合,也可以使用 canvas 原生绘制的能力,只是要关注图像绘制和 canvas 导出的实际问题。
前面两个章节,主要解决在绘制选型上的疑虑,下一步面对的就是如何解决实现过程中出现的问题。
三、H5 常见问题
3.1 设计还原效果
设计还原除了关注最基础的字体样式、排版,同时也要兼顾深色模式适配、字号大小适配、页面缩放、浏览器兼容性。这些因素无疑给前端生成海报图带来巨大的压力,下面我也将从提到的这些方面进行展开,也可以作为大家选型参考或者解决方案的参考。
当业务需求涉及复杂的文本排版,那么建议直接放弃 canvas 原生绘制,难度大风险大耗时长。第三方库处理逻辑大同小异,本质上都是基于 DOM 转为 canvas,但并不是真正的浏览器截图,都会存在一些共性问题。
3.1.1 字体样式
如下图所示,当前设备生效的是特殊字体,那么生成海报图得到的有可能是特殊字体,出现与设计不符,不同用户生成的海报图效果不一致的情况。
![](https://static001.geekbang.org/infoq/5e/5edc6c49452ecdee719d979331395d52.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
如何规避这个问题呢?复制 DOM 并隐藏,对复制的 DOM 嵌入网络字体包,保证设计效果。如果是固定文案,也可以采用嵌入图片的方式。
3.1.2 排版
排版常遇到的问题是文本换行、超长字符打点,通常也会叠加系统字号大小、页面缩放一起出现。
![](https://static001.geekbang.org/infoq/ff/ffd367afa85bb2e8fd897ec8a8e7fb08.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
问题 1: 文本换行打点怎么办?
第三方库中一般会有文本换行相关的处理:
类似 html-to-canvas 它的处理基于"CanvasRenderingContext2D.measureText()",在绘制时测量每个字符的宽度,进行渲染位置的更新。基于这个处理方案,超长字符打点的问题是很难解决的。
而 dom-to-canvas 是基于 svg 转图片的方式,天然的具有文本排版处理的能力,这也是这个库的优势所在。
问题 2: 那如果叠加页面缩放呢?
一般 h5 都具有页面缩放适配的能力,复制 DOM 也会继承该处理,不会有太大的影响。
问题 3: 如果页面有设置大字号模式,DOM 在不同字号大小下展示样式有区别,但是需要导出图片样式保持一致呢?
那么就需要在复制 DOM 时,针对字号大小进行逆向处理,比如系统字体将字号放大 1.25 倍,在该场景下,复制 DOM 时就需要对字号进行缩小 1.25 倍。
3.1.3 适配
深色模式适配,分为两种:
一种是系统级别的反色:系统级别的反色不会侵入到 DOM,复制的 DOM 仍然与浅色模式下一致,导出的图片也不会受到影响。
一种是自定义样式的反色:自定义的反色对 DOM 样式有侵入,复制 DOM 及其样式,导出的图片可能受影响,如下图所示。只需要在复制 DOM 时将侵入的样式或者控制深色模式的判断条件去除即可。
![](https://static001.geekbang.org/infoq/f4/f4df7ee9c25402bf231a9cfe56f69b4f.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
兼容性适配,与 canvas 和 foreignObject 的兼容性相关,一般来说,不建议在 Android 低版本中使用,性能较差。
3.2 跨域问题
只要使用 canvas 绘制图片,必然绕不过的就是跨域的问题,解决跨域问题有以下几种思路:
代理服务器:通过配置服务器代理,将原本跨域的图片请求转为同域的图片请求。从根源上解决跨域问题。针对图片来源单一可控的场景、具备代理配置的能力的业务场景,建议采用该方案。
解决资源跨域:前提需要有权限访问被请求的跨域服务器,可以通过设置服务器上的 CORS 头信息“Access-Control-Allow-Origin: * 或者特定域名”来实现,然后再解决资源的跨域问题。下面就聚焦这个场景,展开介绍下:
3.2.1 crossOrigin
在请求图片时,需要增加属性 img.crossOrigin = 'anonymous' ,告知跨域服务器,本次请求可以匿名访问,不需要提供凭证。代码示例如下图所示:
![](https://static001.geekbang.org/infoq/fa/faa8a99b000a7c9ce19653c4315d574e.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
设置这个就可以一劳永逸了吗,并不是,还可能出现图片缓存引起的请求跨域问题:在同一个页面中,如果 canvas 需要绘制的图片已经加载过了,浏览器会默认缓存下来,当 canvas 使用这张图片时,浏览器会直接返回缓存的图片。这时被缓存后的图片就不展示 CORS 请求响应头携带 Access-Control-Allow-Origin 的要求了,就会导致报错。
解决方案最简单的是给跨域请求追加时间戳,但这个方案会造成资源重复请求,也可能会有缓存击穿的风险。
3.2.2 ajax+Blob/base64
通过额外发一次请求,将图片转为 blob 或者直接用 base64 的图片格式,避免单张图片频繁请求。但如果涉及到的图片比较多,也会带来内存压力问题,需要注意资源及时释放。代码示例如下图所示:
![](https://static001.geekbang.org/infoq/15/1581fe92bcbbfd06e061bb1af490c63f.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
3.3 生成性能
控制复制 DOM 的数量 原因:海报图的生成耗时与复制 dom 的数量成正比。
如果采用的是 iframe 启用的方式,减少 iframe 启动次数。
一般组件库是基于一个 DOM 生成一张海报图,如果一次需要生成多张海报图,可以合并 DOM,启用一次 iframe,批量生成多张图片。
分割操作,将文本相关耗时的操作前置,最后简单的图片+图片的合并,使用 canvas 原生绘制,也是一种提升性能的方案。
四、小程序常见问题
4.1 元素绘制
不论是使用哪种方案,在小程序绘制海报图时,需要提前设定好绘制的尺寸,甚至 wxml-to-canvas 要求为每个元素设置固定的宽度和高度,否则会出现布局错误。所以自适应高度在小程序端是不存在的,对于排版样式就进行了初步限定。
同时,元素之间是存在层级关系的,与书写的结构强关联,这个要尤为注意。
对于元素及其样式的支持是有限的,不同库的支持程度也是不一致的,可根据业务进行选择,这里简单举例对比下:
![](https://static001.geekbang.org/infoq/3b/3baafec8963be0869430df17626dcc86.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
4.2 存储问题
以下是 wxml-to-canvas 的源码,在绘制图片时,往往是绘制网络图片,需要从远程拉取图片,转为本地临时文件存储【wx.downloadFile】,canvas 绘制取临时文件地址【tempFilePath】,然后进行绘制。保存图片时再将 canvas 导出为临时文件【wx.canvasToTempFilePath】。
![](https://static001.geekbang.org/infoq/e7/e7e6cc8ebac197193cf94ebefa75496b.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
若在小程序内触发保存图片到本地【wx.saveImageToPhotosAlbum】,需要进行权限校验,这部分需要业务自行处理,如下所示。
![](https://static001.geekbang.org/infoq/7a/7a30fa7b089d5e32b11b45717282da7c.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
如果选择使用 Painter 库绘制海报图,且为了提升性能开启了 LRU 存储机制,这时就要关注缓存文件的有效性,官方也在小程序 LRU 存储设计中进行了重点讲解。
4.3 生成性能
研究过 Painter 源码的同学可能关注到在 canvas 导出图片的函数中,存在一个 300ms 的延迟,这个是为什么呢?300ms 这个延迟可以去掉吗?
![](https://static001.geekbang.org/infoq/b9/b94faaf525ac792602608908f6df1cc5.webp?x-oss-process=image%2Fresize%2Cp_80%2Fformat%2Cpng)
答案是:这个逻辑下不可以省略!否则可能出现图片缺失的问题。
Painter 提供了 canvas2d 接口和 canvas 旧接口两套逻辑,而延迟 300ms 出现在旧接口逻辑体系中。这是因为旧接口使用的是 downloadFile 将网络图片缓存到本地后再进行 drawImage,而整个绘制-导出过程中没有监听图片加载完成的步骤,因此有概率出现图片未加载完成就直接绘制的情况,进而出现内容缺失。虽然加入了定时器延长时间,但不同的机器不同的图片绘制时间不同,设定单一的延迟等待并不能从根本上解决问题,反而影响生成图片的性能。
相应的,在 canvas2d 的体系中则可以通过 Canvas.createImage 创建图片对象,并监听图片的 load 事件,在回调之后执行 ctx.drawImage 绘制到 canvas 中,这样就可以有效的解决上述问题。
针对这种场景,如果有其他比较好的解决方案,欢迎读者在评论区留言。
ps: 微信官方正在推出 Skyline/snapshot 的截图组件,也可以实现 wxml/wxss 导出图片的功能,详见微信小程序Skyline /snapshot
五、思考总结
本文主要聚焦前端生成海报图的技术选型,不同选型可能出现的问题及其解决思路。期望通过本篇文章,能够让读者在接收到相关需求时,对于一些容易踩坑的点做好前期评估。如果当前正在开发相关的需求,遇到相似的问题能够提供一些解决思路。
同时如果大家有更好的解决方案,欢迎在评论区留言。
当然,随着 AI 技术的发展,生成海报图可能不再需要通过代码逻辑实现。可以通过数据模型训练,更加智能化的快速生成营销海报图,那么以上的问题将不再是问题。我们也将持续关注在 ToC 业务中相关的实现方案,与大家一起探讨。
参考连接:
版权声明: 本文为 InfoQ 作者【vivo互联网技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/4a8a73a013b02ffcd806c2a61】。文章转载请联系作者。
评论