@lwjjike/xbsdom
v28.1.2
Published
A JavaScript implementation of many web standards
Readme
使用
npm i @lwjjike/xbsdomxbsdom通过魔改jsdom,除去jsdom中的大量特征,模拟真实浏览器的值
v28.1.2更新说明:
- 完美添加了navigator.plugins和navigator.mimeTypes
- 修复了navigator.userAgent和navigator.deviceMemory的问题
- JSDOM构造函数第二哥参数中增加了xbsConfig配置项,可以自由配置userAgent和deviceMemory的值
- JSDOM实例上增肌了setHistoryLength方法,可用来设置history.length值
xbsConfig配置说明:
const jsdom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
xbsConfig: {
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
deviceMemory: 8
}
});
const window = jsdom.window;
console.log(window.navigator.userAgent);
console.log(window.navigator.deviceMemory);
jsdom.setHistoryLength(10);
console.log(window.history.length); // 10以下是jsdom官方包的介绍: jsdom 是一个纯 JavaScript 实现,它支持许多 Web 标准,特别是 WHATWG 的 DOM 和 HTML 标准,并可与 Node.js 配合使用。该项目的目标是模拟 Web 浏览器的子集,使其能够用于测试和抓取真实 Web 应用程序。
最新版本的 jsdom 需要较新版本的 Node.js;详情请参阅 package.json 文件中的 engines 字段。
基本用法
const jsdom = require("jsdom");
const { JSDOM } = jsdom;要使用 jsdom,您主要会用到 JSDOM 构造函数,它是 jsdom 主模块的一个命名导出。将一个字符串传递给构造函数。您将获得一个 JSDOM 对象,该对象具有许多有用的属性,特别是 window 属性:
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
console.log(dom.window.document.querySelector("p").textContent); // "Hello world"(请注意,jsdom 会像浏览器一样解析您传递的 HTML,包括隐含的 <html>、<head> 和 <body> 标签。)
生成的对象是 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",
includeNodeLocations: true,
storageQuota: 10000000
});url设置window.location、document.URL和document.documentURI返回的值,并影响文档内相对 URL 的解析、同源限制以及获取子资源时使用的引用页等。默认值为"about:blank"。referrer仅影响从document.referrer读取的值。默认值为空(即没有引用页)。contentType影响从document.contentType读取的值,以及文档的解析方式:HTML 或 XML。非 HTML MIME 类型(https://mimesniff.spec.whatwg.org/#html-mime-type)或非 XML MIME 类型(https://mimesniff.spec.whatwg.org/#xml-mime-type)的值会抛出异常。默认值为"text/html"。如果存在charset参数,它可能会影响二进制数据处理。includeNodeLocations会保留 HTML 解析器生成的位置信息,允许您使用nodeLocation()方法(如下所述)检索它。它还能确保在<script>元素内运行的代码的异常堆栈跟踪中报告的行号正确。为了获得最佳性能,它默认为false,并且不能与 XML 内容类型一起使用,因为我们的 XML 解析器不支持位置信息。storageQuota是localStorage和sessionStorage使用的独立存储区域的最大代码单元大小。尝试存储超过此限制的数据将导致抛出DOMException异常。默认情况下,它设置为每个源 5,000,000 个代码单元,这是受 HTML 规范的启发。
请注意,url 和 referrer 在使用前都会被规范化,例如:如果传入 "https:example.com",jsdom 会将其解释为 "https://example.com/"。如果传入无法解析的 URL,调用将会抛出异常。(URL 会根据 URL 标准 进行解析和序列化。)
执行脚本
jsdom 最强大的功能在于它可以在 jsdom 内部执行脚本。这些脚本可以修改页面内容,并访问 jsdom 实现的所有 Web 平台 API。
然而,当处理不受信任的内容时,这也极其危险。jsdom 沙箱并非万无一失,运行在 DOM 的 <script> 标签内的代码,如果尝试足够多,就有可能获得对 Node.js 环境的访问权限,进而访问你的计算机。因此,默认情况下,执行嵌入在 HTML 中的脚本的功能是被禁用的。
const dom = new JSDOM(`<body>
<div id="content"></div>
<script>document.getElementById("content").append(document.createElement("hr"));</script>
</body>`);
// 默认情况下,脚本不会执行:
console.log(dom.window.document.getElementById("content").children.length); // 0要允许在页面内执行脚本,可以使用 runScripts: "dangerously" 选项:
const dom = new JSDOM(`<body>
<div id="content"></div>
<script>document.getElementById("content").append(document.createElement("hr"));</script>
</body>`, { runScripts: "dangerously" });
// 该脚本将被执行并修改 DOM:
console.log(dom.window.document.getElementById("content").children.length); // 1我们再次强调,仅在向 jsdom 提供您确定安全的代码时才使用此功能。如果您将其用于任意用户提供的代码或来自互联网的代码,则实际上您是在运行不受信任的 Node.js 代码,您的计算机可能会受到攻击。
如果您想要执行通过 <script src=""> 引入的外部脚本,您还需要确保它们能够加载。为此,请添加 resources: "usable" 选项(如下所述)。(您可能还需要设置 url 选项,原因已在下文讨论。)
事件处理程序属性(例如 <div onclick="">)也受此设置控制;除非将 runScripts 设置为 "dangerously",否则它们将无法工作。(但是,事件处理程序属性(例如 div.onclick = ...)无论 runScripts 是否设置为 "dangerously" 都将正常工作。)请注意,此保证涵盖 jsdom 正在处理的 Web 内容。它不涵盖宿主 Node.js 环境本身已被攻破的情况(例如,通过原型污染)。更多详情请参阅安全策略。
如果您只是想“从外部”执行脚本,而不是让 <script> 元素和事件处理程序属性“从内部”运行,则可以使用 runScripts: "outside-only" 选项,该选项允许在 window 上安装所有 JavaScript 规范提供的全局变量的全新副本。这包括 window.Array、window.Promise 等。值得注意的是,它还包括 window.eval,该方法允许运行脚本,但全局变量是 jsdom 的 window。
const dom = new JSDOM(`<body>
<div id="content"></div>
<script>document.getElementById("content").append(document.createElement("hr"));</script>
</body>`, { runScripts: "outside-only" });
// run a script outside of JSDOM:
dom.window.eval('document.getElementById("content").append(document.createElement("p"));');
console.log(dom.window.document.getElementById("content").children.length); // 1
console.log(dom.window.document.getElementsByTagName("hr").length); // 0
console.log(dom.window.document.getElementsByTagName("p").length); // 1出于性能考虑,此功能默认关闭,但启用它是安全的。
请注意,在默认配置下,如果不设置 runScripts,window.Array、window.eval 等的值将与外部 Node.js 环境提供的值相同。也就是说,window.eval === eval 将成立,因此 window.eval 将无法有效地运行脚本。
我们强烈建议不要尝试通过将 jsdom 和 Node 全局环境混在一起(例如,使用 global.window = dom.window)来“执行脚本”,然后在 Node 全局环境中执行脚本或测试代码。相反,您应该像对待浏览器一样对待 jsdom,并在 jsdom 环境中运行所有需要访问 DOM 的脚本和测试,使用 window.eval 或 runScripts: "dangerously"。例如,这可能需要创建一个 Browserify 包,并将其作为 <script> 元素执行——就像在浏览器中一样。
最后,对于高级用例,您可以使用 dom.getInternalVMContext() 方法,具体文档如下。
假装是可视化浏览器
jsdom 本身不具备渲染视觉内容的能力,默认情况下会像一个无头浏览器一样运行。它会通过 document.hidden 等 API 向网页发出提示,表明其内容不可见。
当 pretendToBeVisual 选项设置为 true 时,jsdom 会假装正在渲染和显示内容。具体做法如下:
将
document.hidden的返回值从true改为false将
document.visibilityState的返回值从prerender改为visible启用原本不存在的
window.requestAnimationFrame()和window.cancelAnimationFrame()方法
const window = (new JSDOM(``, { pretendToBeVisual: true })).window;
window.requestAnimationFrame(timestamp => {
console.log(timestamp > 0);
});请注意,jsdom 仍然不进行任何布局或渲染,所以这实际上只是_假装_具有视觉效果,而不是实现真正的、可视化的 Web 浏览器会实现的平台部分。
正在加载子资源
基本选项
默认情况下,jsdom 不会加载任何子资源,例如脚本、样式表、图像或 iframe。如果您希望 jsdom 加载这些资源,可以传递 resources: "usable" 选项,该选项将加载所有可用的资源。这些资源包括:
通过
<frame>和<iframe>加载的框架和 iframe通过
<link rel="stylesheet">加载的样式表通过
<script>加载的脚本,但前提是同时设置了runScripts: "dangerously"通过
<img>加载的图像,但前提是同时安装了canvasnpm 包(请参阅下文的 Canvas 支持)。
尝试加载资源时,请记住 url 选项的默认值为 "about:blank",这意味着任何通过相对 URL 包含的资源都将无法加载。 (尝试将 URL /something 与 URL about:blank 进行解析会出错。)因此,在这些情况下,您可能需要为 url 选项设置一个非默认值,或者使用可以自动执行此操作的便捷 API之一。
高级配置
为了更全面地自定义 jsdom 的资源加载行为,包括通过 JSDOM.fromURL() 发起的初始加载,以及通过 dom.window.XMLHttpRequest 或 dom.window.WebSocket 发起的任何加载,您可以将一个选项对象作为 resources 选项的值传递。这样做会将上述 resources: "usable" 行为作为基准,您可以在此基础上进行自定义。
可用选项包括:
userAgent会影响发送的User-Agent标头,从而影响navigator.userAgent的值。其默认值为Mozilla/5.0 (${process.platform || "unknown OS"}) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/${jsdomVersion}。dispatcher可以设置为自定义的 undiciDispatcher,用于配置代理或自定义 TLS 设置等高级用例。例如,要使用代理,可以使用 undici 的ProxyAgent。interceptors可以设置为 undici 拦截器函数 数组。拦截器可用于修改请求或响应,而无需编写全新的Dispatcher。
对于检查传入请求或返回合成响应的简单情况,您可以使用 jsdom 的 requestInterceptor() 辅助函数,它接收一个 Request 对象和上下文,并可以返回一个 Response:
const { JSDOM, requestInterceptor } = require("jsdom");
const dom = new JSDOM(`<script src="https://example.com/some-specific-script.js"></script>`, {
url: "https://example.com/",
runScripts: "dangerously",
resources: {
userAgent: "Mellblomenator/9000",
dispatcher: new ProxyAgent("http://127.0.0.1:9001"),
interceptors: [
requestInterceptor((request, context) => {
// Override the contents of this script to do something unusual.
if (request.url === "https://example.com/some-specific-script.js") {
return new Response("window.someGlobal = 5;", {
headers: { "Content-Type": "application/javascript" }
});
}
// Return undefined to let the request proceed normally
})
]
}
});传递给拦截器的上下文对象包含 element(发起请求的 DOM 元素,如果请求并非来自 DOM 元素,则为 null)。例如:
requestInterceptor((request, { element }) => {
if (element) {
console.log(`Element ${element.localName} is requesting ${request.url}`);
}
// Return undefined to let the request proceed normally
})为了更清楚地了解流程:当您的 jsdom 中的某些代码获取资源时,首先由 jsdom 设置请求,然后按提供的顺序传递给所有 interceptors(拦截器),接着到达任何提供的 dispatcher(调度器,默认为 undici 的全局调度器)。如果您使用 jsdom 的 requestInterceptor(),返回一个用 Response 解决的 promise 将阻止任何后续拦截器运行,或者阻止到达基础调度器。
[!WARNING] 当 jsdom 内部的脚本使用同步的
XMLHttpRequest时,所有的资源加载自定义都将被忽略。这是一个技术限制,因为我们无法跨进程边界传递调度器或拦截器。
虚拟控制台
与 Web 浏览器类似,jsdom 也有“控制台”的概念。它既记录直接来自页面的信息(通过在文档内执行的脚本使用 window.console API),也记录来自 jsdom 实现本身的信息。我们将用户可控的控制台称为“虚拟控制台”,以区别于 Node.js 的 console API 和页面内的 window.console API。
默认情况下,JSDOM 构造函数将返回一个包含虚拟控制台的实例,该控制台会将其所有输出转发到 Node.js 控制台。这包括 jsdom 的输出(如未实现警告或 CSS 解析错误)和页面内的 window.console 调用。
要创建自己的虚拟控制台并将其传递给 jsdom,您可以通过以下方式覆盖此默认设置:
const virtualConsole = new jsdom.VirtualConsole();
const dom = new JSDOM(``, { virtualConsole });这样的代码将创建一个没有任何行为的虚拟控制台。您可以通过为所有可能的控制台方法添加事件监听器来赋予它行为:
virtualConsole.on("error", () => { ... });
virtualConsole.on("warn", () => { ... });
virtualConsole.on("info", () => { ... });
virtualConsole.on("dir", () => { ... });
// ... 等等。请参阅 https://console.spec.whatwg.org/#logging(请注意,最好在调用 new JSDOM() _之前_设置这些事件监听器,因为错误或调用控制台的脚本可能会在解析期间发生。)
如果您只是想将虚拟控制台的输出重定向到另一个控制台(如默认的 Node.js 控制台),您可以这样做:
virtualConsole.forwardTo(console);还有一个特殊的事件 "jsdomError",它会带着错误对象触发,以报告来自 jsdom 本身的错误。这类似于错误消息通常如何在 Web 浏览器控制台中显示,即使它们不是由 console.error 启动的。
如上所述,jsdom 的默认行为是将这些错误发送到 Node.js 控制台。这是通过 console.error(jsdomError.message) 完成的,或者在从 jsdom 中运行的脚本发生的 "unhandled-exception" 类型的 jsdom 错误的情况下,通过 console.error(jsdomError.cause.stack) 完成。使用 forwardTo() 将产生相同的行为。如果您想要非默认行为,您可以通过以下方式进行自定义:
// 不要将任何 jsdom 错误发送到 Node.js 控制台:
virtualConsole.forwardTo(console, { jsdomErrors: "none" });
// 仅将某些 jsdom 错误发送到 Node.js 控制台,忽略其他错误:
virtualConsole.forwardTo(console, { jsdomErrors: ["unhandled-exception", "not-implemented"]});
// 自定义处理所有 jsdom 错误:
virtualConsole.forwardTo(console, { jsdomErrors: "none" });
virtualConsole.on("jsdomError", err => {
switch (err.type) {
case "unhandled-exception": {
// ... 处理 ...
break;
}
case "css-parsing": {
// ... 以其他方式处理 ...
break;
}
// ... 等等 ...
}
});每种类型的 jsdom 错误的详细信息(按其 type 属性列出)如下:
"css-parsing": 解析 CSS 样式表时出错cause: 来自我们的 CSS 解析器库@acemir/cssom的异常对象sheetText: 我们试图解析的样式表的全文
"not-implemented": 当调用未实现的 Web 平台部分中的某些存根方法时发出的错误"resource-loading": 加载资源时出错,例如由于网络错误或来自服务器的错误响应代码cause属性:来自 jsdom 在检索资源时进行的内部 Node.js 网络调用的异常对象,或来自开发者的自定义资源加载器的异常对象url属性:尝试获取的资源的 URL
"unhandled-exception": 未被Window"error"事件监听器处理的脚本执行错误cause属性:包含原始异常对象
Cookie 罐 (Cookie jars)
像 Web 浏览器一样,jsdom 有 cookie 罐的概念,用于存储 HTTP cookie。与文档同域且未标记为 HTTP-only 的 Cookie 可以通过 document.cookie API 访问。此外,cookie 罐中的所有 cookie 都会影响子资源的获取。
默认情况下,JSDOM 构造函数将返回一个包含空 cookie 罐的实例。要创建自己的 cookie 罐并将其传递给 jsdom,您可以通过以下方式覆盖此默认设置:
const cookieJar = new jsdom.CookieJar(store, options);
const dom = new JSDOM(``, { cookieJar });这主要在您想在多个 jsdom 之间共享同一个 cookie 罐,或者提前用某些值填充 cookie 罐时非常有用。
Cookie 罐由 tough-cookie 包提供。jsdom.CookieJar 构造函数是 tough-cookie 的 cookie 罐的一个子类,默认设置了 looseMode: true 选项,因为这更好地匹配了浏览器的行为。如果您想自己使用 tough-cookie 的实用程序和类,可以使用 jsdom.toughCookie 模块导出以访问与 jsdom 打包的 tough-cookie 模块实例。
解析前干预
jsdom 允许您在创建 jsdom 的非常早期阶段进行干预:在 Window 和 Document 对象创建之后,但在解析任何 HTML 以用节点填充文档之前:
const dom = new JSDOM(`<p>Hello</p>`, {
beforeParse(window) {
window.document.childNodes.length === 0;
window.someCoolAPI = () => { /* ... */ };
}
});这在您想要以某种方式修改环境时特别有用,例如为 jsdom 不支持的 Web 平台 API 添加垫片 (shims)。
JSDOM 对象 API
构建 JSDOM 对象后,它将具有以下有用的功能:
属性
window 属性获取为您创建的 Window 对象。
virtualConsole 和 cookieJar 属性反映了您传入的选项,或者如果没有为这些选项传入任何内容,则反映为您创建的默认值。
使用 serialize() 序列化文档
serialize() 方法将返回文档的 HTML 序列化,包括 doctype:
const dom = new JSDOM(`<!DOCTYPE html>hello`);
dom.serialize() === "<!DOCTYPE html><html><head></head><body>hello</body></html>";
// 对比:
dom.window.document.documentElement.outerHTML === "<html><head></head><body>hello</body></html>";使用 nodeLocation(node) 获取节点的源位置
nodeLocation() 方法将查找 DOM 节点在源文档中的位置,返回该节点的 parse5 位置信息:
const dom = new JSDOM(
`<p>Hello
<img src="foo.jpg">
</p>`,
{ includeNodeLocations: true }
);
const document = dom.window.document;
const bodyEl = document.body; // 隐式创建
const pEl = document.querySelector("p");
const textNode = pEl.firstChild;
const imgEl = document.querySelector("img");
console.log(dom.nodeLocation(bodyEl)); // null; 它不在源代码中
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 选项时,此功能才有效;出于性能原因,节点位置默认是关闭的。
使用 getInternalVMContext() 与 Node.js vm 模块交互
Node.js 的内置 vm 模块是 jsdom 脚本运行魔法的基础。某些高级用例,如预编译脚本然后多次运行它,会从直接将 vm 模块与 jsdom 创建的 Window 结合使用中受益。
要获得适用于 vm API 的上下文全局对象,您可以使用 getInternalVMContext() 方法:
const { Script } = require("vm");
const dom = new JSDOM(``, { runScripts: "outside-only" });
const script = new Script(`
if (!this.ran) {
this.ran = 0;
}
++this.ran;
`);
const vmContext = dom.getInternalVMContext();
script.runInContext(vmContext);
script.runInContext(vmContext);
script.runInContext(vmContext);
console.assert(dom.window.ran === 3);这属于高级功能,除非您有非常具体的需求,否则我们建议坚持使用普通的 DOM API(例如 window.eval() 或 document.createElement("script"))。
请注意,如果在创建 JSDOM 实例时未设置 runScripts,则此方法将抛出异常。
使用 reconfigure(settings) 重新配置 jsdom
规范中 window 上的 top 属性被标记为 [Unforgeable],这意味着它是一个不可配置的自有属性,因此不能被 jsdom 内部运行的普通代码覆盖或遮蔽,即使使用 Object.defineProperty 也是如此。
同样,目前 jsdom 不处理导航(例如设置 window.location.href = "https://example.com/");这样做会导致虚拟控制台发出一个 "jsdomError",解释该功能未实现,并且没有任何变化:不会有新的 Window 或 Document 对象,现有的 window 的 location 对象仍然具有所有相同的属性值。
但是,如果您从窗口外部执行操作,例如在创建 jsdom 的某些测试框架中,您可以使用特殊的 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 将影响所有返回当前文档 URL 的 API,例如 window.location、document.URL 和 document.documentURI,以及文档内相对 URL 的解析,以及获取子资源时使用的同源检查和引用页。但是,它不会执行到该 URL 内容的导航;DOM 的内容将保持不变,并且不会创建 Window、Document 等的新实例。
便捷 API
fromURL()
除了 JSDOM 构造函数本身之外,jsdom 还提供了一个返回 promise 的工厂方法,用于从 URL 构造 jsdom:
JSDOM.fromURL("https://example.com/", options).then(dom => {
console.log(dom.serialize());
});如果 URL 有效且请求成功,则返回的 promise 将以一个 JSDOM 实例解决。任何重定向都将被跟踪到它们的最终目的地。
提供给 fromURL() 的选项与提供给 JSDOM 构造函数的选项类似,但有以下额外的限制和后果:
- 不能提供
url和contentType选项。 referrer选项用作初始请求的 HTTPReferer请求头。resources选项也影响初始请求;这在您想要例如配置代理时很有用(见上文)。- 生成的 jsdom 的 URL、内容类型和引用页由响应确定。
- 任何通过 HTTP
Set-Cookie响应头设置的 cookie 都存储在 jsdom 的 cookie 罐中。同样,提供的 cookie 罐中已有的任何 cookie 都会作为 HTTPCookie请求头发送。
fromFile()
类似于 fromURL(),jsdom 还提供了一个 fromFile() 工厂方法,用于从文件名构造 jsdom:
JSDOM.fromFile("stuff.html", options).then(dom => {
console.log(dom.serialize());
});如果给定的文件可以被打开,返回的 promise 将以一个 JSDOM 实例解决。与 Node.js API 中的惯例一样,文件名是相对于当前工作目录给出的。
提供给 fromFile() 的选项与提供给 JSDOM 构造函数的选项类似,但有以下额外的默认值:
url选项将默认为对应于给定文件名的文件 URL,而不是"about:blank"。- 如果给定的文件名以
.xht、.xhtml或.xml结尾,contentType选项将默认为"application/xhtml+xml";否则它将继续默认为"text/html"。
fragment()
对于最简单的情况,您可能不需要一个完整的 JSDOM 实例及其所有相关功能。您甚至可能不需要 Window 或 Document!相反,您只需要解析一些 HTML,并获得一个可以操作的 DOM 对象。为此,我们有 fragment(),它从给定的字符串创建一个 DocumentFragment:
const frag = JSDOM.fragment(`<p>Hello</p><p><strong>Hi!</strong>`);
frag.childNodes.length === 2;
frag.querySelector("strong").textContent === "Hi!";
// 等等。这里的 frag 是一个 DocumentFragment 实例,其内容是通过解析提供的字符串创建的。解析是使用 <template> 元素完成的,因此您可以在其中包含任何元素(包括具有奇怪解析规则的元素,如 <td>)。同样重要的是要注意,生成的 DocumentFragment 将没有关联的浏览上下文:也就是说,元素的 ownerDocument 将具有空的 defaultView 属性,资源将不会加载,等等。
所有对 fragment() 工厂的调用都会产生共享同一个模板所有者 Document 的 DocumentFragment。这允许对 fragment() 进行多次调用而没有额外的开销。但也意味着对 fragment() 的调用不能使用任何选项进行自定义。
请注意,DocumentFragment 的序列化不如完整的 JSDOM 对象容易。如果您需要序列化您的 DOM,您可能应该更直接地使用 JSDOM 构造函数。但是,对于包含单个元素的片段的特殊情况,通过正常方法很容易做到:
const frag = JSDOM.fragment(`<p>Hello</p>`);
console.log(frag.firstChild.outerHTML); // 记录 "<p>Hello</p>"其他值得注意的功能
Canvas 支持
jsdom 包含对使用 canvas 包以使用 canvas API 扩展任何 <canvas> 元素的支持。为了使其工作,您需要将 canvas 作为项目的依赖项包含在内,与 jsdom 处于同级。如果 jsdom 能够找到 3.x 版本的 canvas 包,它就会使用它,但如果不存在,那么 <canvas> 元素将表现得像 <div> 一样。
编码嗅探
除了提供字符串之外,JSDOM 构造函数还可以提供二进制数据,形式为标准的 JavaScript 二进制数据类型,如 ArrayBuffer、Uint8Array、DataView 等。完成此操作后,jsdom 将从提供的字节中嗅探编码,像浏览器一样扫描 <meta charset> 标签。
如果提供的 contentType 选项包含 charset 参数,则该编码将覆盖嗅探到的编码——除非存在 UTF-8 或 UTF-16 BOM,在这种情况下,BOM 优先。(同样,这就像浏览器一样。)
这种编码嗅探也适用于 JSDOM.fromFile() 和 JSDOM.fromURL()。在后一种情况下,随响应发送的任何 Content-Type 头都将优先,方式与构造函数的 contentType 选项相同。
请注意,在许多情况下,以这种方式提供字节可能比提供字符串更好。例如,如果您尝试使用 Node.js 的 buffer.toString("utf-8") API,Node.js 将不会剥离任何前导 BOM。如果您然后将此字符串提供给 jsdom,它将逐字解释它,保留 BOM 完好无损。但是 jsdom 的二进制数据解码代码将剥离前导 BOM,就像浏览器一样;在这种情况下,直接提供 buffer 将产生所需的结果。
关闭 jsdom
jsdom 中的定时器(通过 window.setTimeout() 或 window.setInterval() 设置)根据定义将在将来的窗口上下文中执行代码。由于在不保持进程存活的情况下无法在将来执行代码,因此未完成的 jsdom 定时器将保持您的 Node.js 进程存活。同样,由于在不保持对象存活的情况下无法在对象的上下文中执行代码,未完成的 jsdom 定时器将阻止它们被调度的窗口的垃圾回收。
如果您想确保关闭 jsdom 窗口,请使用 window.close(),这将终止所有正在运行的定时器(并删除窗口和文档上的任何事件监听器)。
使用 Chrome DevTools 调试 DOM
在 Node.js 中,您可以使用 Chrome DevTools 调试程序。有关如何开始,请参阅官方文档。
默认情况下,jsdom 元素在控制台中被格式化为普通的旧 JS 对象。为了使其更易于调试,您可以使用 jsdom-devtools-formatter,它允许您像检查真实的 DOM 元素一样检查它们。
注意事项
异步脚本加载
在使用 jsdom 时,人们经常在异步脚本加载方面遇到麻烦。许多页面异步加载脚本,但是无法判断它们何时完成此操作,因此也无法判断何时是运行代码并检查生成的 DOM 结构的好时机。这是一个根本性的限制;我们无法预测网页上的脚本将执行什么操作,因此无法告诉您它们何时完成加载更多脚本。
可以通过几种方式解决此问题。最好的方法(如果您控制相关页面)是使用脚本加载器提供的任何机制来检测加载何时完成。例如,如果您使用的是像 RequireJS 这样的模块加载器,代码可能如下所示:
// 在 Node.js 端:
const window = (new JSDOM(...)).window;
window.onModulesLoaded = () => {
console.log("准备就绪!");
};<!-- 在您提供给 jsdom 的 HTML 内部 -->
<script>
requirejs(["entry-module"], () => {
window.onModulesLoaded();
});
</script>如果您不控制该页面,您可以尝试一些解决方法,例如轮询特定元素的存在。
有关更多详细信息,请参阅 #640 中的讨论,特别是 @matthewkastor 的富有洞察力的评论。
Web 平台未实现的部分
虽然我们喜欢向 jsdom 添加新功能并使其保持与最新 Web 规范同步,但它缺少许多 API。请随时为缺少的任何内容提交问题,但我们是一个小而忙碌的团队,因此提交拉取请求可能更好。
jsdom 的某些功能由我们的依赖项提供。在这方面,值得注意的文档包括我们的 CSS 选择器引擎 nwsapi 的支持的 CSS 选择器列表。
除了我们尚未实现的功能之外,目前还有两个主要功能超出了 jsdom 的范围。它们是:
- 导航 (Navigation):在单击链接或分配
location.href或类似操作时,更改全局对象以及所有其他对象的能力。 - 布局 (Layout):计算由于 CSS 导致元素将被视觉布局在何处的能力,这会影响诸如
getBoundingClientRects()之类的方法或诸如offsetTop之类的属性。
目前,jsdom 对于这些功能的某些方面具有虚拟行为,例如针对导航向虚拟控制台发送“未实现” "jsdomError",或者为许多与布局相关的属性返回零。通常,您可以在代码中解决这些限制,例如,为爬取期间“导航”到的每个页面创建新的 JSDOM 实例,或者使用 Object.defineProperty() 更改各种与布局相关的 getter 和方法返回的内容。
请注意,同一领域的其他工具(例如 PhantomJS)确实支持这些功能。在 wiki 上,我们有一篇关于 jsdom vs. PhantomJS 的更完整的文章。
