h5活动页经常会有生成海报分享到朋友圈的需求,虽然后端可以合成,但是会并发量大的时候会给服务器带来压力,并且比较耗时,这个业务放到前端做更合适,下面梳理几个实现方案
比如这么一张海报,上顶部是一张固定的背景图,下方是用户头像和根据接口获取的动态文案
Canvas绘制方案
画布大小
为了保证绘制出来的图片足够清晰,我们需要使用两倍的像素来绘制,比如设计的海报是320x484,那么需要设置canvas大小为640x968,当然3倍图也没问题。
放置背景图
由于图片加载是异步的,需要用promise来保证,在图片加载完成以后再进行其他处理
function imgPromise (img) {
return new Promise((resolve, reject) => {
img.onload = () => {
resolve(img.src)
}
img.onerror = (e) => {
reject(e)
}
})
}
由于图片通常托管在CDN上,在使用canvas绘制图片的时候可能会出现跨域问题,因此需要设置图片的跨域属性,这里要注意的是必须先设置跨域属性。
let queue = []
let bg = new Image()
bg.setAttribute('crossOrigin', 'anonymous')
bg.src = posterBgUrl
queue.push(this.imgPromise(bg))
出于安全考虑,跨域的的图片如果在canvas中绘制后,会导致canvas被污染,如果再想从canvas中通过如下三个API取得数据就会因为安全问题报错。
- getImageData
- toBlob
- toDataURL
设置crossOrigin属性等于告诉浏览器将要加载一张跨域的图片,同时如果图片服务器返回的跨域头,图片就能被正常使用,canvas不会被污染。 crossOrigin有两个值anonymous和use-credentials,通常从CDN加载图片不需要凭证使用anonymous 即可,如果图片服务器设置了需要cookie之类的凭证就需要使用use-credentials。
绘制圆角矩形
function roundRect (oCtx, x, y, w, h, r, borderColor) {
if (w < 2 * r) r = w / 2
if (h < 2 * r) r = h / 2
oCtx.beginPath()
oCtx.strokeStyle = borderColor
oCtx.moveTo(x + r, y)
oCtx.arcTo(x + w, y, x + w, y + h, r)
oCtx.arcTo(x + w, y + h, x, y + h, r)
oCtx.arcTo(x, y + h, x, y, r)
oCtx.arcTo(x, y, x + w, y, r)
oCtx.closePath()
oCtx.lineWidth = 1
oCtx.stroke()
ctx.save()
}
绘制图片与文字
ctx.drawImage(bg, 26, 26, 586, 692)
ctx.fillStyle = '#979797'
ctx.font = 'normal 24px Arial'
ctx.fillText('欢迎使用', 368, 820)
ctx.save()
绘制文字的时候有个问题需要注意,文字默认的baseline是alphabetic,即英文字母的baseline,如果绘制字母a的纵坐标设置为0,那么等于是a的底部的纵坐标是0,上面一部分超出了画布,因此需要设置文字的baseline为top
ctx.textBaseline = 'top'
转图片
canvas.toDataURL('image/png')
HTML2canvas方案
html2canvas是一个生成网页截屏的库,实现方案有两种方式foreignObject与canvas绘制
foreignObject
foreignObject是svg的一个标签。它使得SVG可以嵌入不同命名空间的XML元素,比如在svg内嵌入HTML代码。 借助这个特性,可以在svg内部嵌入海报的HTML模版,然后通过canvas将svg转成图片
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<style>
div {
color: white;
font: 18px serif;
height: 100%;
overflow: auto;
}
</style>
<polygon points="5,5 195,10 185,185 10,195" />
<foreignObject x="20" y="20" width="160" height="160">
<div xmlns="http://www.w3.org/1999/xhtml">
This is <code>code</code>.
<img src="https://a.com/b.png" width="100px" height="50px" alt="">
</div>
</foreignObject>
</svg>
canvas绘制DOM
此方案比较复杂,需要解析每个节点的宽高,背景色,边框等信息,然后递归绘制,这里不展开讨论
nodejs截图
浏览器原生有截图的功能,例如chrome中通过命令面板可以调出截图功能
相比上面的方案,某种程度上这可以说是最能精准还原页面的方案,那么如何通过nodejs来调用浏览器呢?
Puppeteer
Puppeteer是一个服务端的无头浏览器API库,与此相似得还有phantomjs,不过它太老了而且不在开发。
Puppeteer原生支持截图
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({
width: 640,
height: 480,
deviceScaleFactor: 1,
});
await page.goto('https://example.com');
await page.screenshot({path: 'example.png'});
await browser.close();
})();
然后通过page.setContent
这个api可以将html字符串加载到页面中,
那么前端只需要拼接好html字符串和相应的样式,然后通过调用node服务截图生成图片返回即可
梳理流程如下:
- 前端根据设计稿拼接写好html模版
- 将html字符串传给nodejs截图服务
- nodejs生成截图 并上传图片到cdn返回图片CDN地址,或者返回图片的base64码
对比
复杂度
nodejs的复杂度最高
除了需要nodejs服务端,还有服务器要求。由于puppeteer依赖的gtk3库只能在centos7才有,所以必须使用centos7的服务器,另外还有一大堆的依赖库需要安装
另外为了保证渲染的精确,还需要安装字体。
纯canvas方案麻烦的地方在与需要算图片和文本的坐标,另外还有文本换行的问题,需要和设计和交互方讨论
兼容性
纯Canvas绘制最好,nodejs方案次之,html2canvas方案的兼容性问题比较大
最终选择
- 活动页统一使用了纯canvas绘制的方案
- 开发了一个海报生成的nodejs服务,用来给某些并发量不大的业务使用
- html2canvas方案由于兼容性差被放弃