网页截图踩坑记录(html2vans+百度地图+svg)

简介

最近项目中有业务是做截图下载图片的功能,因此很快想到之前用过的一个前端截图库html2canvas,主要遇到了以下几个问题:

  • 无法绘制svg元素
  • 无法加载跨域图片资源
  • IE下各种乱七八糟的兼容性。(IE10+)

原本要求的是IE9+,但是在挣扎一段时间后放弃了IE9,主要是要做前端下载的功能,必须用到的BLOB,该api仅支持IE10+,没有这个api支持的话,就只能将生成的canvas转化成base64的形式,但是生成的图片足足有50kb左右,所以就战略性放弃了。。。

无法绘制svg元素

html2canvas的issues里也有很多反映svg绘制不出来的问题,虽然有人说升级版本就可以解决,但是我尝试过很多版本都无法很完美的绘制出svg,最终我选择了一个比较稳定的版本v1.0.0-alpha.12
网上解决svg的问题我看到过两种方案:
1.将svg的样式写在行内;
2.将svg转化成canvas再使用html2canvas截图;
不知道是我版本选择的不对还是其他原因,我任选其中一种方案都不能完美的截出svg,要么没有截出来,要么就是样式错乱。

我的解决方案是两种一起上。

方案一代码:

const setInlineStyles = (targetEle) => {
  const transformProperties = [
    'fill',
    'color',
    // 'font-size',
    // 'stroke',
    // 'font',
    'width',
    'height',
  ]
  const resetStyles = (node) => {
    if (!node.style) {
      return
    }
    let styles = getComputedStyle(node)
    for (let transformProperty of transformProperties) {
      node.style[transformProperty] = styles[transformProperty]
    }
    // node.style.overflow = 'visible'
    for (let child of Array.from(node.childNodes)) {
      resetStyles(child)
    }
  }
  let svgElems = Array.from(targetEle.getElementsByTagName('svg'))
  for (let svgElem of svgElems) {
    resetStyles(svgElem)
  }
}

方案二代码:

const svgToCanvas = (target) = > {
  const svgElems = Array.from(target.getElementsByTagName('svg'))
    for (let svgElem of svgElems) {
      let parentNode = svgElem.parentNode
      // let svg = (svgElem.outerHTML || new XMLSerializer().serializeToString(svgElem)).trim()
      let svg = (svgElem.outerHTML || xmlserializer.serializeToString(svgElem)).trim()
      let svgStyles = getComputedStyle(svgElem)
      let canvas = document.createElement('canvas')
      canvas.width = parseInt(svgStyles.width, 10)
      canvas.height = parseInt(svgStyles.height, 10)
      try {
        canvg(canvas, svg)
      } catch (error) {
        console.error('canvg', error)
      }
      if (svgStyles.position) {
        canvas.style.position += svgStyles.position
        canvas.style.left += svgStyles.left
        canvas.style.top += svgStyles.top
      }
      parentNode.removeChild(svgElem)
      parentNode.appendChild(canvas)
    }

    return target
}

其中svg.outterHTML在ie下不兼容,new XMLSerializer().serializeToString(svgElem)可以在ie下获取到,但是会有canvg这个库会有一个报错https://github.com/canvg/canvg/issues/189,因此使用xmlserializer.serializeToString(svgElem)获取到svg。

方案一、二均在html2canvs的onclone配置里,不会改变原dom。之后svg正常出现在截图里。

万恶的IE

在IE下有部分截图截出来的宽高和实际目标元素的宽高不一致,导致截图被压缩。为了定位问题我特意查看了html2canvas的源代码,

Bounds.fromClientRect(node.getBoundingClientRect(), scrollX, scrollY)

核心是node.getBoundingClientRect(),实时计算出来的宽高,而且这个api也没有兼容性问题。因此可以断定不是库的问题,最后一遍遍的尝试最终发现问题所在。

<form @submit="submit">
  <el-button basic-type=“submit”>搜索</el-button>
</form>

最终定位出来是出错的截图附近都有这么一段html, 有一点多此一举。。。
将其修改成<el-button @click="submit"></el-button>的形式,问题解决。至于原因,暂时没有找到!!!

无法加载跨域图片资源

这个问题出现在百度地图这边。项目里有个显示地图的地方,通过观察html结构可以发现地图的背景是一小块一小块儿的图片拼接起来的,图片域和我当前域是不同滴。先来看下html2canvs的配置,

Name Default Description
allowTaint false 是否允许 跨域图片污染canvans画布
proxy null 使用代理去加载跨域图片
useCORS false 使用CORS尝试加载图片

与跨域相关的配置就是以上这些。

友情提示:设置allowTaint:true后canvas画布会被污染,导致无法获取数据(即无法使用toBlob(), toDataURL() 或 getImageData() 方法)https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_enabled_image,也就是说无法下载和上传了,那是万万不行滴。

方案一:

chrome下使用useCORS: true即可截出跨域图片。IE下跨域请求报错,导致截不出来。(万恶的IE)

方案二:

使用html2canvas官方提供的proxy,即代理。设置的是一个接口地址,官方有node实例,原理很简单,由服务器去下载跨域图片,然后返回给前端。本地调试是可以的,非常好。但是有一个问题,就是多了一个服务需要部署,并且还得和前端访问地址同域。。。IT小哥哥是万万不想给你搞得。

方案三:

终极方案。不行只能跪求IT大哥了。
在onclone里分析目标节点的所有图片资源,如果是跨域资源,先用ajax请求并缓存在内存里,当html2canvas绘制的时候只需要从内存中读取就ok了,看上去十分理想。直接上代码:

...
const cleanImages = await downloadCrossImages(el)
const canvasImageBlob = await generateCanvas(el, opts, cleanImages)
// 释放
 cleanImages.forEach(({value}) => window.URL.revokeObjectURL(value))
...

// 下载跨域图片,并通过URL.createObjectURL创建可直接访问流的链接
const downloadCrossImages = (el) => {
  const crossImgs = getCrossImages(el, ['logo'])
  return Promise.all(crossImgs.map(node => axios({
    method: 'get',
    url: node.getAttribute('src'),
    responseType: 'blob',
  }).then(res => ({
    data: res.data,
    src: node.src,
  })))).then(res => {
    return res.map(({data, src}) => ({
      value: URL.createObjectURL(data),
      key: src,
    }))
  })
}

// 拿到跨域的图片节点
const getCrossImages = (el, exclude = []) => {
  const imgs = el.getElementsByTagName('img')
  return Array.from(imgs).filter(node => {
    const imgSrc = node.getAttribute('src') || ''
    let host = ''
    try {
      let res = imgSrc.match(/^https?:\/\/[^\/\?#:]+/) // eslint-disable-line
      host = res && res[0]
    } catch (error) {
    }
    const isCross = imgSrc && host && host !== window.location.host
    return isCross && !exclude.some(item => imgSrc.indexOf(item) > 0)
  })
}

最终在onclone里将实际跨域图片节点的src换成我们生产的链接。使用完成之后记得释放链接URL.revokeObjectURL。

以上就是完美解决跨域图片的问题了。最后来张截图以表尊敬。
地图
svg

IE下兼容性

1.上面有提到过:svg.outterHTML不支持。
2.canvas.toBlob方法不支持。MDN上有polyfill,这里就不贴了
3.location.href 不会触发路由跳转(待查明原因),polyfill:

if (!!window.ActiveXObject || 'ActiveXObject' in window) {
      window.addEventListener('hashchange', () => {
        const currentPath = window.location.hash.slice(1)
        if (router.currentRoute.path !== currentPath) {
          router.push(currentPath)
        }
      }, false)
    }

4.暂时想不起来了。

补充IE10下各种问题:

  1. 元素宽度计算错误,导致截图样式错乱。

html2canvas内部计算宽度使用的标准api,Element.getBoundingClientRect(),兼容到ie9,但是ie10上还是会出现计算错误问题。

1.1 Element: radio-group存在时,计算宽度错误, 用div实现了一个;
1.2 Element :elect存在时,计算宽度错误,mounted里面手动触发下拉框,让其添加到body里;
1.3 display: flex时 元素不显示;
1.4 存在样式box-shadow时 计算宽度错误;
1.5 ul, li标签存在时计算宽度有问题

以上问题暂时未找到导致错误的源头,因为项目紧急,临时解决方案。
html2canvas: 1.0.0-alpha.12,
element-ui: 2.2.0

其他截图库

在遇到IE下截图不完美时,我也想过放弃html2canvas转而拥抱其他库,发现基本都只能支持到IE10。

Name 兼容 对比
rasterizeHTML 与html2canvas对比,参考https://juejin.im/entry/58b91491570c35006c4f7fdf
html2canvas
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容