npm package: @lwjjike/xbsdom
Description: A JavaScript implementation of many web standards
Latest version: 28.1.2


<h1 align="center">
    <img width="100" height="100" src="logo.svg" alt=""><br>
    xbsdom
</h1>

## 使用
```js
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配置说明:
```javascript
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](https://dom.spec.whatwg.org/) 和 [HTML](https://html.spec.whatwg.org/multipage/) 标准，并可与 Node.js 配合使用。该项目的目标是模拟 Web 浏览器的子集，使其能够用于测试和抓取真实 Web 应用程序。

最新版本的 jsdom 需要较新版本的 Node.js；详情请参阅 `package.json` 文件中的 `engines` 字段。

## 基本用法

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

要使用 jsdom，您主要会用到 `JSDOM` 构造函数，它是 jsdom 主模块的一个命名导出。将一个字符串传递给构造函数。您将获得一个 `JSDOM` 对象，该对象具有许多有用的属性，特别是 `window` 属性：

```js
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 无法实现的操作。对于不需要这些功能的简单场景，我们建议使用类似这样的编码模式：

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

有关 `JSDOM` 类的所有功能的完整文档请参见下文“`JSDOM` 对象 API”部分。

## 自定义 jsdom

`JSDOM` 构造函数接受第二个参数，该参数可用于通过以下方式自定义 jsdom。

### 简单的选项

```js
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 标准](https://url.spec.whatwg.org/) 进行解析和序列化。）

### 执行脚本

jsdom 最强大的功能在于它可以在 jsdom 内部执行脚本。这些脚本可以修改页面内容，并访问 jsdom 实现的所有 Web 平台 API。

然而，当处理不受信任的内容时，这也极其危险。jsdom 沙箱并非万无一失，运行在 DOM 的 `<script>` 标签内的代码，如果尝试足够多，就有可能获得对 Node.js 环境的访问权限，进而访问你的计算机。因此，默认情况下，执行嵌入在 HTML 中的脚本的功能是被禁用的。

```js
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"` 选项：

```js
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 环境本身已被攻破的情况（例如，通过原型污染）。更多详情请参阅[安全策略](https://github.com/jsdom/.github/blob/master/SECURITY.md)。

如果您只是想“从外部”执行脚本，而不是让 `<script>` 元素和事件处理程序属性“从内部”运行，则可以使用 `runScripts: "outside-only"` 选项，该选项允许在 `window` 上安装所有 JavaScript 规范提供的全局变量的全新副本。这包括 `window.Array`、`window.Promise` 等。值得注意的是，它还包括 `window.eval`，该方法允许运行脚本，但全局变量是 jsdom 的 `window`。

```js
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()` 方法

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

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

请注意，jsdom 仍然[不进行任何布局或渲染](#unimplemented-parts-of-the-web-platform)，所以这实际上只是_假装_具有视觉效果，而不是实现真正的、可视化的 Web 浏览器会实现的平台部分。

### 正在加载子资源

#### 基本选项

默认情况下，jsdom 不会加载任何子资源，例如脚本、样式表、图像或 iframe。如果您希望 jsdom 加载这些资源，可以传递 `resources: "usable"` 选项，该选项将加载所有可用的资源。这些资源包括：

- 通过 `<frame>` 和 `<iframe>` 加载的框架和 iframe

- 通过 `<link rel="stylesheet">` 加载的样式表

- 通过 `<script>` 加载的脚本，但前提是同时设置了 `runScripts: "dangerously"`

- 通过 `<img>` 加载的图像，但前提是同时安装了 `canvas` npm 包（请参阅下文的 [Canvas 支持](#canvas-support)）。

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

#### 高级配置

为了更全面地自定义 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` 可以设置为自定义的 [undici](https://undici.nodejs.org/#/docs/api/Dispatcher) [`Dispatcher`](https://undici.nodejs.org/#/docs/api/Dispatcher)，用于配置代理或自定义 TLS 设置等高级用例。例如，要使用代理，可以使用 undici 的 `ProxyAgent`。

- `interceptors` 可以设置为 [undici](https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-interceptor) [拦截器函数](https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-interceptor) 数组。拦截器可用于修改请求或响应，而无需编写全新的 `Dispatcher`。

对于检查传入请求或返回合成响应的简单情况，您可以使用 jsdom 的 `requestInterceptor()` 辅助函数，它接收一个 [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) 对象和上下文，并可以返回一个 [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response):

```js
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`）。例如：

```js
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` 的全局调度器](https://undici.nodejs.org/#/?id=undicigetglobaldispatcher)）。如果您使用 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，您可以通过以下方式覆盖此默认设置：

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

这样的代码将创建一个没有任何行为的虚拟控制台。您可以通过为所有可能的控制台方法添加事件监听器来赋予它行为：

```js
virtualConsole.on("error", () => { ... });
virtualConsole.on("warn", () => { ... });
virtualConsole.on("info", () => { ... });
virtualConsole.on("dir", () => { ... });
// ... 等等。请参阅 https://console.spec.whatwg.org/#logging
```

（请注意，最好在调用 `new JSDOM()` _之前_设置这些事件监听器，因为错误或调用控制台的脚本可能会在解析期间发生。）

如果您只是想将虚拟控制台的输出重定向到另一个控制台（如默认的 Node.js 控制台），您可以这样做：

```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()` 将产生相同的行为。如果您想要非默认行为，您可以通过以下方式进行自定义：

```js
// 不要将任何 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`](https://github.com/acemir/CSSOM) 的异常对象
  - `sheetText`: 我们试图解析的样式表的全文
- `"not-implemented"`: 当调用[未实现的 Web 平台部分](#unimplemented-parts-of-the-web-platform)中的某些存根方法时发出的错误
- `"resource-loading"`: [加载资源](#loading-subresources)时出错，例如由于网络错误或来自服务器的错误响应代码
  - `cause` 属性：来自 jsdom 在检索资源时进行的内部 Node.js 网络调用的异常对象，或来自开发者的自定义资源加载器的异常对象
  - `url` 属性：尝试获取的资源的 URL
- `"unhandled-exception"`: 未被 `Window` `"error"` 事件监听器处理的[脚本执行](#executing-scripts)错误
  - `cause` 属性：包含原始异常对象

### Cookie 罐 (Cookie jars)

像 Web 浏览器一样，jsdom 有 cookie 罐的概念，用于存储 HTTP cookie。与文档同域且未标记为 HTTP-only 的 Cookie 可以通过 `document.cookie` API 访问。此外，cookie 罐中的所有 cookie 都会影响子资源的获取。

默认情况下，`JSDOM` 构造函数将返回一个包含空 cookie 罐的实例。要创建自己的 cookie 罐并将其传递给 jsdom，您可以通过以下方式覆盖此默认设置：

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

这主要在您想在多个 jsdom 之间共享同一个 cookie 罐，或者提前用某些值填充 cookie 罐时非常有用。

Cookie 罐由 [tough-cookie](https://www.npmjs.com/package/tough-cookie) 包提供。`jsdom.CookieJar` 构造函数是 tough-cookie 的 cookie 罐的一个子类，默认设置了 `looseMode: true` 选项，因为这[更好地匹配了浏览器的行为](https://github.com/whatwg/html/issues/804)。如果您想自己使用 tough-cookie 的实用程序和类，可以使用 `jsdom.toughCookie` 模块导出以访问与 jsdom 打包的 tough-cookie 模块实例。

### 解析前干预

jsdom 允许您在创建 jsdom 的非常早期阶段进行干预：在 `Window` 和 `Document` 对象创建之后，但在解析任何 HTML 以用节点填充文档之前：

```js
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 序列化](https://html.spec.whatwg.org/#html-fragment-serialisation-algorithm)，包括 doctype：

```js
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 位置信息](https://www.npmjs.com/package/parse5#options-locationinfo)：

```js
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`](https://nodejs.org/api/vm.html) 模块是 jsdom 脚本运行魔法的基础。某些高级用例，如预编译脚本然后多次运行它，会从直接将 `vm` 模块与 jsdom 创建的 `Window` 结合使用中受益。

要获得适用于 `vm` API 的[上下文全局对象](https://nodejs.org/api/vm.html#vm_what_does_it_mean_to_contextify_an_object)，您可以使用 `getInternalVMContext()` 方法：

```js
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()` 方法覆盖其中一个或两个：

```js
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：

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

如果 URL 有效且请求成功，则返回的 promise 将以一个 `JSDOM` 实例解决。任何重定向都将被跟踪到它们的最终目的地。

提供给 `fromURL()` 的选项与提供给 `JSDOM` 构造函数的选项类似，但有以下额外的限制和后果：

- 不能提供 `url` 和 `contentType` 选项。
- `referrer` 选项用作初始请求的 HTTP `Referer` 请求头。
- `resources` 选项也影响初始请求；这在您想要例如配置代理时很有用（见上文）。
- 生成的 jsdom 的 URL、内容类型和引用页由响应确定。
- 任何通过 HTTP `Set-Cookie` 响应头设置的 cookie 都存储在 jsdom 的 cookie 罐中。同样，提供的 cookie 罐中已有的任何 cookie 都会作为 HTTP `Cookie` 请求头发送。

### `fromFile()`

类似于 `fromURL()`，jsdom 还提供了一个 `fromFile()` 工厂方法，用于从文件名构造 jsdom：

```js
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`：

```js
const frag = JSDOM.fragment(`<p>Hello</p><p><strong>Hi!</strong>`);

frag.childNodes.length === 2;
frag.querySelector("strong").textContent === "Hi!";
// 等等。
```

这里的 `frag` 是一个 [`DocumentFragment`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) 实例，其内容是通过解析提供的字符串创建的。解析是使用 `<template>` 元素完成的，因此您可以在其中包含任何元素（包括具有奇怪解析规则的元素，如 `<td>`）。同样重要的是要注意，生成的 `DocumentFragment` 将没有[关联的浏览上下文](https://html.spec.whatwg.org/multipage/#concept-document-bc)：也就是说，元素的 `ownerDocument` 将具有空的 `defaultView` 属性，资源将不会加载，等等。

所有对 `fragment()` 工厂的调用都会产生共享同一个模板所有者 `Document` 的 `DocumentFragment`。这允许对 `fragment()` 进行多次调用而没有额外的开销。但也意味着对 `fragment()` 的调用不能使用任何选项进行自定义。

请注意，`DocumentFragment` 的序列化不如完整的 `JSDOM` 对象容易。如果您需要序列化您的 DOM，您可能应该更直接地使用 `JSDOM` 构造函数。但是，对于包含单个元素的片段的特殊情况，通过正常方法很容易做到：

```js
const frag = JSDOM.fragment(`<p>Hello</p>`);
console.log(frag.firstChild.outerHTML); // 记录 "<p>Hello</p>"
```

## 其他值得注意的功能

### Canvas 支持

jsdom 包含对使用 [`canvas`](https://www.npmjs.com/package/canvas) 包以使用 canvas API 扩展任何 `<canvas>` 元素的支持。为了使其工作，您需要将 `canvas` 作为项目的依赖项包含在内，与 `jsdom` 处于同级。如果 jsdom 能够找到 3.x 版本的 `canvas` 包，它就会使用它，但如果不存在，那么 `<canvas>` 元素将表现得像 `<div>` 一样。

### 编码嗅探

除了提供字符串之外，`JSDOM` 构造函数还可以提供二进制数据，形式为标准的 JavaScript 二进制数据类型，如 `ArrayBuffer`、`Uint8Array`、`DataView` 等。完成此操作后，jsdom 将从提供的字节中[嗅探编码](https://html.spec.whatwg.org/multipage/syntax.html#encoding-sniffing-algorithm)，像浏览器一样扫描 `<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 调试程序。有关如何开始，请参阅[官方文档](https://nodejs.org/en/docs/inspector/)。

默认情况下，jsdom 元素在控制台中被格式化为普通的旧 JS 对象。为了使其更易于调试，您可以使用 [jsdom-devtools-formatter](https://github.com/jsdom/jsdom-devtools-formatter)，它允许您像检查真实的 DOM 元素一样检查它们。

## 注意事项

### 异步脚本加载

在使用 jsdom 时，人们经常在异步脚本加载方面遇到麻烦。许多页面异步加载脚本，但是无法判断它们何时完成此操作，因此也无法判断何时是运行代码并检查生成的 DOM 结构的好时机。这是一个根本性的限制；我们无法预测网页上的脚本将执行什么操作，因此无法告诉您它们何时完成加载更多脚本。

可以通过几种方式解决此问题。最好的方法（如果您控制相关页面）是使用脚本加载器提供的任何机制来检测加载何时完成。例如，如果您使用的是像 RequireJS 这样的模块加载器，代码可能如下所示：

```js
// 在 Node.js 端：
const window = (new JSDOM(...)).window;
window.onModulesLoaded = () => {
  console.log("准备就绪！");
};
```

```html
<!-- 在您提供给 jsdom 的 HTML 内部 -->
<script>
requirejs(["entry-module"], () => {
  window.onModulesLoaded();
});
</script>
```

如果您不控制该页面，您可以尝试一些解决方法，例如轮询特定元素的存在。

有关更多详细信息，请参阅 [#640](https://github.com/jsdom/jsdom/issues/640) 中的讨论，特别是 [@matthewkastor](https://github.com/matthewkastor) 的[富有洞察力的评论](https://github.com/jsdom/jsdom/issues/640#issuecomment-22216965)。

### Web 平台未实现的部分

虽然我们喜欢向 jsdom 添加新功能并使其保持与最新 Web 规范同步，但它缺少许多 API。请随时为缺少的任何内容提交问题，但我们是一个小而忙碌的团队，因此提交拉取请求可能更好。

jsdom 的某些功能由我们的依赖项提供。在这方面，值得注意的文档包括我们的 CSS 选择器引擎 [`nwsapi`](https://github.com/dperini/nwsapi) 的[支持的 CSS 选择器](https://github.com/dperini/nwsapi/wiki/CSS-supported-selectors)列表。

除了我们尚未实现的功能之外，目前还有两个主要功能超出了 jsdom 的范围。它们是：

- **导航 (Navigation)**：在单击链接或分配 `location.href` 或类似操作时，更改全局对象以及所有其他对象的能力。
- **布局 (Layout)**：计算由于 CSS 导致元素将被视觉布局在何处的能力，这会影响诸如 `getBoundingClientRects()` 之类的方法或诸如 `offsetTop` 之类的属性。

目前，jsdom 对于这些功能的某些方面具有虚拟行为，例如针对导航向虚拟控制台发送“未实现” `"jsdomError"`，或者为许多与布局相关的属性返回零。通常，您可以在代码中解决这些限制，例如，为爬取期间“导航”到的每个页面创建新的 `JSDOM` 实例，或者使用 `Object.defineProperty()` 更改各种与布局相关的 getter 和方法返回的内容。

请注意，同一领域的其他工具（例如 PhantomJS）确实支持这些功能。在 wiki 上，我们有一篇关于 [jsdom vs. PhantomJS](https://github.com/jsdom/jsdom/wiki/jsdom-vs.-PhantomJS) 的更完整的文章。