1 序章
《你并不总是需要html2canvas》将会是系列文章,逐步讲解如何使用canvas原生api来实现我们的截图需求,文章将围绕不使用html2canvas需要解决的一系列问题来展开,期望能给大家带来如下收获:
- 熟悉canvas原生api(但不像接口文档);
- 了解到html2canvas解决了哪些问题,而我们自己可以怎么解决;
- 分析需求,按需使用或制造“轮子”的能力;
2 背景
相信H5截图分享的功能大家都做过,而且会反复做。凡是反复做的事情,我都会想:“下一次怎么做得更好,或者更快”,毕竟我是一个“懒惰”而又“没有耐心”的人。懒惰驱动我提高生产力,没有耐心驱使我在同样一件事情上不断挖掘新的东西,否则容易在重复中失去耐心。
说到截图,大名鼎鼎的html2canvas - Screenshots with javascript大家肯定耳熟能详,强大,易用。感谢开源社区的贡献,确实帮我们节省了很多力气。
// 使用示例
html2canvas(document.querySelector("#capture")).then(canvas => {
document.body.appendChild(canvas)
});

但在使用过程中我也遇到了一些问题:
- 体积大(gzip 40k,未gzip 161k) - 通用的开源模块大而全也是常态;
- 黑盒 - 和预期不符只能搜索求助,比较难定位;
- 不好个性化处理 - 例如显示和截图不一致时,需要多维护一套截图用的DOM;
- 不好优化绘制耗时 - 例如在重复绘图时,可以把一些固定不变的内容绘制到一个缓存canvas上面,后续把这个canvas绘制到目标canvas即可,这个缓存canvas的内容不用反复绘制;
因此,简单分析截图分享的需求(如上图所示)后,我决定试试自己直接绘制,于是就开始了我的踩坑之旅。如果你也想为了一点点的体积优化而不使用html2canvas,那请做好心理准备解决下列问题:
- 适配(1/2/3倍图...)
- 图片绘制(头像圆形裁剪...)
- 文字绘制(定位,局部高亮,自动换行,自动省略...)
- ...
本系列文章将逐步补充和解决我做各种各样的截图需求遇到的问题,也欢迎大家补充到留言里面,我会抽空解决。
作为系列文章的第一篇,和大家介绍了一下出发点,后续不再赘述,下面进到正题。
3 截图适配
3.1 问题是什么
截图适配,主要指布局适配、图片分辨率和字体大小适配。众所周知,不同的设备显示尺寸、显示精度可能不一样,所以同样的代码在不同的设备上运行会显示不完全一样的图像,会导致如下截屏问题:
- 截图尺寸不一样,导致iPhone6不能一屏显示完iPhoneX的截屏:iPhone6下整屏截屏的图片尺寸是375*667,而iPhoneX的尺寸为375*812;
- 元素大小、位置不一样:因为页面适配(rem、百分比、flex...)的缘故,屏幕尺寸越大,实际显示元素的尺寸也越大,同时元素的top和left位置也会不一样。例如
375px
宽的屏幕居中显示175px
宽的图片时,图片left = 100px
,而600px
宽的屏幕居中显示一样的图片(图片尺寸为175 * 600/375 = 280px
)时,图片left = (600 - 280) / 2 = 160px
; - 截图显示精度不一样,导致iPhoneX看iPhone6的截屏会觉得模糊:iPhone6的devicePixelRatio为2,iPhoneX为3;
3.2 解决
解决问题前,我们先明确我们的截图分享需求:生成一张“看起来”一样的图片。
为了看起来一样,我们首先会确定一个截图的尺寸,以750px
视觉稿为基准,假设截图是750 * 1280
。视觉稿给的750px
是以iPhone6为基准的两倍图,所以在iPhone6上完美显示,但是iPhoneX应该使用三倍图,因此750 * 1280
在iPhoneX上显示会觉得有些模糊(特别是文字,认真看能看到锯齿),因此我们有两个解决方法:
方案 | 优点 | 缺点 |
---|---|---|
1. 所有设备都按照三倍图的尺寸绘制(甚至更大尺寸) | 生成的图片完全一致 | canvas绘制图片越大,耗时越长 |
2. 不同设备安装自己的显示精度进行绘制 | 在当前设备显示清晰 | 分享出去的图片在更高显示精度的设备上会感觉到模糊 |
两种方案可以按需挑选,假设需要频繁绘制,优先选择方案2,假设只需要绘制1次,并且对绘制内容有较高的视觉要求,优先选择方案1。方案2可以理解为方案1的一种特例,下面重点说方案1的实现,大家可以推导出方案1怎么实现:
- 以
750px
视觉稿为基准,根据当前屏幕尺寸和屏幕显示精度计算视觉稿到屏幕的缩放关系:scale = window.innerWidth / 750 * window.devicePixelRatio
; - 在视觉稿中量取绘制元素的大小,位置,绘制的时候根据上面的
scale
进行缩放即可; - 【方案1】不同的地方在于:
window.innerWidth = 375
,window.devicePixelRatio = 3
;
3.3 实践
理解了适配的原理之后,具体的编码工作就水到渠成了,下面主要是补充操作细节。
3.3.1 图片预加载 & 跨域设置
绘制图片的第一步是加载图片,只有在图片加载完成后,才能把图片内容绘制到canvas上。其次,出于浏览器内容安全考虑,仅有支持跨域访问的图片才能从canvas上导出成base64编码。
// 预加载图片 & 跨域设置
const imgs = ['avatar.png', 'bg_screenshot.jpg', 'container.png'];
const imgEls = {};
let loadCount = 0;
const imgLoad = (callback) => {
loadCount ++;
if (loadCount === imgs.length) callback();
}
const preloadImg = (callback) => {
imgs.forEach((imgUrl) => {
let img = document.createElement('img');
img.crossOrigin = 'Anonymous'; // 【重要】设置跨域,服务器需要返回跨域支持
img.onload = ()=>{
imgEls[imgUrl] = img;
imgLoad(callback);
};
img.src = './img/' + imgUrl;
});
}
// 使用
preloadImg(() => {
console.log('img loaded!');
});
注意:
img.crossOrigin = 'Annoymous
只是将该图片请求设置为一个跨域请求,这个时候,如果服务器没有设置合适的CORS,浏览器会提示图片加载失败,因此这里需要后端配合设置:Access-Control-Allow-Origin:*
。
3.3.2 高清图 & 位置适配
高清图这个概念和两倍图、三倍图的概念一样,不理解的同学可以去搜索一下。举个例子,iPhone6下window.devicePixelRatio = 2
, 而window.innerWidth = 375
,因此需要给750px
宽的视觉稿才能在手机上看到和视觉稿一样的效果。而iPhoneX是三倍图,因此需要375 * 3
的背景图,但是视觉设计师一般只会给二倍图,所以这个时候在iPhoneX下图片显示没有视觉稿那么清晰,如果对视觉有要求,还需要设计师给三倍图(除非是矢量图,我们可以自由缩放)。
理解了高清图之后,我们截屏的canvas大小就可以计算出来了,和高清图一样,在window.devicePixelRatio = 2
的设备上,我们会将canvas设置成window.innerWidth
的两倍,最后生成的图片就相当于是两倍图,能在该设备清晰显示。
在canvas大小已知的基础上,我们就可以从视觉稿中量出绘制元素的宽度,left,top信息,直接缩放绘制到canvas上了。
const scale = window.innerWidth / 750 * window.devicePixelRatio; // 【重要】以750px(视觉稿给的宽度往往是750px)为基准计算canvas的宽度
const ratio = 1.7; // 截图的高宽比
const width = 750 * scale;
const height = width * ratio;
screenshotCanvas = document.createElement('canvas');
screenshotCanvas.width = width;
screenshotCanvas.height = height;
const ctx = screenshotCanvas.getContext('2d');
ctx.drawImage(imgEls['container.png'], (750 - 643) / 2 * scale, 184 * scale, 634 * scale, 843 * scale ); //【重要】 1. 图片水平居中,top从视觉稿中量出来是184 2. 634 * 843是视觉稿中图片的尺寸
3.3.3 圆形头像绘制
微信后台给到的用户头像都是方形的,如果需要绘制圆形头像,可以先设置头像圆形区域,裁剪后进行绘制,这个时候只有在圆形区域的内容才会被绘制进去,然后恢复ctx设置(ctx.save() / ctx.restore()
)继续绘制别的内容即可。
ctx.save();
const avatarR = 103 * scale / 2; // 头像半径
ctx.arc(540 * scale + avatarR, 244 * scale + avatarR, avatarR, 0, Math.PI * 2, false); // 设置特定区域
ctx.clip(); // 裁剪区域,仅绘制特定区域内的内容
ctx.drawImage(imgEls['avatar.png'], 540 * scale, 244 * scale, 103 * scale, 103 * scale );
ctx.restore();
3.3.4 绘制文字 & 适配大小
文字大小我们经常用rem
来适配,但是canvas绘制只支持px
单位,因此需要我们自己计算,其实和上面的高清图一样,等比缩放即可:
const getFont = (size) => {
return size * scale + 'px serif'; // 可以按需设置字体
};
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; // 【重要】 文本设置垂直居中,默认为向上对齐,但是不同浏览器向上对齐的表现不一样,因此使用垂直居中,下面的591.5, 380相当于文案的中心点位置
ctx.font = getFont(26); // 【重要】计算字号大小,视觉稿上字号大小为26,实际大小为26 * scale
看上面的注释大家应该也看到了,关于字体的垂直位置设置有个坑,字体绘制的时候,默认是顶部对齐的,但是不同浏览器顶部对齐的表现不同,因此改用垂直居中。假设原来的top是400,字体高度为100,那设置ctx.textBaseline = 'middle'
后,绘制字体的y位置就是400 + 100 / 2 = 450
。
参考:Canvas文字绘制top位置不一致问题
5 End
截图分享需求,大都是绘制图片、分享文案和用户信息,很多时候并不需要出动html2canvas这种“重型武器”,了解如何做适配和查阅绘制相关的api,你也可以轻松实现截图需求。
前方还有很多问题等着我们去解决,例如文字绘制怎么局部高亮、自动换行和自动省略等等,下一篇博客见,同时期待与大家共同成长。
附:demo源码
写完后发现系统相关推荐的一篇文章讲截图(主要用了html2canvas)讲得比较全面,这里引用一下,大家可以结合起来学习: