猪小花1号

个人签名

282篇博客

wap页生成海报功能踩坑总结(下篇)

猪小花1号2018-09-03 13:59

作者:李新新

问题三,图片跨域和CDN缓存导致报错

首先,图片跨域的问题,可以通过useCORS这个参数来解决。原理就是上面说过的crossOrigin。

html2canvas(p, {
  useCORS: true,}) //先写这个参数

而关于CND缓存,是这样的:因为我们的图片一般都是上传到CDN上,而CDN为了更快的响应,会缓存图片的返回值,而缓存的值是不带跨域的头的。因为没有跨域的头,所以js请求会被拦截。而html2canvas中,画图片之前会先preload所有的图片,这就导致了js报错:图片跨域(此处没有图,相信前端小司机应该见过很多次这个报错)

解决的思路是这样的:js中,请求图片的时候,给请求的图片链接加上个时间戳参数,这样CDN就映射不到缓存了,会回源,回源到 NOS,而NOS的图片是带跨域的头的,这样返回就不会再报错。

5.0

修改html2canvas源码的imageContainer方法,self.image.src = src + (src.split('?')[1] ? '&':'?') + (+new Date());

function ImageContainer(src, cors) {
    this.src = src;
    this.image = new Image();
    var self = this;
    this.tainted = null;
    this.promise = new Promise(function(resolve, reject) {
        self.image.onload = resolve;
        self.image.onerror = reject;
        if (cors) {
            self.image.crossOrigin = "anonymous";
        }
        // 原来是self.image.src = src,改为现在这句
        self.image.src = src + (src.split('?')[1] ? '&':'?') + (+new Date()); 
        if (self.image.complete === true) {
            resolve(self.image);
        }
    });
}

4.1

基本类似,修改loadImage方法

loadImage: function( src ) {
      var img, imageObj;
      if ( src && images[src] === undefined ) {
        // 这里,加时间戳参数
        src = src + (src.split('?')[1] ? '&':'?') + (+new Date());
        img = new Image();

另外,其实并不建议用CDN的图片,因为正好看到刘诗川的文章,开发富文本编辑器的一些经验教训,CDN会导致回源,因此请求会更慢返回。

但是,我上面提到的将含有跨域CDN图片的DOM节点渲染成图片的情况下,向CDN代理节点请求图片资源反而会比我们直接向静态资源源站点请求要来的慢,...CDN代理节点遇到一个自己没有缓存的资源,它就会向静态资源的源站点去请求,得到结果后再转发给用户,这等于说我们这个带有时间戳的图片URL的请求,不但没能利用的CDN的缓存提速,反而由CDN代理节点充当了一次中介,这显然会增加资源的返回耗时

所以建议使用NOS的图片,而不要使用NOS CDN的图片,比如:http://edu-image.nosdn.127.net/AE32703A6908FBD2A57F917F5E93A55D.jpg


到此为止,demo已经没什么大问题了,剩下的就是应用到实际工程中了。然而,在实际使用的过程中,依然遇到了一些问题:


1. 画出来的图中,只有background-image一定会出现,其他图片有概率不出现


很令人费解的情况。这个几率不算很高,点个20次大概会有一次出现这种情况。最初以为是5.0beta版本的原因,所以换成了用4.1正式版,然而换了版本之后并没有解决这个问题。


v4.1源码中对img标签和background-image的处理其实是类似的。都是先preload,然后canvas.draw。只是background-image多了一个createPattern步骤,用于处理background-repeat属性。


解决方案是把html中的图片都写成background-image,尝试后没有再出现显示不出img的情况。


另外,因为使用了v4.1版本,所以后面的问题和解决方案都是针对v4.1。


2. 有一定几率截出来的图是全白屏


经过排查,在最后一步的canvas中,还是有所有图片的,但是canvas->newcanvas后,图片不见了,所以怀疑是这句产生了问题:


newctx.drawImage(canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, newCanvas.width, newCanvas.height);

这里稍微解释下,为什么会有canvas->newcanvas这个步骤。html2canvas库中,画每个节点时 ,需要定位这个节点从canvas的哪里开始的,即需要一个(x,y)坐标。通过 ele.getBoundingClientRect可以得到这个元素在client(也就是窗口)的位置,然后从这个点开始画出该元素。如果传入的element不是body的话,意思就是我们只想要这个元素的canvas图,并不关心它在窗口中的位置。所以对前一步的canvas进行一下裁剪,重新画到一个新的canvas上面去。一图胜千言,下面上图:

newcanvas" src="http://edu-image.nosdn.127.net/7edb40cc-0630-4a0a-89b6-a4f2f90a6522.jpg?imageView&thumbnail=726y349&&quality=100">


红框框表示我们传进去的element,左边是canvas(最初的画布),右边是newcanvas(我们需要的画布)。


回到刚刚的问题,锁定了出问题的行之后,我们看一下原因。


第一,top,lef有问题,通过log查看,出问题时,top值会为-1,所以修改源码的bound方法,使top和Left始终大于等于0


_html2canvas.Util.Bounds = function (element) {
  var clientRect, bounds = {};

  if (element.getBoundingClientRect){
    clientRect = element.getBoundingClientRect();

    // bounds.top = clientRect.top;
    bounds.top = clientRect.top > 0 ? clientRect.top : 0; // 改成这个
    bounds.bottom = clientRect.bottom || (clientRect.top + clientRect.height);
    // bounds.left = clientRect.left;
    bounds.left = clientRect.left > 0 ? clientRect.left : 0; // 改成这个

    bounds.width = element.offsetWidth;
    bounds.height = element.offsetHeight;
  }

  return bounds;
};

第二,width和height有问题,可能超过了画布大小,导致画出来白图。所以同样修改源代码(经尝试,safari下width和height越界会导致白图,chrome不会),用Math.ceil取宽高


//canvas.width = (options.width ||zStack.ctx.width)*scaleBy;
//canvas.height = (options.height || zStack.ctx.height)*scaleBy;
// 改成下面
canvas.width = Math.ceil(options.width ||zStack.ctx.width)*scaleBy;
canvas.height = Math.ceil(options.height || zStack.ctx.height)*scaleBy;

3. v4.1的html2canvas对background-size: contain不兼容。


可以理解,毕竟它是解析css属性值之后画到canvas上的。不兼容background-size就导致背景图只能显示一部分。


解决方法: 不用background-size。。。


但是不用background-size时,宽高是rem的话,背景图片显示会被切断。


解决方案: 用px作为单位,根据背景图的比例来,同时用nos对背景图进行裁剪

width: 600px;
height: 800px;
background-image: url("http://edu-image.nosdn.127.net/F361D28EEC677CFD44D7C359D24E3DC0.png?imageView&thumbnail=600y800");

这里会遇到第四个问题,本来背景图是600x800的,外面的div宽高应该也写600x800,但是在手机端宽高大于屏幕尺寸时,会导致截图被切断(也就是只能画出来窗口内的内容),像这样:

原因: 在createStack方法里,对于传进去的元素,生成的canvas的宽高最大值取的窗口宽高。canvas就这么大一点,画出来的内容当然不全了。

function createStack(element, parentStack, bounds, transform) {
h2cRenderContext((!parentStack) ? documentWidth() : bounds.width , (!parentStack) ? documentHeight() : bounds.height),
...
}

解决方案: 因为父元素是没有parentStack的,所以它的ctx的宽度会取document的宽度。因此把这段代码改成如下:

function createStack(element, parentStack, bounds, transform) {
    //var ctx = h2cRenderContext((!parentStack) ? documentWidth() : bounds.width , (!parentStack) ? documentHeight() : bounds.height),
    // 改成:
    var ctx = h2cRenderContext( (!parentStack) ? (bounds.width + bounds.left): bounds.width, (!parentStack) ? (bounds.height + bounds.top) : bounds.height),

加上bounds.left是因为util.Bounds方法会计算每个元素距离client的左上顶点的距离,画canvas的时候就从这个点开始画。所以canvas的实际宽度应该是父元素宽度+父元素左边距离窗口的偏移值。

顺便说一下,这里改完了之后,canvas的模糊问题也解决了……因为元素600x800,所以canvas大小也是600x800,而实际应用中,在移动端显示的图片style的宽高并不大,所以看起来不糊了。

4. iphone手机升级IOS11后,当模板里有文字和《混排时,会发生文字位置错乱的现象

话不多说,看图(欢迎大家扫码买课):

本来的课程名应该是:Excel从入门到忘记。这里表现为:文字缺失和文字重叠

经过观察,有以下规律:

  1. 《后面的文字会被吞掉1-2个。
  2. 末尾的》始终显示不出来,
  3. 倒数第n个字会发生重叠。

最初怀疑是font-family对字符集的支持不够完善导致的,但是safari下看了下,html元素的显示是正确的,只有画到canvas上之后才错乱。

然后仔细观察html模板(我们使用的是regular),怀疑是模板渲染之后,《和{xxx}混排导致了这个问题,于是修改模板为:

<!--原来的-->
<div class="courseName">
《{courseData.productName}》
</div>

<div class="courseName">{'《'+courseData.productName+'》'}</div>

问题解决。

探究原因,是《导致的吗?于是把模板中的《换成%,无果,仍然错乱。所以,单个《是不会有问题的,有问题的是《{xxx}》混在一起。

再看下源码,html2canvas是怎么画文字的呢?

取textNode,然后遍历textNode中的每一个字,用document.createRange方法创建一个range,然后设置这个range的范围,最后用getBoundingClientRect计算出这个字符应该占的大小,然后画到canvas上。关键代码:

var range = doc.createRange();
range.setStart(textNode, textOffset);
range.setEnd(textNode, textOffset + text.length);
return range.getBoundingClientRect();

而符号与模板中变量引用混排时,会变成:

这其实是3个textNode。

打个log看一下在处理这3个textNode的时候,每个text的left和top值,发现:

  1. 第一个textNode,即内容为《的这个,位置是正确的
  2. 第二个textNode的开头,range.getBoundingClientRect()的结果是top:0,left:0,所以没有出现。导致文字缺失
  3. 第三个textNode,即》这个,top:0,left:0。所以同样没有出现,导致文字缺失

那么文字重叠是因为什么呢,有一组数据:

text=w,left=270.39...,right=287.609359
text=微,left=287.59375...

可以观察到,前一个字符的right比后一个字符的left大。这应该是"w"和"微"导致重叠的原因。那么为什么会这样呢?是升级后safari的bug?还是createRange和getBoundingClientRect的兼容性问题?到这里我也不知道了,毕竟IOS11的safari连不上电脑,只能真机打LOG调试,难度太大。

总结

文章写到这里就要结束了。总结一下,本文从我们产品大大的需求开始,分析了需求的实现方式和思路整理,并进行了html转成canvas的调研:

有两种方案

  1. 用svg的foreignObject作为中转。缺点是safari下对外域图片有安全性报错。
  2. 使用html2canvas.js库。

我们最后使用的是html2canvas库。

然后分享了在使用html2canvas过程中,遇到了一些问题和最后的解决方案:

  1. 怎么画出不显示的元素
  2. 图片模糊怎么办
  3. 图片跨域和CDN缓存导致报错

以及脱离了demo环境,在实际工程中使用时候遇到的问题和解决方案:

  1. 画出来的图中,只有background-image一定会出现,其他图片有概率不出现
  2. 有一定几率截出来的图是全白屏
  3. v4.1的html2canvas对background-size: contain不兼容
  4. iphone手机升级IOS11后,当模板里有文字和《混排时,会发生文字位置错乱的现象

希望能对需要做wap页生成海报功能的各位小伙伴,以及因为各种原因需要使用html2canvas.js库并且在踩坑的小司机们有所帮助。

(产品大大们,这些坑就是这个功能周五上线失败、周六上线失败、最后周日才上线的原因……希望你们满意这个解释。嗯。)

参考

CORS enabled image

SVG 简介与截图等应用

开发富文本编辑器的一些经验教训


相关文章:wap页生成海报功能踩坑总结(上篇)

网易云大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者李新新授权发布。