html2image原理簡述

三命發表於2018-03-06

html2image原理簡述

前言

看到 TJ 大神 star了dom-to-image,也一直很好奇html怎麼轉 image

那麼就翻下原始碼,看下是如何實現的,其實一共就不到800行程式碼,還蠻容易讀懂的

工作原理

使用svg的一個特性,允許在<foreignobject>標籤中包含任意的html內容。(主要是 XMLSerializer | MDN這個apidom轉為svg) 所以,為了渲染那個dom節點,你需要採取以下步驟:

  1. 遞迴 clone 原始的 dom 節點
  2. 獲取 節點以及子節點 上的 computed style,並將這些樣式新增進新建的style標籤中(不要忘記了clone 偽元素的樣式)
  3. 嵌入網頁字型
  • 找到所有的@font-face
  • 解析URL資源,並下載對應的資源
  • base64編碼和內聯資源 作為 data: URLS引用
  • 把上面處理完的css rules全部都放進<style>中,並把標籤加入到clone的節點中去
  1. 內嵌圖片(都轉成dataUrl)
  • 內聯圖片src 的url 進 <img>元素
  • 背景圖片 使用 background css 屬性,類似fonts的使用方式
  1. 序列化 clone 的 dom 節點 為 svg
  2. 將xml包裝到<foreignobject>標籤中,放入svg中,然後將其作為data: url
  3. 將png內容或原始資料作為uint8array獲取,使用svg作為源建立一個img標籤,並將其渲染到新建立的canvas上,然後把canvas轉為base64
  4. 完成

核心API

import domtoimage from 'dom-to-image'
複製程式碼

domtoimage 有如下一些方法:

    * toSvg (`dom` 轉 `svg`)
    * toPng (`dom` 轉 `png`)
    * toJpeg (`dom` 轉 `jpg`)
    * toBlob (`dom` 轉 `blob`)
    * toPixelData (`dom` 轉 畫素資料)
複製程式碼

見名知意,名字取得非常好

下面我挑一個toPng來簡單解析一下原理,其他的原理也都是類似的


分析 toPng 原理

儘量挑最核心的講,希望不會顯得很繁瑣,瞭解核心思想就好

html2image原理簡述

下面介紹幾個核心函式:

  • toPng (包裝了draw函式,沒啥意義)
  • Draw (dom => canvas)
  • toSvg (dom => svg)
  • cloneNode (clone dom樹和css樣式)
  • makeSvgDataUri (dom => svg => data:uri)

呼叫順序為

toPng 呼叫 Draw
Draw 呼叫 toSvg
toSvg 呼叫 cloneNode
複製程式碼

toPng方法:

// 裡面其實就是呼叫了 draw 方法,promise返回的是一個canvas物件
function toPng(node, options) {
    return draw(node, options || {})
        .then(function (canvas) {
            return canvas.toDataURL();
        });
}
複製程式碼

Draw方法

function draw(domNode, options) {
    // 將 dom 節點轉為 svg(data: url形式的svg)
    return toSvg(domNode, options)    
        // util.makeImage 將 canvas 轉為 new Image(uri)
        .then(util.makeImage)
        .then(util.delay(100))
        .then(function (image) {
            var canvas = newCanvas(domNode);
            canvas.getContext('2d').drawImage(image, 0, 0);
            return canvas;
        });

    // 建立一個空的 canvas 節點
    function newCanvas(domNode) {
        var canvas = document.createElement('canvas');
        canvas.width = options.width || util.width(domNode);
        canvas.height = options.height || util.height(domNode);
		  ......
        return canvas;
    }
}
複製程式碼

toSvg方法

  function toSvg (node, options) {
    options = options || {}
    // 設定一些預設值,如果option是空的話
    copyOptions(options)

    return (
      Promise.resolve(node)
        .then(function (node) {
          // clone dom 樹
          return cloneNode(node, options.filter, true)
        })
        // 把字型相關的csstext 全部都新建一個 stylesheet 新增進去
        .then(embedFonts)
        // 處理img和background url('')裡面的資源,轉成dataUrl
        .then(inlineImages)
        // 把option 裡面的一些 style 放進stylesheet裡面
        .then(applyOptions)
        .then(function (clone) {
          // node 節點序列化成 svg
          return makeSvgDataUri(
            clone,
            // util.width 就是 getComputedStyle 獲取節點的寬
            options.width || util.width(node),
            options.height || util.height(node)
          )
        })
    )
	  // 設定一些預設值
    function applyOptions (clone) {
		......
      return clone
    }
  }
複製程式碼

cloneNode 方法

  function cloneNode (node, filter, root) {
    if (!root && filter && !filter(node)) return Promise.resolve()

    return (
      Promise.resolve(node)
        .then(makeNodeCopy)
        .then(function (clone) {
          return cloneChildren(node, clone, filter)
        })
        .then(function (clone) {
          return processClone(node, clone)
        })
    )
    // makeNodeCopy
    // 如果不是canvas 節點的話,就clone
    // 是的話,就返回 canvas轉image的 img 物件
    function makeNodeCopy (node) {
      if (node instanceof HTMLCanvasElement) { return util.makeImage(node.toDataURL()) }
      return node.cloneNode(false)
    }
    // clone 子節點 (如果存在的話)
    function cloneChildren (original, clone, filter) {
      var children = original.childNodes
      if (children.length === 0) return Promise.resolve(clone)

      return cloneChildrenInOrder(clone, util.asArray(children), filter).then(
        function () {
          return clone
        }
      )
      // 遞迴 clone 節點
      function cloneChildrenInOrder (parent, children, filter) {
        var done = Promise.resolve()
        children.forEach(function (child) {
          done = done
            .then(function () {
              return cloneNode(child, filter)
            })
            .then(function (childClone) {
              if (childClone) parent.appendChild(childClone)
            })
        })
        return done
      }
    }
    
    // 處理新增dom的css,處理svg
    function processClone (original, clone) {
      if (!(clone instanceof Element)) return clone

      return Promise.resolve()
        // 讀取節點的getComputedStyle,新增進css中
        .then(cloneStyle)
        // 獲取偽類的css,新增進css
        .then(clonePseudoElements)
        // 讀取 input textarea 的value
        .then(copyUserInput)
        // 設定svg 的 xmlns
        // 名稱空間宣告由xmlns屬性提供。此屬性表示<svg>標記及其子標記屬於名稱空間為“http://www.w3.org/2000/svg”的XML方言
        .then(fixSvg)
        .then(function () {
          return clone
        })

複製程式碼

下面是這篇的重點 把 html 節點序列化成 svg

  // node 節點序列化成 svg
  function makeSvgDataUri (node, width, height) {
    return Promise.resolve(node)
      .then(function (node) {
        node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')

        // XMLSerializer 物件使你能夠把一個 XML 文件或 Node 物件轉化或“序列化”為未解析的 XML 標記的一個字串。
        // 要使用一個 XMLSerializer,使用不帶引數的建構函式例項化它,然後呼叫其 serializeToString() 方法:
        return new XMLSerializer().serializeToString(node)
      })
      // escapeXhtml程式碼是string.replace(/#/g, '%23').replace(/\n/g, '%0A')
      .then(util.escapeXhtml)
      .then(function (xhtml) {
        return (
          '<foreignObject x="0" y="0" width="100%" height="100%">' +
          xhtml +
          '</foreignObject>'
        )
      })
      // 變成svg
      .then(function (foreignObject) {
        return (
          '<svg xmlns="http://www.w3.org/2000/svg" width="' +
          width +
          '" height="' +
          height +
          '">' +
          foreignObject +
          '</svg>'
        )
      })
      // 變成 data: url
      .then(function (svg) {
        return 'data:image/svg+xml;charset=utf-8,' + svg
      })
  }

複製程式碼

參考連結