Skip to content
Jufeng Zhang edited this page May 13, 2023 · 7 revisions


jsdom

jsdom 是一个纯粹由 JavaScript 实现的一系列 Web 标准,特别是 WHATWG 组织制定的 DOMHTML 标准,用于在 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(``, {
  url: "https://example.org/",
  referrer: "https://example.com/",
  contentType: "text/html",
  includeNodeLocations: true,
  storageQuota: 10000000
});
  • url 设置的值可以通过 window.locationdocument.URLdocument.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 解析器不支持位置信息。
  • storageQuotalocalStoragesessionStorage 使用的单独存储区域的代码单元的最大大小。尝试存储大于此限制的数据将导致抛出 DOMException。默认情况下,受 HTML 规范的启发,每个源设置为 5,000,000 个代码单元。

请注意,urlreferrer 在使用之前已经被规范化了,例如:如果你传入 "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.Arraywindow.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;

由于性能原因,默认情况下会关闭此功能,但可以安全启用。

(注意,默认配置下,不设置 runScriptswindow.Arraywindow.eval等的值会与外部 Node.js 环境提供的值相同。即 window.eval === eval 会成立,所以 window.eval 不会以有用的方式运行脚本。)

我们强烈建议不要试图通过将 jsdom 和 Node 全局环境混合在一起(例如,通过执行 global.window = dom.window)来“执行脚本”,然后在 Node 全局环境中执行脚本或测试代码。相反,您应该像对待浏览器一样对待 jsdom,并使用 window.evalrunScripts: "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({
  proxy: "http://127.0.0.1:9001",
  strictSSL: false,
  userAgent: "Mellblomenator/9000",
});
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 对象检索而来。

virtualConsolecookieJar:可以传入或者使用默认值。

通过 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>`,
  { 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 选项才能使用此功能;由于性能原因这个选项默认为关闭。

使用 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" 错误,说明此功能未实现,并且没有任何变化:也将不会有新的 WindowDocument 对象,并且现有 windowlocation 对象仍保持当前所有相同的属性值。

但是,如果您从 jsdom 窗口之外进行操作,例如在一些创建 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 将影响所有返回当前 document URL 的 API,例如 window.locationdocument.URLdocument.documentURI ,以及文档中相对 URL 的解析以及同源检查和提取子资源时使用的来源。但是,它不会执行导航到该 URL 的内容;DOM 的内容将保持不变,并且不会创建 WindowDocument等新的实例。

便捷的 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 构造函数的选项类似,但具有以下额外的限制和后果:

  • urlcontentType 参数不能被提供。
  • referrer 选项用作初始请求的 HTTP Referer 请求头。
  • resources 选项也会影响初始请求;这很有用,例如,如果您想配置代理(见上文)。
  • 生成的 jsdom 的 URL、内容类型和来源是由响应来决定。
  • 任何通过 HTTP Set-Cookie 响应头设置的 cookie 都存储在 jsdom 的 cookie 容器中。同样,已提供的 cookie 容器中的任何 cookie 都会作为 HTTP Cookie 请求头发送。

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 实例及其所有相关的功能。您甚至可能不需要 WindowDocument!相反,你只需要解析一些 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.

fragDocumentFragment 的实例对象,其内容是通过提供的字符串解析创建的。解析是通过使用 <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 二进制数据类型(如 ArrayBufferUint8ArrayDataView 等)的形式提供二进制数据。当完成后,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(),它将终止所有正在运行的定时器(并且还会删除 windowdocument 上的任何事件监听器)。

使用 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 的帮助,请随时使用以下任何方式: