網(wǎng)頁(yè)截圖踩坑記錄(html2vans+百度地圖+svg)

簡(jiǎn)介

最近項(xiàng)目中有業(yè)務(wù)是做截圖下載圖片的功能,因此很快想到之前用過(guò)的一個(gè)前端截圖庫(kù)html2canvas,主要遇到了以下幾個(gè)問(wèn)題:

  • 無(wú)法繪制svg元素
  • 無(wú)法加載跨域圖片資源
  • IE下各種亂七八糟的兼容性。(IE10+)

原本要求的是IE9+,但是在掙扎一段時(shí)間后放棄了IE9,主要是要做前端下載的功能,必須用到的BLOB,該api僅支持IE10+,沒(méi)有這個(gè)api支持的話,就只能將生成的canvas轉(zhuǎn)化成base64的形式,但是生成的圖片足足有50kb左右,所以就戰(zhàn)略性放棄了。。。

無(wú)法繪制svg元素

html2canvas的issues里也有很多反映svg繪制不出來(lái)的問(wèn)題,雖然有人說(shuō)升級(jí)版本就可以解決,但是我嘗試過(guò)很多版本都無(wú)法很完美的繪制出svg,最終我選擇了一個(gè)比較穩(wěn)定的版本v1.0.0-alpha.12
網(wǎng)上解決svg的問(wèn)題我看到過(guò)兩種方案:
1.將svg的樣式寫(xiě)在行內(nèi);
2.將svg轉(zhuǎn)化成canvas再使用html2canvas截圖;
不知道是我版本選擇的不對(duì)還是其他原因,我任選其中一種方案都不能完美的截出svg,要么沒(méi)有截出來(lái),要么就是樣式錯(cuò)亂。

我的解決方案是兩種一起上。

方案一代碼:

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下獲取到,但是會(huì)有canvg這個(gè)庫(kù)會(huì)有一個(gè)報(bào)錯(cuò)https://github.com/canvg/canvg/issues/189,因此使用xmlserializer.serializeToString(svgElem)獲取到svg。

方案一、二均在html2canvs的onclone配置里,不會(huì)改變?cè)璬om。之后svg正常出現(xiàn)在截圖里。

萬(wàn)惡的IE

在IE下有部分截圖截出來(lái)的寬高和實(shí)際目標(biāo)元素的寬高不一致,導(dǎo)致截圖被壓縮。為了定位問(wèn)題我特意查看了html2canvas的源代碼,

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

核心是node.getBoundingClientRect(),實(shí)時(shí)計(jì)算出來(lái)的寬高,而且這個(gè)api也沒(méi)有兼容性問(wèn)題。因此可以斷定不是庫(kù)的問(wèn)題,最后一遍遍的嘗試最終發(fā)現(xiàn)問(wèn)題所在。

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

最終定位出來(lái)是出錯(cuò)的截圖附近都有這么一段html, 有一點(diǎn)多此一舉。。。
將其修改成<el-button @click="submit"></el-button>的形式,問(wèn)題解決。至于原因,暫時(shí)沒(méi)有找到?。?!

無(wú)法加載跨域圖片資源

這個(gè)問(wèn)題出現(xiàn)在百度地圖這邊。項(xiàng)目里有個(gè)顯示地圖的地方,通過(guò)觀察html結(jié)構(gòu)可以發(fā)現(xiàn)地圖的背景是一小塊一小塊兒的圖片拼接起來(lái)的,圖片域和我當(dāng)前域是不同滴。先來(lái)看下html2canvs的配置,

Name Default Description
allowTaint false 是否允許 跨域圖片污染canvans畫(huà)布
proxy null 使用代理去加載跨域圖片
useCORS false 使用CORS嘗試加載圖片

與跨域相關(guān)的配置就是以上這些。

友情提示:設(shè)置allowTaint:true后canvas畫(huà)布會(huì)被污染,導(dǎo)致無(wú)法獲取數(shù)據(jù)(即無(wú)法使用toBlob(), toDataURL() 或 getImageData() 方法)https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_enabled_image,也就是說(shuō)無(wú)法下載和上傳了,那是萬(wàn)萬(wàn)不行滴。

方案一:

chrome下使用useCORS: true即可截出跨域圖片。IE下跨域請(qǐng)求報(bào)錯(cuò),導(dǎo)致截不出來(lái)。(萬(wàn)惡的IE)

方案二:

使用html2canvas官方提供的proxy,即代理。設(shè)置的是一個(gè)接口地址,官方有node實(shí)例,原理很簡(jiǎn)單,由服務(wù)器去下載跨域圖片,然后返回給前端。本地調(diào)試是可以的,非常好。但是有一個(gè)問(wèn)題,就是多了一個(gè)服務(wù)需要部署,并且還得和前端訪問(wèn)地址同域。。。IT小哥哥是萬(wàn)萬(wàn)不想給你搞得。

方案三:

終極方案。不行只能跪求IT大哥了。
在onclone里分析目標(biāo)節(jié)點(diǎn)的所有圖片資源,如果是跨域資源,先用ajax請(qǐng)求并緩存在內(nèi)存里,當(dāng)html2canvas繪制的時(shí)候只需要從內(nèi)存中讀取就ok了,看上去十分理想。直接上代碼:

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

// 下載跨域圖片,并通過(guò)URL.createObjectURL創(chuàng)建可直接訪問(wèn)流的鏈接
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,
    }))
  })
}

// 拿到跨域的圖片節(jié)點(diǎn)
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里將實(shí)際跨域圖片節(jié)點(diǎn)的src換成我們生產(chǎn)的鏈接。使用完成之后記得釋放鏈接URL.revokeObjectURL。

以上就是完美解決跨域圖片的問(wèn)題了。最后來(lái)張截圖以表尊敬。
地圖
svg

IE下兼容性

1.上面有提到過(guò):svg.outterHTML不支持。
2.canvas.toBlob方法不支持。MDN上有polyfill,這里就不貼了
3.location.href 不會(huì)觸發(fā)路由跳轉(zhuǎn)(待查明原因),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.暫時(shí)想不起來(lái)了。

補(bǔ)充IE10下各種問(wèn)題:

  1. 元素寬度計(jì)算錯(cuò)誤,導(dǎo)致截圖樣式錯(cuò)亂。

html2canvas內(nèi)部計(jì)算寬度使用的標(biāo)準(zhǔn)api,Element.getBoundingClientRect(),兼容到ie9,但是ie10上還是會(huì)出現(xiàn)計(jì)算錯(cuò)誤問(wèn)題。

1.1 Element: radio-group存在時(shí),計(jì)算寬度錯(cuò)誤, 用div實(shí)現(xiàn)了一個(gè);
1.2 Element :elect存在時(shí),計(jì)算寬度錯(cuò)誤,mounted里面手動(dòng)觸發(fā)下拉框,讓其添加到body里;
1.3 display: flex時(shí) 元素不顯示;
1.4 存在樣式box-shadow時(shí) 計(jì)算寬度錯(cuò)誤;
1.5 ul, li標(biāo)簽存在時(shí)計(jì)算寬度有問(wèn)題

以上問(wèn)題暫時(shí)未找到導(dǎo)致錯(cuò)誤的源頭,因?yàn)轫?xiàng)目緊急,臨時(shí)解決方案。
html2canvas: 1.0.0-alpha.12,
element-ui: 2.2.0

其他截圖庫(kù)

在遇到IE下截圖不完美時(shí),我也想過(guò)放棄html2canvas轉(zhuǎn)而擁抱其他庫(kù),發(fā)現(xiàn)基本都只能支持到IE10。

Name 兼容 對(duì)比
rasterizeHTML 與html2canvas對(duì)比,參考https://juejin.im/entry/58b91491570c35006c4f7fdf
html2canvas
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容