前言
看到 TJ 大神 star了dom-to-image,也一直很好奇html
怎麼轉 image
那麼就翻下原始碼,看下是如何實現的,其實一共就不到800行程式碼,還蠻容易讀懂的
工作原理
使用svg
的一個特性,允許在<foreignobject>
標籤中包含任意的html
內容。(主要是 XMLSerializer | MDN這個api
將dom
轉為svg
)
所以,為了渲染那個dom
節點,你需要採取以下步驟:
- 遞迴
clone
原始的dom
節點 - 獲取 節點以及子節點 上的
computed style
,並將這些樣式新增進新建的style標籤中(不要忘記了clone 偽元素的樣式) - 嵌入網頁字型
- 找到所有的
@font-face
- 解析URL資源,並下載對應的資源
- base64編碼和內聯資源 作為
data:
URLS引用 - 把上面處理完的
css rules
全部都放進<style>
中,並把標籤加入到clone的節點中去
- 內嵌圖片(都轉成dataUrl)
- 內聯圖片src 的url 進
<img>元素
- 背景圖片 使用 background css 屬性,類似fonts的使用方式
- 序列化 clone 的 dom 節點 為
svg
- 將xml包裝到
<foreignobject>
標籤中,放入svg
中,然後將其作為data: url
- 將png內容或原始資料作為
uint8array
獲取,使用svg作為源建立一個img
標籤,並將其渲染到新建立的canvas
上,然後把canvas
轉為base64
- 完成
核心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樣式)
- 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
})
}
複製程式碼