npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lwjjike/xbsdom

v28.1.2

Published

A JavaScript implementation of many web standards

Readme

使用

npm i @lwjjike/xbsdom

xbsdom通过魔改jsdom,除去jsdom中的大量特征,模拟真实浏览器的值

v28.1.2更新说明:

  1. 完美添加了navigator.plugins和navigator.mimeTypes
  2. 修复了navigator.userAgent和navigator.deviceMemory的问题
  3. JSDOM构造函数第二哥参数中增加了xbsConfig配置项,可以自由配置userAgent和deviceMemory的值
  4. 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 的 DOMHTML 标准,并可与 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.locationdocument.URLdocument.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 解析器不支持位置信息。

  • storageQuotalocalStoragesessionStorage 使用的独立存储区域的最大代码单元大小。尝试存储超过此限制的数据将导致抛出 DOMException 异常。默认情况下,它设置为每个源 5,000,000 个代码单元,这是受 HTML 规范的启发。

请注意,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>
  <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.Arraywindow.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

出于性能考虑,此功能默认关闭,但启用它是安全的。

请注意,在默认配置下,如果不设置 runScriptswindow.Arraywindow.eval 等的值将与外部 Node.js 环境提供的值相同。也就是说,window.eval === eval 将成立,因此 window.eval 将无法有效地运行脚本。

我们强烈建议不要尝试通过将 jsdom 和 Node 全局环境混在一起(例如,使用 global.window = dom.window)来“执行脚本”,然后在 Node 全局环境中执行脚本或测试代码。相反,您应该像对待浏览器一样对待 jsdom,并在 jsdom 环境中运行所有需要访问 DOM 的脚本和测试,使用 window.evalrunScripts: "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> 加载的图像,但前提是同时安装了 canvas npm 包(请参阅下文的 Canvas 支持)。

尝试加载资源时,请记住 url 选项的默认值为 "about:blank",这意味着任何通过相对 URL 包含的资源都将无法加载。 (尝试将 URL /something 与 URL about:blank 进行解析会出错。)因此,在这些情况下,您可能需要为 url 选项设置一个非默认值,或者使用可以自动执行此操作的便捷 API之一。

高级配置

为了更全面地自定义 jsdom 的资源加载行为,包括通过 JSDOM.fromURL() 发起的初始加载,以及通过 dom.window.XMLHttpRequestdom.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 可以设置为自定义的 undici Dispatcher,用于配置代理或自定义 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 的非常早期阶段进行干预:在 WindowDocument 对象创建之后,但在解析任何 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 对象。

virtualConsolecookieJar 属性反映了您传入的选项,或者如果没有为这些选项传入任何内容,则反映为您创建的默认值。

使用 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",解释该功能未实现,并且没有任何变化:不会有新的 WindowDocument 对象,现有的 windowlocation 对象仍然具有所有相同的属性值。

但是,如果您从窗口外部执行操作,例如在创建 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.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 有效且请求成功,则返回的 promise 将以一个 JSDOM 实例解决。任何重定向都将被跟踪到它们的最终目的地。

提供给 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());
});

如果给定的文件可以被打开,返回的 promise 将以一个 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!";
// 等等。

这里的 frag 是一个 DocumentFragment 实例,其内容是通过解析提供的字符串创建的。解析是使用 <template> 元素完成的,因此您可以在其中包含任何元素(包括具有奇怪解析规则的元素,如 <td>)。同样重要的是要注意,生成的 DocumentFragment 将没有关联的浏览上下文:也就是说,元素的 ownerDocument 将具有空的 defaultView 属性,资源将不会加载,等等。

所有对 fragment() 工厂的调用都会产生共享同一个模板所有者 DocumentDocumentFragment。这允许对 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 二进制数据类型,如 ArrayBufferUint8ArrayDataView 等。完成此操作后,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 的更完整的文章。