自己動手實現一個html2canvas
昨天寫了ofollow,noindex">新手引導動畫的4種實現方式
,
裡面用到了html2canvas
於是就順便了解了一下實現思路.
大概就是 利用svg
的foreignObject
標籤, 嵌入 dom, 最後再利用 canvas 繪製 svg. 從而實現最終目的.
先讓大家看看效果
MDN示例
var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); var data = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">' + '<foreignObject width="100%" height="100%">' + '<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px">' + '<em>I</em> like' + '<span style="color:white; text-shadow:0 0 2px blue;">' + 'cheese</span>' + '</div>' + '</foreignObject>' + '</svg>'; var DOMURL = window.URL || window.webkitURL || window; var img = new Image(); var svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'}); var url = DOMURL.createObjectURL(svg); img.onload = function () { ctx.drawImage(img, 0, 0); DOMURL.revokeObjectURL(url); } img.src = url; 複製程式碼
MDN示例其實寫的很清楚,不過也相對比較簡單一點, dom 是已經構建好的字串, 其實我覺得整個過程裡面最麻煩的就是構建 dom. 所以接下來,我們就來看看具體怎麼實現吧
第一步 遍歷目標節點的所有子元素,並構建對應的字串
/** * 遞迴遍歷所有子節點 * @param element Document Element 要計算的元素 * @param isTop Boolean 是否是最外層元素 **/ function renderDom (element, isTop) { let tag = element.tagName.toLowerCase() let str = `<${tag} ` // 最外層的節點,需要加 xmlns 名稱空間 isTop && (str += `xmlns="http://www.w3.org/1999/xhtml" `) str += ` style="${getElementStyles(element)}">\n` if (element.children.length) { // 遞迴子元素 for (let el of element.children) { str += renderDom(el) } } else { str += element.innerHTML } str += `</${tag}>\n` return str } 複製程式碼
這裡只做了一個最簡單的處理,由於是簡單實現,很多特殊情況沒考慮進去(如:單標籤, img等),有興趣的童鞋可以自己嘗試實現看看.
最外層的元素, 需要加名稱空間,否則無法識別
這裡用到的getElementStyles
就是獲取元素的最終渲染樣式,下一步會實現.
第二步, 獲取元素的最終渲染樣式,並拼接成行內樣式
正常的 dom 元素, 是無法直接放在foreignObject
裡面準確地渲染的, 因為還要涉及到父子元素直接的屬性繼承, 元素預設屬性, 非行內樣式無法渲染等問題.
所以我們要獲取每個元素的最終渲染樣式
, 然後拼接成行內樣式.
如何獲取元素的最終渲染樣式呢? 剛好,瀏覽器有提供一個window.getComputedStyle()
方法可以做到.
// 計算每個 dom 的樣式 // 這裡本來應該直接用 Object.keys + forEach 遍歷取出的 // 但是不知道為什麼,遍歷取出的,會渲染不出來,應該是某些屬性有問題 // 暫時沒空去排查那些有問題,所以目前先把常用的直接寫死. function getElementStyles (el) { let css = window.getComputedStyle(el) let style = '' // 尺寸相關 style += `width:${css.width};` style += `height: ${css.height};` style += `line-height: ${css.lineHeight};` style += `max-height: ${css.maxHeight};` style += `min-height: ${css.minHeight};` style += `max-width: ${css.maxWidth};` style += `min-width: ${css.minWidth};` style += `font-size: ${css.fontSize};` // 顏色相關 style += `color: ${css.color};` style += `background: ${css.background};` // 邊框相關 style += `border: ${css.border};` style += `box-sizing: ${css.boxSizing};` // 位置相關 style += `margin: ${css.margin};` style += `padding: ${css.padding};` style += `position: ${css.position};` style += `left: ${css.left};` style += `right: ${css.right};` style += `top: ${css.top};` style += `bottom: ${css.bottom};` // 佈局相關 style += `display: ${css.display};` style += `flex: ${css.flex};` return style } 複製程式碼
第三步, 渲染 svg
把拼接好的 svg 字串用Blob 物件
new 出來(Blob真的是個很強大的物件啊), 然後用DOMURL.createObjectURL()
轉換為 url,
有了url, 接下來就看大家自由發揮了. 可以直接下載,也可以在 canvas 裡繪製. 或者當作圖片直接插入到文件...
// 主入口函式 function shotScreen () { let target = document.querySelector('.content') let data = getSvgDomString(target) let DOMURL = window.URL || window.webkitURL || window; let img = new Image(); let svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'}); let url = DOMURL.createObjectURL(svg); img.src = url; document.body.appendChild(img) } // 計算 svg 的字串 function getSvgDomString (element) { return ` <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">\n <foreignObject width="100%" height="100%">\n ${renderDom(element, 1)} </foreignObject>\n </svg>` } 複製程式碼
這裡順便給個繪製到 canvas 裡的程式碼
//如果想畫到 canvas 裡面 let canvas = document.getElementById('canvas'); let ctx = canvas.getContext('2d'); let img = new Image(); img.onload = function () { ctx.drawImage(img, 0, 0); DOMURL.revokeObjectURL(url); } 複製程式碼