JS實現(xiàn)網(wǎng)頁截圖的三種方式對比
網(wǎng)頁截圖的三種方法
- html2canvas (github.com/niklasvh/html2canvas)
- snapdom (github.com/zumerlab/snapdom/tree/main)
- html-to-image (github.com/bubkoo/html-to-image)
我讓Ai幫我生成了一個官網(wǎng)首頁的長頁面,高度大概是7380px.接下來測試一下同樣生成png的時間。
截圖對比
實現(xiàn)截圖的代碼如下。主要通過埋入當(dāng)前時間與最后生成圖片的時間來做對比。
const captureForHtml2canvas = async () => {
let time1 = Date.now()
const el = document.getElementById('captureBody');
// 把 DOM 節(jié)點轉(zhuǎn)成 canvas
const canvas = await html2canvas(el, {
backgroundColor: null,
useCORS: true, // 允許跨域圖片(如果有)
});
let time2 = Date.now()
console.log(`html2canvas 生成圖片耗時:${time2 - time1}ms`)
// 轉(zhuǎn)成 base64 PNG
const dataUrl = canvas.toDataURL("image/png");
// 生成下載鏈接并觸發(fā)
const link = document.createElement("a");
link.href = dataUrl;
link.download = "captureForHtml2canvas.png";
link.click();
}
const captureForSnapdom = async () => {
let time1 = Date.now()
const el = document.getElementById('captureBody');
// const result = await snapdom(el, { scale: 1 });
const canvas = await snapdom.toCanvas(el)
let time2 = Date.now()
console.log(`snapdom 生成圖片耗時:${time2 - time1}ms`)
// 轉(zhuǎn)成 base64 PNG
const dataUrl = canvas.toDataURL("image/png");
// 生成下載鏈接并觸發(fā)
var link = document.createElement('a');
link.download = 'captureForSnapdom.png';
link.href = dataUrl;
link.click();
}
const captureForHtml2img = () => {
let time1 = Date.now()
htmlToImage
.toCanvas(document.getElementById('captureBody'))
.then((canvas) => {
let time2 = Date.now()
console.log(`html2img 生成圖片耗時:${time2 - time1}ms`)
const dataUrl = canvas.toDataURL("image/png");
var link = document.createElement('a');
link.download = 'captureForHtml2img.png';
link.href = dataUrl;
link.click();
});
}
首先測試了長頁面的結(jié)果如下:
html2canvas 生成圖片耗時:377ms
snapdom 生成圖片耗時:1771ms
html2img 生成圖片耗時:297ms
然后我把長頁面改成高度只有1168px的再測一次
html2canvas 生成圖片耗時:273ms
snapdom 生成圖片耗時:547ms
html2img 生成圖片耗時:60ms
這個測試結(jié)果有點驚訝,按snapdom的說法主要利用瀏覽器自帶的Api來實現(xiàn)理論上應(yīng)該是最快的。
截圖原理
上面的測試結(jié)果有待研究一下,不過我們可以看看他們各自的實現(xiàn)邏輯大概是怎樣的。
html2canvas
看了一下源碼發(fā)現(xiàn)html2canvas支持兩種模式。
- 利用svg里面的foreignObject標(biāo)簽
- 手動一個個節(jié)點畫上去
以上兩種方式在真正截圖之前都會先克隆一份目標(biāo)節(jié)點,主要都是利用Element.cloneNode方法。
foreignObject
這種方式大概的流程首先把克隆下來的目標(biāo)節(jié)點插入到svg的foreignObject標(biāo)簽中。
const xmlns = 'http://www.w3.org/2000/svg'; const svg = document.createElementNS(xmlns, 'svg'); const foreignObject = document.createElementNS(xmlns, 'foreignObject'); svg.appendChild(foreignObject); foreignObject.appendChild(cloneNode);
然后把得到的svg對象加載成img給接下來的canvas通過drawImage畫出來。得到一個目標(biāo)畫了目標(biāo)節(jié)點的canvas。
// 加載svg
const img = new Image();
img.onload = () => {
resolve(img);
};
img.onerror = reject;
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
// 畫出來
ctx.drawImage(img, -this.options.x * this.options.scale, -this.options.y * this.options.scale);
手動畫
這種方式主要是把克隆的目標(biāo)節(jié)點遍歷畫出來,它會先把所有節(jié)點包裝成類似可繪制的對象指令,然后利用stacking context建立一個新的“繪制層”,最后按照順序畫到canvas上。主要遍歷邏輯在以下代碼。
export const parseStackingContexts = (container: ElementContainer): StackingContext => {
// 1. 把 DOM 容器包一層 "可繪制對象"
const paintContainer = new ElementPaint(container, null);
// 2. 用這個 paintContainer 創(chuàng)建一個根 stacking context
const root = new StackingContext(paintContainer);
// 3. 創(chuàng)建一個數(shù)組,用來收集 "list-item" 元素
const listItems: ElementPaint[] = [];
// 4. 遞歸解析 DOM 樹,建立 stacking context 樹
parseStackTree(paintContainer, root, root, listItems);
// 5. 額外處理 <li>(list-item)相關(guān)的 marker(比如列表圓點/數(shù)字)
processListItems(paintContainer.container, listItems);
// 6. 返回整個 root stacking context
return root;
};
關(guān)鍵觸發(fā)繪制層是在parseStackTree方法中遍歷去構(gòu)建繪制的dom樹。
我嘗試使用foreignObject的模式去截圖結(jié)果對比如下:
//小圖 html2canvas (foreignObject方式)生成圖片耗時:220ms html2canvas 生成圖片耗時:246ms // 大圖 html2canvas (foreignObject方式)生成圖片耗時:337ms html2canvas 生成圖片耗時:529ms
長大圖的時候foreignObject的效率還是比較高的。所以我理解中snapdom應(yīng)該效率速度是最高的才對,也有可能是我使用的姿勢不對。
snapdom
snapdom的核心截圖方式主要和html2canvas的foreignObjectRendering是一樣的。核心如下:
const svgNS = "http://www.w3.org/2000/svg";
const fo = document.createElementNS(svgNS, "foreignObject");
fo.setAttribute("width", "100%");
fo.setAttribute("height", "100%");
const styleTag = document.createElement("style");
styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;}" + classCSS;
fo.appendChild(styleTag);
fo.appendChild(clone);
const serializer = new XMLSerializer();
const foString = serializer.serializeToString(fo);
const svgHeader = `<svg xmlns="${svgNS}" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`;
const svgFooter = "</svg>";
svgString = svgHeader + foString + svgFooter;
dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
它會先得到一個dataURL,后續(xù)再根據(jù)不同的需求最終再做轉(zhuǎn)換,例如toImg會生成一個Image節(jié)點,toCanvas會生成canvas節(jié)點。它提供了實例方法如下:
capture
toRaw
toImg
toCanvas
toBlob
toPng
toJpg
toWebp
download
也可以通過snapdom.xx直接調(diào)用。
html-to-image
這個庫的提供的API最終都是調(diào)用到toSvg,其核心原理還是利用svg的foreignObject對象。并且在創(chuàng)建對象前還是會利用cloneNode去克隆目標(biāo)節(jié)點。
export async function nodeToDataURL(
node: HTMLElement,
width: number,
height: number,
): Promise<string> {
const xmlns = 'http://www.w3.org/2000/svg'
const svg = document.createElementNS(xmlns, 'svg')
const foreignObject = document.createElementNS(xmlns, 'foreignObject')
svg.appendChild(foreignObject)
foreignObject.appendChild(node)
return svgToDataURL(svg)
}
然后就會得到一個svg。例如我前面用的toCanvas方法它就是先調(diào)用toSvg得到svg對象然后轉(zhuǎn)換成圖片畫到一個新的canvas對象上并返回。
總結(jié)
三種庫核心都是利用到svg的foreignObject對象插入目標(biāo)節(jié)點,然后再通過不同的方式渲染生成,大同小異,只是html2canvas默認(rèn)不使用該方式。
另外一個就是官方提供的數(shù)據(jù)在實際業(yè)務(wù)中使用時不一定是最好的,大家在業(yè)務(wù)中可多嘗試對比一下真實效果與數(shù)據(jù)再決定使用哪種。
到此這篇關(guān)于JS實現(xiàn)網(wǎng)頁截圖的三種方式對比的文章就介紹到這了,更多相關(guān)js網(wǎng)頁截圖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js showModalDialog參數(shù)的使用詳解
本篇文章主要是對js中showModalDialog參數(shù)的使用進(jìn)行了詳細(xì)的分析介紹,需要的朋友可以過來參考下,希望對大家有所幫助2014-01-01
原生javascript實現(xiàn)分享到朋友圈功能 支持ios和android
本文主要介紹網(wǎng)上一個牛人寫的js可以實現(xiàn)在UC瀏覽器和QQ瀏覽器中調(diào)用瀏覽器內(nèi)置的分享組件進(jìn)行分享。2016-05-05
javascript 定義初始化數(shù)組函數(shù)
有段javascript代碼很困惑,經(jīng)過不斷的查資料,終于弄懂了!呵呵!2009-09-09
細(xì)數(shù)JavaScript 一個等號,兩個等號,三個等號的區(qū)別
下面小編就為大家?guī)硪黄?xì)數(shù)JavaScript 一個等號,兩個等號,三個等號的區(qū)別。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-10-10

