明:SVG 雖然也是標(biāo)簽,但它不是 HTML5,標(biāo)題加了 HTML5 只是為了與 canvas 放到一起。
SVG 意為可縮放矢量圖形(Scalable Vector Graphics),使用 XML 格式定義矢量圖形。其他的圖像格式都是基于像素的,但是 SVG 沒有單位的概念,它的20只是表示1的20倍,所以 SVG 繪制的圖形放大或縮小都不會(huì)失真。
與其他圖像比較,SVG 的優(yōu)勢(shì)有以下幾點(diǎn):
2.1、svg 標(biāo)簽
SVG 的代碼都放到 svg 標(biāo)簽?zāi)兀琒VG 中的標(biāo)簽都是閉合標(biāo)簽,與html中標(biāo)簽用法一致。svg的屬性有:
eg:畫一條直線,完整代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body style="height:600px;">
<svg width="300" height="300">
<line x1="0" y1="0" x2="100" y2="100" stroke="black" stroke-width="20"></line>
</svg>
</body>
</html>
上述 svg 設(shè)置的寬高沒有帶單位,此時(shí)默認(rèn)是像素值,如果需要添加單位時(shí),除了絕對(duì)單位,也可以設(shè)置相對(duì)單位。
使用語法:<svg viewBox=" x1,y1,width,height "></svg>
四個(gè)參數(shù)分別是左上角的橫縱坐標(biāo)、視口的寬高。表示只看SVG的某一部分,由上述四個(gè)參數(shù)決定。
使用 viewBox 之后,相當(dāng)于svg整體大小不變,只能看到 viewBox 設(shè)置部分,視覺上被放大。
2.2、SVG 如何嵌入 HTML
SVG 的代碼可以直接嵌入到 html 頁面中,也可以通過 html 的embed、object、iframe嵌入到html中。嵌入的時(shí)候嵌入的是 SVG 文件,SVG 文件必須使用 .svg 后綴。分別介紹各種方法如何使用?
2.2.1、embed 嵌入:
使用語法:<embed src="line.svg" type="image/svg+xml"></embed>
src是SVG文件路徑,type 表示 embed 引入文件類型。
優(yōu)點(diǎn):所有瀏覽器都支持,并允許使用腳本。
缺點(diǎn):不推薦 html4 和 html 中使用,但 html5 支持。
2.2.2、object 嵌入:
使用語法:<object data="line.svg" type="image/svg+xml"></object>
data 是 SVG 文件路徑,type 表示 object 引入文件類型。
優(yōu)點(diǎn):所有瀏覽器都支持,支持 html、html4 和 html5。
缺點(diǎn):不允許使用腳本。
2.2.3、iframe 嵌入:
使用語法:<iframe width="300" height="300" src="./line.svg" frameborder="0"></iframe>
src是 SVG 文件路徑,width、height、frameborder 設(shè)置的大小和邊框。
優(yōu)點(diǎn):所有瀏覽器都支持,并允許使用腳本。
缺點(diǎn):不推薦 html4 和 html 中使用,但 html5 支持。
2.2.4、html中嵌入:
svg 標(biāo)簽直接插入 html 內(nèi)容內(nèi),與其他標(biāo)簽用法一致。
2.2.5、連接到svg文件:
使用 a 標(biāo)簽,直接鏈接到 SVG 文件。
使用語法:<a href="line.svg">查看SVG</a>
3.1、線 - line
使用語法:
<svg width="300" height="300" >
<line x1="0" y1="0" x2="300" y2="300" stroke="black" stroke-width="20"></line>
</svg>
使用line標(biāo)簽創(chuàng)建線條,(x1,y1)是起點(diǎn),(x2,y2)是終點(diǎn),stroke繪制黑線,stroke-width是線寬。
3.2、矩形 - rect
//使用語法:
<svg width="300" height="300" >
<rect
width="100" height="100" //大小設(shè)置
x="50" y="50" //可選 左上角位置,svg的左上角默認(rèn)(0,0)
rx="20" ry="50" //可選 設(shè)置圓角
stroke-width="3" stroke="red" fill="pink" //繪制樣式控制
></rect>
</svg>
上述參數(shù) width、height是必填參數(shù),x、y是可選參數(shù),如不設(shè)置的時(shí)候,默認(rèn)為(0,0),也就是svg的左上角開始繪制。rx、ry是可選參數(shù),不設(shè)置是矩形沒有圓角。fill定義填充顏色。
3.3、圓形 - circle
// 使用語法
<svg width="300" height="300" >
<circle
cx="100" cy="50" // 定義圓心 ,可選
r="40" // 圓的半徑
stroke="black" stroke-width="2" fill="red"/> //繪制黑框填充紅色
</svg>
上述(cx,xy)定義圓心的位置,是可選參數(shù),如果不設(shè)置默認(rèn)圓心是(0,0)。r是必需參數(shù),設(shè)置圓的半徑。
3.4、橢圓 - ellipse
橢圓與圓相似,不同之處在于橢圓有不同的x和y半徑,而圓兩個(gè)半徑是相同的。
// 使用語法
<svg width="300" height="300" >
<ellipse
rx="20" ry="100" //設(shè)置橢圓的x、y方向的半徑
fill="purple" // 橢圓填充色
cx="150" cy="150" //設(shè)置橢圓的圓心 ,可選參數(shù)
></ellipse>
</svg>
上述橢圓的兩個(gè)rx、ry兩個(gè)方向半徑是必須參數(shù),如果rx=ry就表示是圓形,(cx,cy)是橢圓的圓心,是可選參數(shù),如果不設(shè)置,則默認(rèn)圓心為(0,0)。
3.5、折線 - polyline
// 使用語法
<svg width="300" height="300" style="border:solid 1px red;">
<!-- 繪制出一個(gè)默認(rèn)填充黑色的三角形 -->
<polyline
points=" //點(diǎn)的集合
0 ,0, // 第一個(gè)點(diǎn)坐標(biāo)
100,100, // 第二個(gè)點(diǎn)坐標(biāo)
100,200 // 第三個(gè)點(diǎn)坐標(biāo)
"
stroke="green"
></polyline>
<!-- 繪制一個(gè)臺(tái)階式的一條折線 -->
<polyline
points="0,0,50,0,50,50,100,50,100,100,150,100,150,150"
stroke="#4b27ff" fill="none"
></polyline>
</svg>
上述代碼執(zhí)行結(jié)果如圖所示:
需要注意的是 points 中包含了多個(gè)點(diǎn)的坐標(biāo),但不是一個(gè)數(shù)組。
3.6、多邊形 - polygon
polygon 標(biāo)簽用來創(chuàng)建不少于3個(gè)邊的圖形,多邊形是閉合的,即所有線條連接起來。
// 使用語法
<svg width="300" height="300" style="border:solid 1px red;">
<polygon
points="
0,0, //多邊形的第一點(diǎn)
100,100, //多邊形的第二點(diǎn)
0,100 //多邊形的第三點(diǎn)
"
stroke="purple"
stroke-width="1"
fill="none"
></polygon>
</svg>
polygon繪制的時(shí)候與折線有些類似,但是polygon會(huì)自動(dòng)閉合,折線不會(huì)。
3.7、路徑 - path
path 是SVG基本形狀中最強(qiáng)大的一個(gè),不僅能創(chuàng)建其他基本形狀,還能創(chuàng)建更多其他形狀,如貝塞爾曲線、2次曲線等。
點(diǎn)個(gè)關(guān)注,下篇更精彩!
SVG(Scalable Vector Graphics)是一種基于XML的2D矢量圖形格式,可以實(shí)現(xiàn)圖像的無損縮放和高清晰度顯示。在HTML中嵌入SVG圖像,可以使網(wǎng)頁更加生動(dòng)有趣,提高用戶體驗(yàn)
<svg width="54" height="54" class="c-nav--footer__svgicon c-slackhash" viewBox="0 0 54 54" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M19.712.133a5.381 5.381 0 0 0-5.376 5.387 5.381 5.381 0 0 0 5.376 5.386h5.376V5.52A5.381 5.381 0 0 0 19.712.133m0 14.365H5.376A5.381 5.381 0 0 0 0 19.884a5.381 5.381 0 0 0 5.376 5.387h14.336a5.381 5.381 0 0 0 5.376-5.387 5.381 5.381 0 0 0-5.376-5.386" fill="#44BEDF">
</path>
<path d="M53.76 19.884a5.381 5.381 0 0 0-5.376-5.386 5.381 5.381 0 0 0-5.376 5.386v5.387h5.376a5.381 5.381 0 0 0 5.376-5.387m-14.336 0V5.52A5.381 5.381 0 0 0 34.048.133a5.381 5.381 0 0 0-5.376 5.387v14.364a5.381 5.381 0 0 0 5.376 5.387 5.381 5.381 0 0 0 5.376-5.387" fill="#2EB67D">
</path>
<path d="M34.048 54a5.381 5.381 0 0 0 5.376-5.387 5.381 5.381 0 0 0-5.376-5.386h-5.376v5.386A5.381 5.381 0 0 0 34.048 54m0-14.365h14.336a5.381 5.381 0 0 0 5.376-5.386 5.381 5.381 0 0 0-5.376-5.387H34.048a5.381 5.381 0 0 0-5.376 5.387 5.381 5.381 0 0 0 5.376 5.386" fill="#ECB22E">
</path>
<path d="M0 34.249a5.381 5.381 0 0 0 5.376 5.386 5.381 5.381 0 0 0 5.376-5.386v-5.387H5.376A5.381 5.381 0 0 0 0 34.25m14.336-.001v14.364A5.381 5.381 0 0 0 19.712 54a5.381 5.381 0 0 0 5.376-5.387V34.25a5.381 5.381 0 0 0-5.376-5.387 5.381 5.381 0 0 0-5.376 5.387" fill="#E01E5A">
</path>
</g>
</svg>
om-to-image庫可以幫你把dom節(jié)點(diǎn)轉(zhuǎn)換為圖片,它的核心原理很簡(jiǎn)單,就是利用svg的foreignObject標(biāo)簽?zāi)芮度雋tml的特性,然后通過img標(biāo)簽加載svg,最后再通過canvas繪制img實(shí)現(xiàn)導(dǎo)出,好了,本文到此結(jié)束。
另一個(gè)知名的html2canvas庫其實(shí)也支持這種方式。
雖然原理很簡(jiǎn)單,但是dom-to-image畢竟也有1000多行代碼,所以我很好奇它具體都做了哪些事情,本文就來詳細(xì)剖析一下,需要說明的是dom-to-image庫已經(jīng)六七年前沒有更新了,可能有點(diǎn)過時(shí),所以我們要看的是基于它修改的dom-to-image-more庫,這個(gè)庫修復(fù)了一些bug,以及增加了一些特性,接下來我們就來詳細(xì)了解一下。
我們用的最多的api應(yīng)該就是toPng(node),所以以這個(gè)方法為入口:
function toPng(node, options) {
return draw(node, options).then(function (canvas) {
return canvas.toDataURL();
});
}
toPng方法會(huì)調(diào)用draw方法,然后返回一個(gè)canvas,最后通過canvas的toDataURL方法獲取到圖片的base64格式的data:URL,我們就可以直接下載為圖片。
看一下draw方法:
function draw(domNode, options) {
options = options || {};
return toSvg(domNode, options)// 轉(zhuǎn)換成svg
.then(util.makeImage)// 轉(zhuǎn)換成圖片
.then(function (image) {// 通過canvas繪制圖片
// ...
});
}
一共分為了三個(gè)步驟,一一來看。
toSvg方法如下:
function toSvg(node, options) {
const ownerWindow = domtoimage.impl.util.getWindow(node);
options = options || {};
copyOptions(options);
let restorations = [];
return Promise.resolve(node)
.then(ensureElement)// 檢查和包裝元素
.then(function (clonee) {// 深度克隆節(jié)點(diǎn)
return cloneNode(clonee, options, null, ownerWindow);
})
.then(embedFonts)// 嵌入字體
.then(inlineImages)// 內(nèi)聯(lián)圖片
.then(makeSvgDataUri)// svg轉(zhuǎn)data:URL
.then(restoreWrappers)// 恢復(fù)包裝元素
}
node就是我們要轉(zhuǎn)換成圖片的DOM節(jié)點(diǎn),首先調(diào)用了getWindow方法獲取window對(duì)象:
function getWindow(node) {
const ownerDocument = node ? node.ownerDocument : undefined;
return (
(ownerDocument ? ownerDocument.defaultView : undefined) ||
global ||
window
);
}
說實(shí)話前端寫了這么多年,但是ownerDocument和defaultView兩個(gè)屬性我完全沒用過,ownerDocument屬性會(huì)返回當(dāng)前節(jié)點(diǎn)的頂層的 document對(duì)象,而在瀏覽器中,defaultView屬性會(huì)返回當(dāng)前 document 對(duì)象所關(guān)聯(lián)的 window 對(duì)象,如果沒有,會(huì)返回 null。
所以這里優(yōu)先通過我們傳入的DOM節(jié)點(diǎn)獲取window對(duì)象,可能是為了處理iframe嵌入之類的情況把。
接下來合并了選項(xiàng)后,就通過Promise實(shí)例的then方法鏈?zhǔn)降恼{(diào)用一系列的方法,一一來看。
ensureElement方法如下:
function ensureElement(node) {
// ELEMENT_NODE:1
if (node.nodeType === ELEMENT_NODE) return node;
const originalChild = node;
const originalParent = node.parentNode;
const wrappingSpan = document.createElement('span');
originalParent.replaceChild(wrappingSpan, originalChild);
wrappingSpan.append(node);
restorations.push({
parent: originalParent,
child: originalChild,
wrapper: wrappingSpan,
});
return wrappingSpan;
}
html節(jié)點(diǎn)的nodeType有如下類型:
值為1也就是我們普通的html標(biāo)簽,其他的比如文本節(jié)點(diǎn)、注釋節(jié)點(diǎn)、document節(jié)點(diǎn)也是比較常用的,如果我們傳入的節(jié)點(diǎn)的類型為1,ensureElement方法什么也不做直接返回該節(jié)點(diǎn),否則會(huì)創(chuàng)建一個(gè)span標(biāo)簽替換掉原節(jié)點(diǎn),并把原節(jié)點(diǎn)添加到該span標(biāo)簽里,可以猜測(cè)這個(gè)主要是處理文本節(jié)點(diǎn),畢竟應(yīng)該沒有人會(huì)傳其他類型的節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換了。
同時(shí)它還把原節(jié)點(diǎn),原節(jié)點(diǎn)的父節(jié)點(diǎn),span標(biāo)簽都收集到restorations數(shù)組里,很明顯,這是為了后面進(jìn)行還原。
接下來執(zhí)行了cloneNode方法:
cloneNode(clonee, options, null, ownerWindow)
// 參數(shù):需要克隆的節(jié)點(diǎn)、選項(xiàng)、父節(jié)點(diǎn)的樣式、所屬window對(duì)象
function cloneNode(node, options, parentComputedStyles, ownerWindow) {
const filter = options.filter;
if (
node === sandbox ||
util.isHTMLScriptElement(node) ||
util.isHTMLStyleElement(node) ||
util.isHTMLLinkElement(node) ||
(parentComputedStyles !== null && filter && !filter(node))
) {
return Promise.resolve();
}
return Promise.resolve(node)
.then(makeNodeCopy)// 處理canvas元素
.then(function (clone) {// 克隆子節(jié)點(diǎn)
return cloneChildren(clone, getParentOfChildren(node));
})
.then(function (clone) {// 處理克隆的節(jié)點(diǎn)
return processClone(clone, node);
});
}
先做了一堆判斷,如果是script、style、link標(biāo)簽,或者需要過濾掉的節(jié)點(diǎn),那么會(huì)直接返回。
sandbox、parentComputedStyles后面會(huì)看到。
接下來又調(diào)用了幾個(gè)方法,沒辦法,跟著它一起入棧把。
function makeNodeCopy(original) {
if (util.isHTMLCanvasElement(original)) {
return util.makeImage(original.toDataURL());
}
return original.cloneNode(false);
}
如果元素是canvas,那么會(huì)通過makeImage方法將其轉(zhuǎn)換成img標(biāo)簽:
function makeImage(uri) {
if (uri === 'data:,') {
return Promise.resolve();
}
return new Promise(function (resolve, reject) {
const image = new Image();
if (domtoimage.impl.options.useCredentials) {
image.crossOrigin = 'use-credentials';
}
image.onload = function () {
if (window && window.requestAnimationFrame) {
// 解決 Firefox 的一個(gè)bug (webcompat/web-bugs#119834)
// 需要等待一幀
window.requestAnimationFrame(function () {
resolve(image);
});
} else {
// 如果沒有window對(duì)象或者requestAnimationFrame方法,那么立即返回
resolve(image);
}
};
image.onerror = reject;
image.src = uri;
});
}
crossOrigin屬性用于定義一些元素如何處理跨域請(qǐng)求,主要有兩個(gè)取值:
anonymous:元素的跨域資源請(qǐng)求不需要憑證標(biāo)志設(shè)置。
use-credentials:元素的跨域資源請(qǐng)求需要憑證標(biāo)志設(shè)置,意味著該請(qǐng)求需要提供憑證。
除了use-credentials,給crossOrigin設(shè)置其他任何值都會(huì)解析成anonymous,為了解決跨域問題,我們一般都會(huì)設(shè)置成anonymous,這個(gè)就相當(dāng)于告訴服務(wù)器,你不需要返回任何非匿名信息過來,例如cookie,所以肯定是安全的。不過在使用這兩個(gè)值時(shí)都需要服務(wù)端返回Access-Control-Allow-Credentials響應(yīng)頭,否則肯定無法跨域使用的。
非canvas元素的其他元素,會(huì)直接調(diào)用它們的cloneNode方法進(jìn)行克隆,參數(shù)傳了false,代表只克隆自身,不克隆子節(jié)點(diǎn)。
接下來調(diào)用了cloneChildren方法:
cloneChildren(clone, getParentOfChildren(node));
getParentOfChildren方法如下:
function getParentOfChildren(original) {
// 如果該節(jié)點(diǎn)是Shadow DOM的附加節(jié)點(diǎn),那么返回附加的Shadow DOM的根節(jié)點(diǎn)
if (util.isElementHostForOpenShadowRoot(original)) {
return original.shadowRoot;
}
return original;
}
function isElementHostForOpenShadowRoot(value) {
return isElement(value) && value.shadowRoot !== null;
}
這里涉及到了shadow DOM,有必要先簡(jiǎn)單了解一下。
shadow DOM是一種封裝技術(shù),可以將標(biāo)記結(jié)構(gòu)、樣式和行為隱藏起來,比如我們熟悉的video標(biāo)簽,我們看到的只是一個(gè)video標(biāo)簽,但實(shí)際上它里面有很多我們看不到的元素,這個(gè)特性一般會(huì)和Web components結(jié)合使用,也就是可以創(chuàng)建自定義元素,就和Vue和React組件一樣。
先了解一些術(shù)語:
Shadow host:一個(gè)常規(guī) DOM 節(jié)點(diǎn),Shadow DOM 會(huì)被附加到這個(gè)節(jié)點(diǎn)上。
Shadow tree:Shadow DOM 內(nèi)部的 DOM 樹。
Shadow boundary:Shadow DOM 結(jié)束的地方,也是常規(guī) DOM 開始的地方。
Shadow root: Shadow tree 的根節(jié)點(diǎn)。
一個(gè)普通的DOM元素可以使用attachShadow方法來添加shadow DOM:
let shadow = div.attachShadow({ mode: "open" });
這樣就可以給div元素附加一個(gè)shadow DOM,然后我們可以和創(chuàng)建普通元素一樣創(chuàng)建任何元素添加到shadow下:
let para = document.createElement('p');
shadow.appendChild(para);
當(dāng)mode設(shè)為open,我們就可以通過div.shadowRoot獲取到Shadow DOM,如果設(shè)置的是closed,那么外部就獲取不到。
所以前面的getParentOfChildren方法會(huì)判斷當(dāng)前節(jié)點(diǎn)是不是一個(gè)Shadow host節(jié)點(diǎn),是的話就返回它內(nèi)部的Shadow root節(jié)點(diǎn),否則返回自身。
回到cloneChildren方法,它接收兩個(gè)參數(shù):克隆的節(jié)點(diǎn)、原節(jié)點(diǎn)。
function cloneChildren(clone, original) {
// 獲取子節(jié)點(diǎn),如果原節(jié)點(diǎn)是slot節(jié)點(diǎn),那么會(huì)返回slot內(nèi)的節(jié)點(diǎn),
const originalChildren = getRenderedChildren(original);
let done = Promise.resolve();
if (originalChildren.length !== 0) {
// 獲取原節(jié)點(diǎn)的計(jì)算樣式,如果原節(jié)點(diǎn)是shadow root節(jié)點(diǎn),那么會(huì)獲取它所附加到的普通元素的樣式
const originalComputedStyles = getComputedStyle(
getRenderedParent(original)
);
// 遍歷子節(jié)點(diǎn)
util.asArray(originalChildren).forEach(function (originalChild) {
done = done.then(function () {
// 遞歸調(diào)用cloneNode方法
return cloneNode(
originalChild,
options,
originalComputedStyles,
ownerWindow
).then(function (clonedChild) {
// 克隆完后的子節(jié)點(diǎn)添加到該節(jié)點(diǎn)
if (clonedChild) {
clone.appendChild(clonedChild);
}
});
});
});
}
return done.then(function () {
return clone;
});
}
首先通過getRenderedChildren方法獲取子節(jié)點(diǎn):
function getRenderedChildren(original) {
// 如果是slot元素,那么通過assignedNodes方法返回該插槽中的節(jié)點(diǎn)
if (util.isShadowSlotElement(original)) {
return original.assignedNodes();
}
// 普通元素直接通過childNodes獲取子節(jié)點(diǎn)
return original.childNodes;
}
// 判斷是否是html slot元素
function isShadowSlotElement(value) {
return (
isInShadowRoot(value) && value instanceof getWindow(value).HTMLSlotElement
);
}
// 判斷一個(gè)節(jié)點(diǎn)是否處于shadow DOM樹中
function isInShadowRoot(value) {
// 如果是普通節(jié)點(diǎn),getRootNode方法會(huì)返回document對(duì)象,如果是Shadow DOM,那么會(huì)返回shadow root
return (
value !== null &&
Object.prototype.hasOwnProperty.call(value, 'getRootNode') &&
isShadowRoot(value.getRootNode())
);
}
// 判斷是否是shadow DOM的根節(jié)點(diǎn)
function isShadowRoot(value) {
return value instanceof getWindow(value).ShadowRoot;
}
這一連串的判斷,如果對(duì)于shadow DOM不熟悉的話大概率很難看懂,不過沒關(guān)系,跳過這部分也可以,反正就是獲取子節(jié)點(diǎn)。
獲取到子節(jié)點(diǎn)后又調(diào)用了如下方法:
const originalComputedStyles = getComputedStyle(
getRenderedParent(original)
);
function getRenderedParent(original) {
// 如果該節(jié)點(diǎn)是shadow root,那么返回它附加到的普通的DOM節(jié)點(diǎn)
if (util.isShadowRoot(original)) {
return original.host;
}
return original;
}
調(diào)用getComputedStyle獲取原節(jié)點(diǎn)的樣式,這個(gè)方法其實(shí)就是window.getComputedStyle方法,會(huì)返回節(jié)點(diǎn)的所有樣式和值。
接下來就是遍歷子節(jié)點(diǎn),然后對(duì)每個(gè)子節(jié)點(diǎn)再次調(diào)用cloneNode方法,只不過會(huì)把原節(jié)點(diǎn)的樣式也傳進(jìn)去。對(duì)于子元素又會(huì)遞歸處理它們的子節(jié)點(diǎn),這樣就能深度克隆完整棵DOM樹。
對(duì)于每個(gè)克隆節(jié)點(diǎn),又調(diào)用了processClone(clone, node)方法:
function processClone(clone, original) {
// 如果不是普通節(jié)點(diǎn),或者是slot節(jié)點(diǎn),那么直接返回
if (!util.isElement(clone) || util.isShadowSlotElement(original)) {
return Promise.resolve(clone);
}
return Promise.resolve()
.then(cloneStyle)// 克隆樣式
.then(clonePseudoElements)// 克隆偽元素
.then(copyUserInput)// 克隆輸入框
.then(fixSvg)// 修復(fù)svg
.then(function () {
return clone;
});
}
又是一系列的操作,穩(wěn)住,我們繼續(xù)。
function cloneStyle() {
copyStyle(original, clone);
}
調(diào)用了copyStyle方法,傳入原節(jié)點(diǎn)和克隆節(jié)點(diǎn):
function copyStyle(sourceElement, targetElement) {
const sourceComputedStyles = getComputedStyle(sourceElement);
if (sourceComputedStyles.cssText) {
// ...
} else {
// ...
}
}
window.getComputedStyle方法返回的是一個(gè)CSSStyleDeclaration對(duì)象,和我們使用div.style獲取到的對(duì)象類型是一樣的,但是div.style對(duì)象只能獲取到元素的內(nèi)聯(lián)樣式,使用div.style.color = '#fff'設(shè)置的也能獲取到,因?yàn)檫@種方式設(shè)置的也是內(nèi)聯(lián)樣式,其他樣式是獲取不到的,但是window.getComputedStyle能獲取到所有css樣式。
div.style.cssText屬性我們都用過,可以獲取和批量設(shè)置內(nèi)聯(lián)樣式,如果要設(shè)置多個(gè)樣式,比單個(gè)調(diào)用div.style.xxx方便一點(diǎn),但是cssText會(huì)覆蓋整個(gè)內(nèi)聯(lián)樣式,比如下面的方式設(shè)置的字號(hào)是會(huì)丟失的,內(nèi)聯(lián)樣式最終只有color:
div.style.fontSize = '23px'
div.style.cssText = 'color: rgb(102, 102, 102)'
但是window.getComputedStyle方法返回的對(duì)象的cssText和div.style.cssText不是同一個(gè)東西,即使有內(nèi)聯(lián)樣式,window.getComputedStyle方法返回對(duì)象的cssText值也是空,并且它無法修改,所以不清楚什么情況下它才會(huì)有值。
假設(shè)有值的話,接下來的代碼我也不是很能理解:
if (sourceComputedStyles.cssText) {
targetElement.style.cssText = sourceComputedStyles.cssText;
copyFont(sourceComputedStyles, targetElement.style);
}
function copyFont(source, target) {
target.font = source.font;
target.fontFamily = source.fontFamily;
// ...
}
為什么不直接把原節(jié)點(diǎn)的style.cssText復(fù)制給克隆節(jié)點(diǎn)的style.cssText呢,另外為啥文本相關(guān)的樣式又要單獨(dú)設(shè)置一遍呢,無法理解。
我們看看另外一個(gè)分支:
else {
copyUserComputedStyleFast(
options,
sourceElement,
sourceComputedStyles,
parentComputedStyles,
targetElement
);
// ...
}
先調(diào)用了copyUserComputedStyleFast方法,這個(gè)方法內(nèi)部非常復(fù)雜,就不把具體代碼放出來了,大致介紹一下它都做了什么:
1.首先會(huì)獲取原節(jié)點(diǎn)的所謂的默認(rèn)樣式,這個(gè)步驟也比較復(fù)雜:
1.1.先獲取原節(jié)點(diǎn)及祖先節(jié)點(diǎn)的元素標(biāo)簽列表,其實(shí)就是一個(gè)向上遞歸的過程,不過存在終止條件,就是當(dāng)遇到塊級(jí)元素的祖先節(jié)點(diǎn)。比如原節(jié)點(diǎn)是一個(gè)span標(biāo)簽,它的父節(jié)點(diǎn)也是一個(gè)span,再上一個(gè)父節(jié)點(diǎn)是一個(gè)div,那么獲取到的標(biāo)簽列表就是[span, span, div]。
? 1.2.接下來會(huì)創(chuàng)建一個(gè)沙箱,也就是一個(gè)iframe,這個(gè)iframe的DOCTYPE和charset會(huì)設(shè)置成和當(dāng)前頁面的一樣。
? 1.3.再接下來會(huì)根據(jù)前面獲取到的標(biāo)簽列表,在iframe中創(chuàng)建對(duì)應(yīng)結(jié)構(gòu)的DOM節(jié)點(diǎn),也就是會(huì)創(chuàng)建這樣一棵DOM樹:div -> span -> span。并且會(huì)給最后一個(gè)節(jié)點(diǎn)添加一個(gè)零寬字符的文本,并返回這個(gè)節(jié)點(diǎn)。
? 1.4.使用iframe的window.getComputedStyle方法獲取上一步返回節(jié)點(diǎn)的樣式,對(duì)于width和height會(huì)設(shè)置成auto。
? 1.5.刪除iframe里前面創(chuàng)建的節(jié)點(diǎn)。
? 16.返回1.4步獲取到的樣式對(duì)象。
2.遍歷原節(jié)點(diǎn)的樣式,也就是sourceComputedStyles對(duì)象,對(duì)于每一個(gè)樣式屬性,都會(huì)獲取到三個(gè)值:sourceValue、defaultValue、parentValue,分別來自原節(jié)點(diǎn)的樣式對(duì)象sourceComputedStyles、第一步獲取到的默認(rèn)樣式對(duì)象、父節(jié)點(diǎn)的樣式對(duì)象parentComputedStyles,然后會(huì)做如下判斷:
if (
sourceValue !== defaultValue ||
(parentComputedStyles && sourceValue !== parentValue)
) {
// 樣式優(yōu)先級(jí),比如important
const priority = sourceComputedStyles.getPropertyPriority(name);
// 將樣式設(shè)置到克隆節(jié)點(diǎn)的style對(duì)象上
setStyleProperty(targetStyle, name, sourceValue, priority);
}
如果原節(jié)點(diǎn)的某個(gè)樣式值和默認(rèn)的樣式值不一樣,并且和父節(jié)點(diǎn)的也不一樣,那么就需要給克隆的節(jié)點(diǎn)手動(dòng)設(shè)置成內(nèi)聯(lián)樣式,否則其實(shí)就是繼承樣式或者默認(rèn)樣式,就不用管了,不得不說,還是挺巧妙的。
copyUserComputedStyleFast方法執(zhí)行完后還做了如下操作:
if (parentComputedStyles === null) {
[
'inset-block',
'inset-block-start',
'inset-block-end',
].forEach((prop) => targetElement.style.removeProperty(prop));
['left', 'right', 'top', 'bottom'].forEach((prop) => {
if (targetElement.style.getPropertyValue(prop)) {
targetElement.style.setProperty(prop, '0px');
}
});
}
對(duì)于我們傳入的節(jié)點(diǎn),parentComputedStyles是null,本質(zhì)相當(dāng)于根節(jié)點(diǎn),所以直接移除它的位置信息,防止發(fā)生偏移。
克隆完樣式,接下來就是處理偽元素了:
function clonePseudoElements() {
const cloneClassName = util.uid();
[':before', ':after'].forEach(function (element) {
clonePseudoElement(element);
});
}
分別調(diào)用clonePseudoElement方法處理兩種偽元素:
function clonePseudoElement(element) {
// 獲取原節(jié)點(diǎn)偽元素的樣式
const style = getComputedStyle(original, element);
// 獲取偽元素的content
const content = style.getPropertyValue('content');
// 如果偽元素的內(nèi)容為空就直接返回
if (content === '' || content === 'none') {
return;
}
// 獲取克隆節(jié)點(diǎn)的類名
const currentClass = clone.getAttribute('class') || '';
// 給克隆元素增加一個(gè)唯一的類名
clone.setAttribute('class', `${currentClass} ${cloneClassName}`);
// 創(chuàng)建一個(gè)style標(biāo)簽
const styleElement = document.createElement('style');
// 插入偽元素的樣式
styleElement.appendChild(formatPseudoElementStyle());
// 將樣式標(biāo)簽添加到克隆節(jié)點(diǎn)內(nèi)
clone.appendChild(styleElement);
}
window.getComputedStyle方法是可以獲取元素的偽元素的樣式的,通過第二個(gè)參數(shù)指定要獲取的偽元素即可。
如果偽元素的content為空就不管了,總感覺有點(diǎn)不妥,畢竟我經(jīng)常會(huì)用偽元素渲染一些三角形,content都是設(shè)置成空的。
如果不為空,那么會(huì)給克隆的節(jié)點(diǎn)新增一個(gè)唯一的類名,并且創(chuàng)建一個(gè)style標(biāo)簽添加到克隆節(jié)點(diǎn)內(nèi),這個(gè)style標(biāo)簽里會(huì)插入偽元素的樣式,通過formatPseudoElementStyle方法獲取偽元素的樣式字符串:
function formatPseudoElementStyle() {
const selector = `.${cloneClassName}:${element}`;
// style為原節(jié)點(diǎn)偽元素的樣式對(duì)象
const cssText = style.cssText
? formatCssText()
: formatCssProperties();
return document.createTextNode(`${selector}{${cssText}}`);
}
如果樣式對(duì)象的cssText有值,那么調(diào)用formatCssText方法:
function formatCssText() {
return `${style.cssText} content: ${content};`;
}
但是前面說了,這個(gè)屬性一般都是沒值的,所以會(huì)走formatCssProperties方法:
function formatCssProperties() {
const styleText = util
.asArray(style)
.map(formatProperty)
.join('; ');
return `${styleText};`;
function formatProperty(name) {
const propertyValue = style.getPropertyValue(name);
const propertyPriority = style.getPropertyPriority(name)
? ' !important'
: '';
return `${name}: ${propertyValue}${propertyPriority}`;
}
}
很簡(jiǎn)單,遍歷樣式對(duì)象,然后拼接成css的樣式字符串。
對(duì)于輸入框的處理很簡(jiǎn)單:
function copyUserInput() {
if (util.isHTMLTextAreaElement(original)) {
clone.innerHTML = original.value;
}
if (util.isHTMLInputElement(original)) {
clone.setAttribute('value', original.value);
}
}
如果是textarea或者input元素,直接將原節(jié)點(diǎn)的值設(shè)置到克隆后的元素上即可。但是我測(cè)試發(fā)現(xiàn)克隆輸入框也會(huì)把它的值給克隆過去,所以這一步可能沒有必要。
最后就是處理svg節(jié)點(diǎn):
function fixSvg() {
if (util.isSVGElement(clone)) {
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
if (util.isSVGRectElement(clone)) {
['width', 'height'].forEach(function (attribute) {
const value = clone.getAttribute(attribute);
if (value) {
clone.style.setProperty(attribute, value);
}
});
}
}
}
給svg節(jié)點(diǎn)添加命名空間,另外對(duì)于rect節(jié)點(diǎn),還把寬高的屬性設(shè)置成對(duì)應(yīng)的樣式,這個(gè)是何原因,我們也不得而知。
到這里,節(jié)點(diǎn)的克隆部分就結(jié)束了,不得不說,還是有點(diǎn)復(fù)雜的,很多操作其實(shí)我們也沒有看懂為什么要這么做,開發(fā)一個(gè)庫就是這樣,要處理很多邊界和異常情況,這個(gè)只有遇到了才知道為什么。
節(jié)點(diǎn)克隆完后接下來會(huì)處理字體:
function embedFonts(node) {
return fontFaces.resolveAll().then(function (cssText) {
if (cssText !== '') {
const styleNode = document.createElement('style');
node.appendChild(styleNode);
styleNode.appendChild(document.createTextNode(cssText));
}
return node;
});
}
調(diào)用resolveAll方法,會(huì)返回一段css字符串,然后創(chuàng)建一個(gè)style標(biāo)簽添加到克隆的節(jié)點(diǎn)內(nèi),接下來看看resolveAll方法都做了什么:
function resolveAll() {
return readAll()
// ...
}
又調(diào)用了readAll方法:
function readAll() {
return Promise.resolve(util.asArray(document.styleSheets))
.then(getCssRules)
.then(selectWebFontRules)
.then(function (rules) {
return rules.map(newWebFont);
});
}
document.styleSheets屬性可以獲取到文檔中所有的style標(biāo)簽和通過link標(biāo)簽引入的樣式,結(jié)果是一個(gè)類數(shù)組,數(shù)組的每一項(xiàng)是一個(gè)CSSStyleSheet對(duì)象。
function getCssRules(styleSheets) {
const cssRules = [];
styleSheets.forEach(function (sheet) {
if (
Object.prototype.hasOwnProperty.call(
Object.getPrototypeOf(sheet),
'cssRules'
)
) {
util.asArray(sheet.cssRules || []).forEach(
cssRules.push.bind(cssRules)
);
}
});
return cssRules;
}
通過CSSStyleSheet對(duì)象的cssRules屬性可以獲取到具體的css規(guī)則,cssRules的每一項(xiàng)也就是我們寫的一條css語句:
function selectWebFontRules(cssRules) {
return cssRules
.filter(function (rule) {
return rule.type === CSSRule.FONT_FACE_RULE;
})
.filter(function (rule) {
return inliner.shouldProcess(rule.style.getPropertyValue('src'));
});
}
遍歷所有的css語句,找出其中的@font-face語句,shouldProcess方法會(huì)判斷@font-face語句的src屬性是否存在url()值,找出了所有存在的字體規(guī)則后會(huì)遍歷它們調(diào)用newWebFont方法:
function newWebFont(webFontRule) {
return {
resolve: function resolve() {
const baseUrl = (webFontRule.parentStyleSheet || {}).href;
return inliner.inlineAll(webFontRule.cssText, baseUrl);
},
src: function () {
return webFontRule.style.getPropertyValue('src');
},
};
}
inlineAll方法會(huì)找出@font-face語句中定義的所有字體的url,然后通過XMLHttpRequest發(fā)起請(qǐng)求,將字體文件轉(zhuǎn)換成data:URL形式,然后替換css語句中的url,核心就是使用下面這個(gè)正則匹配和替換。
const URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/g;
繼續(xù)resolveAll方法:
function resolveAll() {
return readAll()
.then(function (webFonts) {
return Promise.all(
webFonts.map(function (webFont) {
return webFont.resolve();
})
);
})
.then(function (cssStrings) {
return cssStrings.join('\n');
});
}
將所有@font-face語句的遠(yuǎn)程字體url都轉(zhuǎn)換成data:URL形式后再將它們拼接成css字符串即可完成嵌入字體的操作。
說實(shí)話,Promise鏈太長(zhǎng),看著容易暈。
內(nèi)聯(lián)完了字體后接下來就是內(nèi)聯(lián)圖片:
function inlineImages(node) {
return images.inlineAll(node).then(function () {
return node;
});
}
處理圖片的inlineAll方法如下:
function inlineAll(node) {
if (!util.isElement(node)) {
return Promise.resolve(node);
}
return inlineCSSProperty(node).then(function () {
// ...
});
}
inlineCSSProperty方法會(huì)判斷節(jié)點(diǎn)background和 background-image屬性是否設(shè)置了圖片,是的話也會(huì)和嵌入字體一樣將遠(yuǎn)程圖片轉(zhuǎn)換成data:URL嵌入:
function inlineCSSProperty(node) {
const properties = ['background', 'background-image'];
const inliningTasks = properties.map(function (propertyName) {
const value = node.style.getPropertyValue(propertyName);
const priority = node.style.getPropertyPriority(propertyName);
if (!value) {
return Promise.resolve();
}
// 如果設(shè)置了背景圖片,那么也會(huì)調(diào)用inliner.inlineAll方法將遠(yuǎn)程url的形式轉(zhuǎn)換成data:URL形式
return inliner.inlineAll(value).then(function (inlinedValue) {
// 將樣式設(shè)置成轉(zhuǎn)換后的值
node.style.setProperty(propertyName, inlinedValue, priority);
});
});
return Promise.all(inliningTasks).then(function () {
return node;
});
}
處理完節(jié)點(diǎn)的背景圖片后:
function inlineAll(node) {
return inlineCSSProperty(node).then(function () {
if (util.isHTMLImageElement(node)) {
return newImage(node).inline();
} else {
return Promise.all(
util.asArray(node.childNodes).map(function (child) {
return inlineAll(child);
})
);
}
});
}
會(huì)檢查節(jié)點(diǎn)是否是圖片節(jié)點(diǎn),是的話會(huì)調(diào)用newImage方法處理,這個(gè)方法也很簡(jiǎn)單,也是發(fā)個(gè)請(qǐng)求獲取圖片數(shù)據(jù),然后將它轉(zhuǎn)換成data:URL設(shè)置回圖片的src。
如果是其他節(jié)點(diǎn),那么就遞歸處理子節(jié)點(diǎn)。
圖片也處理完了接下來就可以將svg轉(zhuǎn)換成data:URL了:
function makeSvgDataUri(node) {
let width = options.width || util.width(node);
let height = options.height || util.height(node);
return Promise.resolve(node)
.then(function (svg) {
svg.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
return new XMLSerializer().serializeToString(svg);
})
.then(util.escapeXhtml)
.then(function (xhtml) {
const foreignObjectSizing =
(util.isDimensionMissing(width)
? ' width="100%"'
: ` width="${width}"`) +
(util.isDimensionMissing(height)
? ' height="100%"'
: ` height="${height}"`);
const svgSizing =
(util.isDimensionMissing(width) ? '' : ` width="${width}"`) +
(util.isDimensionMissing(height) ? '' : ` height="${height}"`);
return `<svg xmlns="http://www.w3.org/2000/svg"${svgSizing}>
<foreignObject${foreignObjectSizing}>${xhtml}</foreignObject>
</svg>`;
})
.then(function (svg) {
return `data:image/svg+xml;charset=utf-8,${svg}`;
});
}
其中的isDimensionMissing方法就是判斷是否是不合法的數(shù)字。
主要做了四件事。
一是給節(jié)點(diǎn)添加命名空間,并使用XMLSerializer對(duì)象來將DOM節(jié)點(diǎn)序列化成字符串。
二是轉(zhuǎn)換DOM字符串中的一些字符:
function escapeXhtml(string) {
return string.replace(/%/g, '%25').replace(/#/g, '%23').replace(/\n/g, '%0A');
}
第三步就是拼接svg字符串了,將序列化后的字符串使用foreignObject標(biāo)簽包裹,同時(shí)會(huì)計(jì)算一下DOM節(jié)點(diǎn)的寬高設(shè)置到svg上。
最后一步是拼接成data:URL的形式。
在最開始的【檢查和包裝元素】步驟會(huì)替換掉節(jié)點(diǎn)類型不為1的節(jié)點(diǎn),這一步就是用來恢復(fù)這個(gè)操作:
function restoreWrappers(result) {
while (restorations.length > 0) {
const restoration = restorations.pop();
restoration.parent.replaceChild(restoration.child, restoration.wrapper);
}
return result;
}
這一步結(jié)束后將節(jié)點(diǎn)轉(zhuǎn)換成svg的操作就結(jié)束了。
現(xiàn)在我們可以回到draw方法:
function draw(domNode, options) {
options = options || {};
return toSvg(domNode, options)
.then(util.makeImage)
.then(function (image) {
// ...
});
}
獲取到了svg的data:URL后會(huì)調(diào)用makeImage方法將它轉(zhuǎn)換成圖片,這個(gè)方法前面我們已經(jīng)看過了,這里就不重復(fù)說了。
繼續(xù)draw方法:
function draw(domNode, options) {
options = options || {};
return toSvg(domNode, options)
.then(util.makeImage)
.then(function (image) {
const scale = typeof options.scale !== 'number' ? 1 : options.scale;
const canvas = newCanvas(domNode, scale);
const ctx = canvas.getContext('2d');
ctx.msImageSmoothingEnabled = false;// 禁用圖像平滑
ctx.imageSmoothingEnabled = false;// 禁用圖像平滑
if (image) {
ctx.scale(scale, scale);
ctx.drawImage(image, 0, 0);
}
return canvas;
});
}
先調(diào)用newCanvas方法創(chuàng)建一個(gè)canvas:
function newCanvas(node, scale) {
let width = options.width || util.width(node);
let height = options.height || util.height(node);
// 如果寬度高度都沒有,那么默認(rèn)設(shè)置成300
if (util.isDimensionMissing(width)) {
width = util.isDimensionMissing(height) ? 300 : height * 2.0;
}
// 如果高度沒有,那么默認(rèn)設(shè)置成寬度的一半
if (util.isDimensionMissing(height)) {
height = width / 2.0;
}
// 創(chuàng)建canvas
const canvas = document.createElement('canvas');
canvas.width = width * scale;
canvas.height = height * scale;
// 設(shè)置背景顏色
if (options.bgcolor) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = options.bgcolor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas;
}
把svg圖片繪制到canvas上后,就可以通過canvas.toDataURL()方法轉(zhuǎn)換成圖片的data:URL,你可以渲染到頁面,也可以直接進(jìn)行下載。
本文通過源碼詳細(xì)介紹了dom-to-image-more的原理,核心就是克隆節(jié)點(diǎn)和節(jié)點(diǎn)樣式,內(nèi)聯(lián)字體、背景圖片、圖片,然后通過svg的foreignObject標(biāo)簽嵌入克隆后的節(jié)點(diǎn),最后將svg轉(zhuǎn)換成圖片,圖片繪制到canvas上進(jìn)行導(dǎo)出。
可以看到源碼中大量的Promise,很多不是異步的邏輯也會(huì)通過then方法來進(jìn)行管道式調(diào)用,大部分情況會(huì)讓代碼很清晰,一眼就知道大概做了什么事情,但是部分地方串聯(lián)了太長(zhǎng),反倒不太容易理解。
限于篇幅,源碼中其實(shí)還要很多有意思的細(xì)節(jié)沒有介紹,比如為了修改iframe的DOCTYPE和charset,居然寫了三種方式,雖然我覺得第一種就夠了,又比如獲取節(jié)點(diǎn)默認(rèn)樣式的方式,通過iframe創(chuàng)建同樣標(biāo)簽同樣層級(jí)的元素,說實(shí)話我是從來沒見過,再比如解析css中的字體的url時(shí)用的是如下方法:
function resolveUrl(url, baseUrl) {
const doc = document.implementation.createHTMLDocument();
const base = doc.createElement('base');
doc.head.appendChild(base);
const a = doc.createElement('a');
doc.body.appendChild(a);
base.href = baseUrl;
a.href = url;
return a.href;
}
base標(biāo)簽我也是從來沒有見過。等等。
所以看源碼還是挺有意思的一件事,畢竟平時(shí)寫業(yè)務(wù)代碼局限性太大了,很多東西都了解不到,強(qiáng)烈推薦各位去閱讀一下。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。