➡ 简

jsdom 中文文檔

jsdom

jsdom是一個純粹由 javascript 實現的一系列 web標準,特別是 WHATWG 組織制定的DOMHTML 標準,用於在 nodejs 中使用。大體上來說,該項目的目標是模擬足夠的Web瀏覽器子集,以便用於測試和挖掘真實世界的Web應用程序。

最新版本的 jsdom 運行環境需要 node.js v6或者更高的版本。(jsdom v10以下版本在 nodejs v4以下仍然可用,但是我們已經不支持維護了)

v10版本的 jsdom 擁有全新的 API(如下所述).舊的 API 現在仍然支持;詳細的參照文檔

基本用法

const jsdom = require("jsdom");
const { JSDOM } = jsdom;

為了使用 jsdom,主要用到jsdom主模塊的一個命名導出的 jsdom 構造函數。往構造器傳遞一個字符串,將會得到一個 jsdom 構造實例對象,這個對象有很多實用的屬性,特別是 window 對象:

const dom = new JSDOM(`

Hello world
`);
console.log(dom.window.document.querySelector("p").textContent); // "Hello world"

(請注意,jsdom會像瀏覽器一樣解析您傳遞的HTML,包括隱含的標記)

生成的對象是JSDOM類的一個實例,其中包括 window 對象在內的許多有用的屬性和方法。一般來說,它可以用來從“外部”對jsdom進行操作,而這些操作對於普通DOM API來說是不可能的。對於不需要任何功能的簡單場景,我們推薦使用類似的編碼模式

const { window } = new JSDOM(`...`);
// or even
const { document } = (new JSDOM(`...`)).window;

下面是關於JSDOM類所能做的一切的完整文檔,在“JSDOM對象API”部分。

定製 jsdom

JSDOM構造函數接受第二個參數,可以用以下方式定製您的jsdom。

簡單選項

const dom = new JSDOM(``, {
  url: "https://example.org/",
  referrer: "https://example.com/",
  contentType: "text/html",
  userAgent: "Mellblomenator/9000",
  includeNodeLocations: true
});

請注意urlreferrer在使用之前已經被規範化了,例如 如果你傳入"https:example.com",jsdom會自動規範化解釋為"https://example.com/"。 如果你傳遞了一個不可解析的URL,該調用將拋出錯誤。 (URL根據URL標準進行分析和序列化。)

執行腳本

jsdom最強大的功能是它可以在jsdom中執行腳本。這些腳本可以修改頁面的內容並訪問jsdom實現的所有Web平臺API。

但是,這在處理不可信內容時也非常危險。 jsdom沙箱並不是萬無一失的,在DOM的內部運行的代碼如果足夠深入,就可以訪問Node.js環境,從而訪問您的計算機。 因此,默認情況下,執行嵌入在HTML中的腳本的功能是禁用的:

const dom = new JSDOM(`

  
document.body.appendChild(document.createElement("hr"));


`);

// 腳本默認將不能執行:
dom.window.document.body.children.length === 1;

要在頁面內啟用腳本,可以使用runScripts:"dangerously"選項:

const dom = new JSDOM(`

  
document.body.appendChild(document.createElement("hr"));


`, { runScripts: "dangerously" });

// 腳本將執行並修改 DOM:
dom.window.document.body.children.length === 2;

我們再次強調只有在提供給jsdom的代碼是你已知道是安全的代碼時方可使用它。如果您運行了任意用戶提供的或Internet上的不可信的Node.js代碼,可能會危及您的計算機。

假如你想通過來執行外部腳本,你需要確保已經加載了它們。為此,請添加選項resources:"usable" 如下所述

請注意,除非runScripts設置為"dangerously",否則事件處理程序屬性(如)也將不起作用。(但是,事件處理函數屬性,比如div.onclick = ...,將無視runScripts參數 並且會起作用)

如果您只是試圖從“外部”執行腳本,而不是通過元素(和內聯事件處理程序)從內部運行“,則可以使用runScripts: "outside-only"選項,該選項會啟用window.eval

const window = (new JSDOM(``, { runScripts: "outside-only" })).window;

window.eval(`document.body.innerHTML = "
Hello, world!
";`);
window.document.body.children.length === 1;

由於性能原因,默認情況下會關閉此功能,但可以安全啟用。

請注意,我們強烈建議不要試圖通過將jsdom和Node全局環境混合在一起(例如,通過執行global.window = dom.window)來“執行腳本”,然後在Node全局環境中執行腳本或測試代碼。相反,您應該像對待瀏覽器一樣對待jsdom,並使用window.evalrunScripts: "dangerously"來運行需要訪問jsdom環境內的DOM的所有腳本和測試。例如,這可能需要創建一個browserify包作為元素執行 - 就像在瀏覽器中一樣。

最後,對於高級用例,您可以使用dom.runVMScript(腳本)方法,如下所述。

假裝成一個視覺瀏覽器

jsdom沒有渲染可視內容的能力,並且默認情況下會像無頭瀏覽器一樣工作。它通過API(如document.hidden)向網頁提供提示,表明其內容不可見。

pretendToBeVisual選項設置為true時,jsdom會假裝它正在呈現並顯示內容。它是這樣做的:

const window = (new JSDOM(``, { pretendToBeVisual: true })).window;

window.requestAnimationFrame(timestamp => {
  console.log(timestamp > 0);
});

請注意,jsdom仍然不做任何佈局或渲染,因此這實際上只是假裝為可視化,而不是實現真正的可視化Web瀏覽器將實現的部分。

加載子資源

默認情況下,jsdom不會加載任何子資源,如腳本,樣式表,圖像或iframe。如果您希望jsdom加載這些資源,則可以傳遞resources: "usable"選項,該選項將加載所有可用資源。資源列表如下:

未來,我們計劃通過此選項提供更多的資源加載定製,但現在只提供的兩種模式:'default''usable'

虛擬控制檯

像網頁瀏覽器一樣,jsdom也具有“控制檯”的概念。通過在文檔內執行的腳本以及來自jsdom本身實現的信息和記錄會從頁面直接發送過來。我們將用戶可控制的控制檯稱為“虛擬控制檯”,以便將其與Node.js console API和頁面內部的window.console API區分開來。

默認情況下,JSDOM構造函數將返回一個具有虛擬控制檯的實例,該虛擬控制檯將其所有輸出轉發到Node.js控制檯。為了創建自己的虛擬控制檯並將其傳遞給jsdom,可以通過執行下面代碼來覆蓋此默認值

const virtualConsole = new jsdom.VirtualConsole();
const dom = new JSDOM(``, { virtualConsole });

這樣的代碼將創建一個沒有任何行為的虛擬控制檯。您可以為所有可能的控制檯方法添加事件偵聽器來為其提供行為:

virtualConsole.on("error", () => { ... });
virtualConsole.on("warn", () => { ... });
virtualConsole.on("info", () => { ... });
virtualConsole.on("dir", () => { ... });
// ... etc. See https://console.spec.whatwg.org/#logging

請注意,最好在調用 new JSDOM()之前設置這些事件偵聽器,因為在解析期間可能會發生錯誤或控制檯調用腳本錯誤。)

如果你只是想將虛擬控制檯輸出重定向到另一個控制檯,比如默認的Node.js,你可以這樣做

virtualConsole.sendTo(console);

還有一個特殊的事件,"jsdomError",它的觸發將通過錯誤對象來記錄jsdom本身的錯誤。這與錯誤消息在Web瀏覽器控制檯中的顯示方式類似,即使它們不是由console.error輸出的。到目前為止,錯誤會按照下面的方式輸出:

如果您使用sendTo(c)將錯誤發送給c,則默認情況下,它將使用來自"jsdomError"事件的信息調用console.error。如果您希望保持事件與方法調用的嚴格的一對一映射,並且可能自己處理"jsdomError",那麼您可以執行

virtualConsole.sendTo(c, { omitJSDOMErrors: true });

Cookie jars(存儲Cookie的容器)

像網頁瀏覽器一樣,jsdom也具有cookie jar的概念,存儲HTTP cookie 。在文檔的同一個域上一個URL,並且沒有標記為HTTP only的cookies,可以通過document.cookie API來訪問。此外,Cookie jar中的所有cookie都會影響子資源的http加載。

默認情況下,JSDOM構造函數將返回一個帶有空cookie的實例。要創建自己的cookie jar並將其傳遞給jsdom,可以通過以下代碼來覆蓋默認值

const cookieJar = new jsdom.CookieJar(store, options);
const dom = new JSDOM(``, { cookieJar });

如果您想要在多個jsdoms中共享同一個cookie jar,或者提前使用特定的值來填充cookie jar,這將非常有用。

Cookie jar包由tough-cookie包提供的。jsdom.CookieJar構造函數是tough-cookie cookie jar的子類,並且默認設置了looseMode:true選項,因為它更符合瀏覽器的行為方式。如果您想自己使用tough-cookie的方法和類,則可以使用jsdom.toughCookie模塊導出來訪問使用jsdom打包的tough-cookie模塊實例。

在解析之前進行干預

jsdom允許您在很早的時候介入創建jsdom:創建Window和Document對象之後,但在解析任何HTML並使用節點填充文檔之前

const dom = new JSDOM(`
Hello
`, {
  beforeParse(window) {
    window.document.childNodes.length === 0;
    window.someCoolAPI = () => { /* ... */ };
  }
});

如果您希望以某種方式修改環境,這尤其有用,例如添加jsdom不支持的Web API的填充程序。

JSDOM object API

一旦你構建了一個JSDOM對象,它將具有以下有用的功能:

Properties

window屬性: window對象的key 從Window 對象檢索而來 virtualConsolecookieJar:可以傳入或者使用默認值

通過serialize()序列化document

const dom = new JSDOM(`
hello`);

dom.serialize() === "




hello

";

// Contrast with:
dom.window.document.documentElement.outerHTML === "



hello

";

通過nodeLocation(node)獲取 dom 節點的源位置信息

nodeLocation()方法將查找DOM節點在源文檔中的位置,並返回節點的parse5位置信息

const dom = new JSDOM(
  `
Hello
    

  
`,
  { includeNodeLocations: true }
);

const document = dom.window.document;
const bodyEl = document.body; // implicitly created
const pEl = document.querySelector("p");
const textNode = pEl.firstChild;
const imgEl = document.querySelector("img");

console.log(dom.nodeLocation(bodyEl));   // null; it's not in the source
console.log(dom.nodeLocation(pEl));      // { startOffset: 0, endOffset: 39, startTag: ..., endTag: ... }
console.log(dom.nodeLocation(textNode)); // { startOffset: 3, endOffset: 13 }
console.log(dom.nodeLocation(imgEl));    // { startOffset: 13, endOffset: 32 }

請注意,只有您設置了includeNodeLocations選項才能使用此功能;由於性能原因,節點位置默認為關閉。

使用runVMScript(script)運行vm創建的腳本

Node.js的內置vm模塊允許您創建Script實例,這些腳本實例可以提前編譯,然後在給定的“VM上下文”上運行多次。在這個場景背後,jsdom Window是一個確定的VM上下文。要訪問此功能,請使用runVMScript()方法:

const { Script } = require("vm");

const dom = new JSDOM(``, { runScripts: "outside-only" });
const s = new Script(`
  if (!this.ran) {
    this.ran = 0;
  }

  ++this.ran;
`);

dom.runVMScript(s);
dom.runVMScript(s);
dom.runVMScript(s);

dom.window.ran === 3;

這是高級功能,除非您有特殊的需求,否則我們建議堅持使用普通的DOM API(如window.eval()或document.createElement(“script”))。

通過reconfigure(settings)重新配置jsdom

window.top屬性在規範中被標記為[Unforgeable][中文:偽造的],這意味著它是一個不可配置的私有屬性,因此在jsdom內運行的普通代碼是不能覆蓋或遮擋它的,即使使用Object.defineProperty

同樣,目前在jsdom中是不能夠處理navigation相關信息的(比如設置window.location.href ="https://example.com/");這樣做會導致虛擬控制檯發出"jsdomError",說明此功能未實現,並且沒有任何變化,也將不會有新的WindowDocument對象,並且現有window.location對象仍保持當前所有相同的屬性值。

但是,如果您從 jsdom 窗口之外進行演示,例如在一些創建jsdoms的測試框架中,可以使用特殊的reconfigure()方法覆蓋其中的一個或兩個:

const dom = new JSDOM();

dom.window.top === dom.window;
dom.window.location.href === "about:blank";

dom.reconfigure({ windowTop: myFakeTopForTesting, url: "https://example.com/" });

dom.window.top === myFakeTopForTesting;
dom.window.location.href === "https://example.com/";

請注意,更改jsdom的URL將影響所有返回當前 document URL的API,例如window.locationdocument.URL``和document.documentURI,以及文檔中相對URL的解析以及同源檢查和提取子資源時使用的引用。但是,它不會執行導航到該URL的內容;DOM的內容將保持不變,並且不會創建WindowDocument等新的實例。

便捷的 APIs

fromURL()

除了JSDOM構造函數本身之外,jsdom還提供了一個返回 Promise 的工廠方法,用於通過URL構建一個jsdom實例

JSDOM.fromURL("https://example.com/", options).then(dom => {
  console.log(dom.serialize());
});

如果URL有效且請求成功,則onFullfilled回調執行並返回JSDOM實例。任何URL重定向都將遵循其最終目的地。

fromURL()提供的參數選項與提供給JSDOM構造函數的選項類似,但具有以下額外的限制和後果:

初始的請求並不能無限定製到像request npm 包一樣的程度;fromURL()旨在為大多數情況提供便利的API。如果您需要更好地控制初始請求,您應該自己執行它,然後手動使用JSDOM構造函數。

fromFile()

fromURL()類似,jsdom還提供了一個fromFile()工廠方法,用於從文件名構建jsdom

JSDOM.fromFile("stuff.html", options).then(dom => {
  console.log(dom.serialize());
});

如果可以打開給定的文件,則onFullfilled回調執行並返回JSDOM實例。和Node.js API一樣,文件名是相對於當前工作目錄的。

fromFile()提供的選項與提供給JSDOM構造函數的選項相似,但具有以下額外的默認值:

fragment()

對於最簡單的情況,你可能不需要一個完整的JSDOM實例及其所有相關的功能。您甚至可能不需要WindowDocument!相反,你只需要解析一些HTML片段,並獲得一個你可以操作的DOM對象。為此,我們提供了fragment(),它可以從給定的字符串中創建一個DocumentFragment

const frag = JSDOM.fragment(`
Hello


Hi!
`);

frag.childNodes.length === 2;
frag.querySelector("strong").textContent = "Why hello there!";
// etc.

fragDocumentFragment的實例對象,其內容是通過提供的字符串解析創建的。解析是通過使用元素完成的,因此您可以在其中包含任何元素(包括具有奇怪解析規則的元素,如)。

fragment()工廠函數的所有調用結果的DocumentFragments實例都會共享相同的DocumentWindow。這允許多次調用fragment()而沒有額外的開銷。但這也意味著對fragment()的調用不能用任何選項自定義。

請注意,對DocumentFragments的序列化並不像使用JSDOM對象那樣容易。如果你需要序列化你的DOM,你應該直接使用JSDOM構造函數。但對於包含單個元素的片段的特殊情況,通過常規方法就很容易做到。

const frag = JSDOM.fragment(`
Hello
`);
console.log(frag.firstChild.outerHTML); // logs "
Hello
"

其他值得注意的功能

支持 Canvas

jsdom支持使用canvascanvas-prebuilt包來擴展任何使用canvas API的元素。為了做到這一點,您需要將canvas作為依賴項加入到您的項目中,和 jsdom包並列。如果jsdom可以找到canvas包,它將使用它,但是如果它不存在,那麼元素的行為就像一樣。

編碼嗅探

除了提供一個字符串外,JSDOM構造函數還支持Node.js Buffer或標準JavaScript二進制數據類型(如ArrayBuffer,Uint8Array,DataView等)的形式提供二進制數據。當完成後,jsdom將從提供的字節進行嗅探編碼,就像瀏覽器掃描標籤一樣。

這種編碼嗅探也適用於JSDOM.fromFile()JSDOM.fromURL()。在後一種情況下,就像在瀏覽器中一樣,任何與response響應一起發送的Content-Type頭信息優先級更高。

請注意,在許多情況下,提供字節這種方式可能比提供字符串更好。例如,如果您試圖使用Node.js的buffer.toString('utf-8')API,則Node.js將不會去除任何前導BOM。如果您將此字符串提供給jsdom,它會逐字解釋,從而使BOM保持不變。但jsdom的二進制數據解碼代碼將剝離前導的BOM,就像瀏覽器一樣;在這種情況下,直接提供buffer將會得到想要的結果。

關閉一個jsdom

jsdom中定義的定時器(通過window.setTimeoutwindow.setInterval設置)將在window上下文中執行代碼。由於進程在不活躍的情況下無法執行未來的定時器代碼,所以卓越的jsdom定時器將保持您的Node.js進程處於活動狀態。同樣,對象不活躍的情況下也沒有辦法在對象的上下文中執行代碼,卓越的jsdom定時器將阻止垃圾回收調度它們的window。

如果你想確保關閉jsdom窗口,使用window.close(),它將終止所有正在運行的定時器(並且還會刪除 windowdocument上的任何事件監聽器)。

在Web瀏覽器中運行jsdom

使用browserify模塊,jsdom某些方面也支持在Web瀏覽器中運行。也就是說,在Web瀏覽器中,您可以使用被browserify模塊編譯過的jsdom去創建完全獨立的普通JavaScript對象集,其外觀和行為與瀏覽器的現有DOM對象非常相似,但完全獨立於它們,也就是"虛擬DOM"!

jsdom的主要目標對象仍然是Node.js,因此我們使用僅存在於最新Node.js版本(即Node.js v6 +)中的語言特性功能。因此,在舊版瀏覽器可能無法正常工作。(即使編譯也不會有多大幫助:我們計劃在jsdom v10.x的整個過程中廣泛使用Proxy。)

值得注意的是,jsdom在web worker中能很好的運行。項目的開發者@lawnsea使這一功能點成為可能,他發表了一篇關於他的項目的論文,該論文就使用了這種能力。

在Web瀏覽器中運行jsdom時,並非所有的工作都完美。有些情況下,這是由於基礎的條件限制(比如沒有文件系統訪問),但有些情況下也是因為我們沒有花足夠的時間去進行適當的小調整。歡迎大家來提BUG。

使用Chrome Devtools調試DOM

從Node.js v6開始,您可以使用Chrome Devtools來調試程序。請參閱官方文檔瞭解如何使用。

默認情況下,jsdom元素在控制檯中被格式化為普通的舊JS對象。為了便於調試,可以使用jsdom-devtools-formatter,它可以讓你像真正的DOM元素一樣調試它們。

注意事項

異步腳本加載

使用jsdom時,開發者在加載異步腳本時經常遇到麻煩。許多頁面異步加載腳本,但無法分辨腳本什麼時候完成,因此無法知道何時是運行代碼並檢查生成的DOM結構的好時機。這是一個基本的限制;我們無法預測網頁上的哪些腳本會做什麼,因此無法告訴您腳本何時加載完畢。

這個問題可以通過幾種方法來解決。如果您能控制頁面邏輯,最好的方法是使用腳本加載器提供的機制來檢測何時加載完成。例如,如果您使用像RequireJS這樣的模塊加載器,代碼可能如下所示:

// On the Node.js side:
const window = (new JSDOM(...)).window;
window.onModulesLoaded = () => {
  console.log("ready to roll!");
};



requirejs(["entry-module"], () => {
  window.onModulesLoaded();
});


如果您不能控制該頁面,則可以嘗試其他解決方法,例如輪詢檢查特定元素是否存在。有關更多詳細信息,請查看#640中的討論,尤其是@ matthewkastor深刻見解

共享的構造函數和原型

目前,對於大多數Web平臺API,jsdom在多個看似獨立的jsdoms之間共享相同的類定義。這將意味著,可能會出現以下情況

const dom1 = new JSDOM();
const dom2 = new JSDOM();

dom1.window.Element.prototype.expando = "blah";
console.log(dom2.window.document.createElement("frameset").expando); // logs "blah"

這主要是出於性能和內存的原因:如果在Web平臺上每次創建jsdom時,創建所有類的單獨副本,開銷將會相當昂貴。

儘管如此,我們仍然有興趣在有一天提供一個選項配置來創建一個“獨立”的jsdom,但要犧牲一些性能。

新API中缺失的功能

與v9.x之前的舊版jsdom API相比,新API顯然缺少對資源加載的精細控制。先前版本的jsdom允許您設置request時使用的選項(既可以用於初始請求,也可以用於舊版本的JSDOM.fromURL()和子資源請求)。他們還允許您控制請求哪些子資源並將其應用於主文檔,以便您可以下載樣式表,但不下載腳本文件。最後,他們提供了一個可定製的資源加載器,可以攔截任何傳出的請求並用完全合成的response 響應來結束。

以上這些功能尚未在新的jsdom API中實現,儘管我們也希望儘快將它們添加回來,但不幸的是,這需要相當大的幕後工作去實施。

同時,請隨時使用舊的jsdom API來訪問此功能。它一直處於支持和維護中,但它不會獲得新功能。舊的文檔位於lib/old-api.md中。

未實現的Web平臺部分

目前jsdom中有很多缺失的API,儘管我們也想要在jsdom中添加新的功能並保持最新的Web規範。請隨時為缺失的任何內容提交issue,但我們是一個很小並且忙碌的團隊,因此大家一起來提交 pull request可能會更好。

除了我們尚未擁有的功能之外,還有兩個主要功能目前超出了jsdom的範圍。這些是:

目前,jsdom對某些功能的某些方面具有虛擬行為,例如操作navigation 時向虛擬控制檯發送“未實現的”"jsdomError",或者為許多與佈局相關的屬性返回0。您通常可以在代碼中解決這些限制,例如通過在爬網過程中為每個頁面創建新的JSDOM實例,或使用Object.defineProperty更改各種與佈局相關的getter和方法的返回值

請注意,相同領域中的其他工具(如PhantomJS)確實支持這些功能。在wiki上,我們有關於jsdom vs. PhantomJS的更完整的比較介紹。

獲取幫助

如果您需要jsdom的幫助,請隨時使用以下任何方式:

特別聲明

以上文檔翻譯自開源項目 jsdom,如有翻譯錯誤,歡迎指正。

jsdom 原文鏈接

jsdom 項目鏈接

原文博客地址