dom转image原理简述

前言

看到 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. 内嵌图片
  • 内联图片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 原理

尽量挑最核心的讲,希望不会显得很繁琐,了解核心思想就好

下面介绍几个核心函数:

  • toPng (包装了draw函数,没啥意义)
  • Draw (dom 转 canvas)
  • toSvg (dom 转 svg)
  • cloneNode (clone dom树和css样式)

调用顺序为

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)
        // clone 处理图片啊,background url('')里面的资源,顺便加载好
        .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
      }
    }

    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
        })

参考链接

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章