jsdom
jsdom 是一个纯粹由 JavaScript 实现的一系列 Web 标准,特别是 WHATWG 组织制定的 DOM 和 HTML 标准,用于在 Node.js 中使用。大体上来说,该项目的目标是模拟足够的 Web 浏览器子集,以便用于测试和挖掘真实世界的 Web 应用程序。
最新版本的 jsdom 运行环境需要 Node.js v16 或者更高的版本。(jsdom v22 以下版本依旧可以在 Node.js 以前的版本使用,但是我们已经不支持维护了。)
基本用法
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(``, {
: "https://example.org/",
url: "https://example.com/",
referrer: "text/html",
contentType: true,
includeNodeLocations: 10000000
storageQuota
});
url
设置的值可以通过window.location
,document.URL
和document.documentURI
来返回,并会影响文档中相关 URL 的解析以及获取子资源时使用的同源限制和 referrer。默认值为"about:blank"
。referrer
仅仅影响document.referrer
的值。默认没有引用(即为空字符串)。contentType
影响document.contentType
的值,是按照 HTML 解析文档还是 XML 来解析。它的值如果不是 HTML MIME 类型 或 XML MIME 类型 值的话将会抛出异常。默认值为"text/html"
。如果存在charset
参数,它会影响二进制数据处理。includeNodeLocations
保留由 HTML 解析器生成的位置信息,允许您使用nodeLocation()
方法(如下所述)检索它。它还能确保在<script>
元素内运行的代码的异常堆栈跟踪中报告的行号是正确的。默认值为false
以提供最佳性能,并且不能与 XML 内容类型一起使用,因为我们的 XML 解析器不支持位置信息。storageQuota
是localStorage
和sessionStorage
使用的单独存储区域的代码单元的最大大小。尝试存储大于此限制的数据将导致抛出DOMException
。默认情况下,受 HTML 规范的启发,每个源设置为 5,000,000 个代码单元。
请注意,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>
<script>document.body.appendChild(document.createElement("hr"));</script>
</body>`);
// 脚本默认将不会执行:
dom.window.document.body.children.length === 1;
要在页面内启用脚本,可以使用 runScripts: "dangerously"
选项:
const dom = new JSDOM(`<body>
<script>document.body.appendChild(document.createElement("hr"));</script>
</body>`, { runScripts: "dangerously" });
// 脚本将执行并修改 DOM:
dom.window.document.body.children.length === 2;
我们再次强调只有在提供给 jsdom 的代码是你已知道是安全的才可使用它。如果您运行了任意用户提供的或 Internet 上的不可信的 Node.js 代码,可能会危及您的计算机。
假如你想通过 <script src="">
来执行外部脚本,你需要确保已经加载了它们。为此,请添加选项 resources: "usable"
如下所述。(出于此处讨论的原因,您可能还需要设置 url
选项。)
除非 runScripts
设置为 "dangerously"
,否则事件处理属性(如<div onclick="">
)也将受此选项控制不起作用。(但是,事件处理函数属性,比如 div.onclick = ...
,将忽略 runScripts
参数并且会起作用。)
如果您只是试图从“外部”执行脚本,而不是通过 <script>
元素和事件处理属性从“内部”运行,则可以使用 runScripts: "outside-only"
选项,这使得所有 JavaScript 规范提供的全局变量的新副本都可以安装在 window
上。 这包括诸如 window.Array
、window.Promise
等等。值得注意的是,它还包括 window.eval
可以用来运行脚本,运行时将 jsdom 的 window
作为全局:
const { window } = new JSDOM(``, { runScripts: "outside-only" });
window.eval(`document.body.innerHTML = "<p>Hello, world!</p>";`);
window.document.body.children.length === 1;
由于性能原因,默认情况下会关闭此功能,但可以安全启用。
(注意,默认配置下,不设置 runScripts
,window.Array
、window.eval
等的值会与外部 Node.js 环境提供的值相同。即 window.eval === eval
会成立,所以 window.eval
不会以有用的方式运行脚本。)
我们强烈建议不要试图通过将 jsdom 和 Node 全局环境混合在一起(例如,通过执行 global.window = dom.window
)来“执行脚本”,然后在 Node 全局环境中执行脚本或测试代码。相反,您应该像对待浏览器一样对待 jsdom,并使用 window.eval
或 runScripts: "dangerously"
来运行需要访问 jsdom 环境内的 DOM 的所有脚本和测试。例如,这可能需要创建一个 browserify 包作为 <script>
元素执行 - 就像在浏览器中一样。
最后,对于高级用例,您可以使用 dom.getInternalVMContext()
方法,如下所述。
假装成一个视觉浏览器
jsdom 没有渲染可视内容的能力,并且默认情况下会像无头浏览器一样工作。它通过 API(如 document.hidden
)向网页提供提示,表明其内容不可见。
当 pretendToBeVisual
选项设置为 true 时,jsdom 会假装它正在呈现并显示内容。它是这样做的:
- 更改
document.hidden
以返回false
而不是true
- 更改
document.visibilityState
以返回"visible"
而不是"prerender"
- 启用
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>
加载 frame 和 iframe - 通过
<link rel="stylesheet">
加载样式 - 通过
<script>
加载脚本,但是前提是runScripts: "dangerously"
设置了 - 通过
<img>
加载图片,但是前提是canvas
npm 包已安装(详见下面的支持 Canvas)
尝试加载资源时,请记住 url
选项的默认值是 "about:blank"
,这意味着通过相对 URL 包含的任何资源都将无法加载。(针对 URL about:blank
解析 URL /something
的结果是一个错误。)因此,在这些情况下,您可能希望为 url
选项设置一个非默认值,或使用自动执行此操作的便捷的 API 之一。
高级配置
要更全面地自定义 jsdom 的资源加载行为,可以将 ResourceLoader
类的实例作为 resources
选项值传递:
const resourceLoader = new jsdom.ResourceLoader({
: "http://127.0.0.1:9001",
proxy: false,
strictSSL: "Mellblomenator/9000",
userAgent
});const dom = new JSDOM(``, { resources: resourceLoader });
ResourceLoader
构造函数的三个选项是:
proxy
是要使用的 HTTP 代理的地址。strictSSL
可以设置为 false 以禁用 SSL 证书有效的要求。userAgent
影响发送的User-Agent
标头,从而影响navigator.userAgent
的结果值。 它默认为`Mozilla/5.0 (${process.platform || "unknown OS"}) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/${jsdomVersion}`
。
您可以通过继承 ResourceLoader
并覆盖 fetch()
方法来进一步自定义资源获取。例如,覆盖指定 URL 返回的内容:
class CustomResourceLoader extends jsdom.ResourceLoader {
fetch(url, options) {
// 重写此脚本的内容以执行一些不寻常的操作。
if (url === "https://example.com/some-specific-script.js") {
return Promise.resolve(Buffer.from("window.someGlobal = 5;"));
}
return super.fetch(url, options);
}
}
jsdom 在遇到之前说到的“可用”资源时会调用自定义资源加载器的 fetch()
方法。该方法接受一个 URL 字符串,以及一些在调用 super.fetch()
时应透传的选项。它必须返回一个 Node.js 的 Buffer
对象的 promise,或者如果有意不加载资源则返回 null
。一般来说,大多数情况下都希望像上面代码一样委托给 super.fetch()
。
可以在 fetch()
中收到的选项之一是获取资源的元素(如果适用)。
class CustomResourceLoader extends jsdom.ResourceLoader {
fetch(url, options) {
if (options.element) {
console.log(`Element ${options.element.localName} is requesting the url ${url}`);
}
return super.fetch(url, options);
}
}
虚拟控制台
像网页浏览器一样,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 输出的。到目前为止,错误会按照下面的方式输出:
- 加载或解析子资源时出错(脚本,样式表,frames 和 iframe)
- 不是由 window
onerror
事件处理程序处理的脚本执行错误,它将会返回true
或调用event.preventDefault()
- 由于调用 jsdom 没有实现的方法而导致的错误,例如
window.alert
,兼容性的 Web 浏览器都实现了这些方法
如果您使用 sendTo(c)
将错误发送给 c
,则默认情况下,它将使用来自"jsdomError"
事件的信息调用 c.error(errorStack[, errorDetail])
。如果您希望保持事件与方法调用的严格的一对一映射,并且可能自己处理"jsdomError"
,那么您可以执行
virtualConsole.sendTo(c, { omitJSDOMErrors: true });
Cookie 容器
像网页浏览器一样,jsdom 也具有 cookie 容器的概念,存储 HTTP cookie。在文档的同一个域上一个 URL,并且没有标记为 HTTP-only 的 cookies,可以通过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 jar 由 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 添加 shim,这将特别有用。
JSDOM
object API
一旦你构建了一个 JSDOM
对象,它将具有以下有用的功能:
Properties
window
属性: window
对象的 key 从 Window
对象检索而来。
virtualConsole
和 cookieJar
:可以传入或者使用默认值。
serialize()
序列化 document
通过 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>`,
: true }
{ includeNodeLocations
);
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
选项才能使用此功能;由于性能原因这个选项默认为关闭。
getInternalVMContext()
与 Node.js vm
模块交互
使用 Node.js 的内置 vm
模块是 jsdom 脚本运行魔法的基础。一些高级用例,例如预编译脚本然后多次运行,可以通过直接使用 vm
模块和 jsdom 创建的 Window
实现。
可以使用 getInternalVMContext()
方法获取与 vm
API 一起使用的上下文化的全局对象:
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")
)。
请注意,如果在没有设置 runScripts
的情况下创建了 JSDOM
实例,或者如果您 在 Web 浏览器中使用 jsdom,此方法将抛出异常。
reconfigure(settings)
重新配置 jsdom
通过 window.top
属性在规范中被标记为 [Unforgeable]
,这意味着它是一个不可配置的私有属性,因此在 jsdom 内运行的普通代码是不能覆盖或隐藏它,即使使用 Object.defineProperty
。
同样,目前在 jsdom 中是不能够处理导航相关信息的(比如设置 window.location.href = "https://example.com/"
);这样做会导致在虚拟控制台报 "jsdomError"
错误,说明此功能未实现,并且没有任何变化:也将不会有新的 Window
或 Document
对象,并且现有 window
的 location
对象仍保持当前所有相同的属性值。
但是,如果您从 jsdom 窗口之外进行操作,例如在一些创建 jsdom 的测试框架中,可以使用特殊的 reconfigure()
方法覆盖其中的一个或两个:
const dom = new JSDOM();
dom.window.top === dom.window;
dom.window.location.href === "about:blank";
dom.reconfigure({
: myFakeTopForTesting,
windowTop: "https://example.com/",
url
});
dom.window.top === myFakeTopForTesting;
dom.window.location.href === "https://example.com/";
请注意,更改 jsdom 的 URL 将影响所有返回当前 document 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 有效且请求成功,则 onFullfilled
回调执行并返回 JSDOM
实例。任何 URL 重定向都将遵循其最终目的地。
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());
});
如果可以打开给定的文件,则 onFullfilled
回调执行并返回 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!";
// etc.
frag
是 DocumentFragment
的实例对象,其内容是通过提供的字符串解析创建的。解析是通过使用 <template>
元素完成的,因此您可以在其中包含任何元素(包括具有奇怪解析规则的元素,如 <td>
)。还需要注意的是,生成的 DocumentFragment
不会有关联的浏览上下文:即元素的 ownerDocument
将有一个空的 defaultView
属性,资源不会加载,等等。
fragment()
工厂函数的所有调用结果的 DocumentFragments
实例都会共享相同的 Document
。这允许多次调用 fragment()
而没有额外的开销。但这也意味着对 fragment()
的调用不能用任何选项自定义。
请注意,对 DocumentFragments
的序列化并不像使用 JSDOM
对象那样容易。如果你需要序列化你的 DOM ,你应该直接使用 JSDOM
构造函数。但对于包含单个元素的片段的特殊情况,通过常规方法就很容易做到。
const frag = JSDOM.fragment(`<p>Hello</p>`);
console.log(frag.firstChild.outerHTML); // logs "<p>Hello</p>"
其他值得注意的功能
支持 Canvas
jsdom 支持使用 canvas
包来扩展任何使用 canvas API 的 <canvas>
元素。为了做到这一点,您需要将 canvas
作为依赖项加入到您的项目中,和 jsdom
包并列。如果 jsdom 可以找到 canvas
包,它将使用它,但是如果它不存在,那么 <canvas>
元素的行为就像 <div>
一样。从 jsdom v13 开始,需要 canvas
的 2.x 版本;不再支持 1.x 版。
编码嗅探
除了提供一个字符串外,JSDOM
构造函数还支持 Node.js Buffer
或标准 JavaScript 二进制数据类型(如 ArrayBuffer
,Uint8Array
,DataView
等)的形式提供二进制数据。当完成后,jsdom 将就像浏览器一样从提供的字节进行嗅探编码,扫描 <meta charset>
标签。
如果提供的 contentType
选项包含 charset
参数,则该编码将覆盖嗅探的编码 - 除非存在 UTF-8 或 UTF-16 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()
设置)将在 window 上下文中执行代码。由于进程在不活跃的情况下无法执行未来的定时器代码,所以卓越的 jsdom 定时器将保持您的 Node.js 进程处于活动状态。同样,对象不活跃的情况下也没有办法在对象的上下文中执行代码,卓越的 jsdom 定时器将阻止垃圾回收调度它们的 window。
如果你想确保关闭 jsdom 窗口,使用 window.close()
,它将终止所有正在运行的定时器(并且还会删除 window
和 document
上的任何事件监听器)。
使用 Chrome 开发者工具调试 DOM
可以使用 Chrome 开发者工具来调试 Node.js 程序。请参阅官方文档了解如何使用。
默认情况下,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!");
};
<!-- Inside the HTML you supply to jsdom -->
script>
<requirejs(["entry-module"], () => {
window.onModulesLoaded();
});script>
</
如果您不能控制该页面,则可以尝试其他解决方法,例如轮询检查特定元素是否存在。
有关更多详细信息,请查看#640中的讨论,尤其是 @matthewkastor 的深刻见解。
未实现的 Web 平台部分
目前 jsdom 中有很多缺失的 API,尽管我们也想要在 jsdom 中添加新的功能并保持最新的 Web 规范。请随时为缺失的任何内容提交 issue,但我们是一个很小并且忙碌的团队,因此大家一起来提交 pull request 可能会更好。
jsdom 的一些特性是由我们的依赖项提供的。这方面值得注意的文档包括我们的 CSS 选择器引擎 nwsapi
。
除了我们尚未拥有的功能之外,还有两个主要功能目前超出了 jsdom 的范围。这些是:
- 导航:在点击链接或赋值
location.href
或类似操作时可以更改全局对象和所有其他的对象。 - 布局:计算 CSS 元素的视觉布局的能力,这会影响诸如
getBoundingClientRects()
或者诸如offsetTop
之类的属性。
目前,jsdom 对某些功能的某些方面具有虚拟行为,例如操作导航时向虚拟控制台发送“未实现的”"jsdomError"
,或者为许多与布局相关的属性返回 0。您通常可以在代码中解决这些限制,例如通过在爬网过程中为每个页面创建新的 JSDOM
实例,或使用 Object.defineProperty()
更改各种与布局相关的 getter 和方法的返回值
请注意,相同领域中的其他工具(如 PhantomJS)确实支持这些功能。在 wiki 上,我们有关于 jsdom 与 PhantomJS 比较的更完整的介绍。
支持 jsdom
jsdom 是一个社区驱动的项目,由志愿者团队维护。您可以通过以下方式支持 jsdom:
- 在 Tidelift 订阅中获得对 jsdom 的专业支持。Tidelift 帮助我们实现开源的可持续发展,同时为团队提供维护、许可和安全方面的保证。
- 直接对项目做贡献。
获取帮助
如果您需要 jsdom 的帮助,请随时使用以下任何方式:
- 邮件组(最好以 “how do I” 的形式提问)
- 提 issue(最好用来提 bug)
- Matrix 房间:#jsdom:matrix.org
原文地址:jsdom/jsdom: A JavaScript implementation of various web standards, for use with Node.js
此翻译由 zjffun/translated-document 提供,在 GitHub 上完善此翻译。