> ## Documentation Index
> Fetch the complete documentation index at: https://bun.zhcndoc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# WebView

> 通过 Bun 控制无头浏览器，用于自动化、测试和抓取 — 在 macOS 上零依赖；其余平台到处都是 Chrome DevTools Protocol

`Bun.WebView` 是内置在运行时中的无头浏览器。使用它来加载页面、在其中运行 JavaScript、模拟真实用户输入，并截取屏幕截图 — 不需要 Puppeteer、Playwright，也不需要单独下载浏览器。

<Warning>此 API 为实验性质，可能会在后续版本中发生变化。</Warning>

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await using view = new Bun.WebView();

await view.navigate("https://example.com");
await view.click("a[href]"); // 等待链接变为可点击
const title = await view.evaluate("document.title");

await Bun.write("page.png", await view.screenshot());
```

在 macOS 上，`Bun.WebView` 使用系统的 `WKWebView` — 无需安装任何东西。在 Linux 和 Windows 上，它通过 Chrome DevTools Protocol 驱动已安装的 Chrome、Chromium、Edge 或 Brave。

每个视图都会在单独的渲染器进程中运行其页面。所有输入方法（`click`、`type`、`press`、`scroll`）都会分发**原生**浏览器事件，因此页面会看到 `isTrusted: true` — 与真实用户一致。

***

## 创建视图

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const view = new Bun.WebView({
  width: 1280, // 视口宽度（CSS 像素，1-16384，默认 800）
  height: 720, // 视口高度（CSS 像素，1-16384，默认 600）
  url: "https://bun.com", // 可选：立即开始导航
});
```

构造函数是同步的 — 它会立即返回，并在后台启动浏览器子进程。你第一次 `await` 的操作（`navigate()`、`evaluate()` 等）会等待浏览器准备就绪。

如果你传入 `url`，那么在构造函数返回之前视图就会开始导航。等价于在下一行调用 `view.navigate(url)`。

### 使用 `using` 的自动清理

`Bun.WebView` 实现了 `Symbol.dispose` 和 `Symbol.asyncDispose`，因此你可以使用 `using` 或 `await using`，当视图超出作用域时自动关闭：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
{
  await using view = new Bun.WebView();
  await view.navigate("https://example.com");
  // ...
} // 这里会自动调用 view.close()
```

### 持久化存储

默认情况下，每个视图使用**短暂的**内存存储 — cookies、`localStorage`、IndexedDB 和缓存都会在视图关闭时丢弃。要在多次运行之间持久化状态，请传入目录：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const view = new Bun.WebView({
  dataStore: { directory: "./browser-profile" },
});
```

共享同一个 `directory` 的视图会共享 cookies 和存储。传入 `dataStore: "ephemeral"`（默认值）以显式回到内存存储。

<Note>
  使用 Chrome 后端时，`dataStore.directory` 会映射到 `--user-data-dir`，并作用于**整个 Chrome 进程**，
  而不是每个视图。由于 Chrome 会在每个 Bun 进程中只启动一次，因此第一个视图的目录会赢，影响后续所有视图。
</Note>

<Note>
  使用 WebKit 后端时，持久化存储需要 macOS 15.2+。在旧版本的 macOS 上，请使用 `dataStore:
      "ephemeral"`（默认值）。
</Note>

***

## 后端

`Bun.WebView` 支持两种渲染引擎。默认取决于你的平台：

| 后端         | 引擎        | 平台            | 需求                                                                       |
| ---------- | --------- | ------------- | ------------------------------------------------------------------------ |
| `"webkit"` | WKWebView | 仅 macOS       | 无 — 使用系统 `WebKit.framework`                                              |
| `"chrome"` | Blink     | macOS / Linux | 已安装 Chrome、Chromium、Edge 或 Brave（或 Playwright 的 `chrome-headless-shell`） |

在 macOS 上默认是 `"webkit"`；其他平台上是 `"chrome"`。在非 macOS 平台请求 `backend: "webkit"` 会抛出异常。

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// 在 macOS 上强制使用 Chrome
const view = new Bun.WebView({ backend: "chrome" });
```

### WebKit 后端如何工作

Bun 会生成一个轻量的宿主子进程（`bun` 可执行文件本身，会以特殊模式重新执行），它在主线程上拥有 `WKWebView`。你的 Bun 进程通过 Unix socket 使用紧凑的二进制协议与它通信。宿主进程只会生成一次，并被程序中所有 `"webkit"` 视图共享。

### Chrome 后端如何工作

Bun 要么 **连接** 到一个已经运行的 Chrome（通过 WebSocket），要么 **生成** 一个无头 Chrome 子进程，并通过管道与它通信（`--remote-debugging-pipe`）。无论哪种方式，通信都使用 [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)。

Chrome 会在每个 Bun 进程中生成（或连接）一次。每个 `new Bun.WebView({ backend: "chrome" })` 会在这一个 Chrome 实例中通过 `Target.createTarget` 创建一个新的标签页。

#### 查找 Chrome 可执行文件

当 Bun 需要生成 Chrome 时，它会按以下顺序搜索：

1. 你传入的 `backend: { type: "chrome", path: "..." }` 中的 `path`
2. `BUN_CHROME_PATH` 环境变量
3. `$PATH`（`google-chrome-stable`, `google-chrome`, `chromium-browser`, `chromium`, `brave-browser`, `microsoft-edge`, `chrome`）
4. 标准安装位置（`/Applications/Google Chrome.app`, `~/Applications/...`, `/usr/bin/...`, `/snap/bin/...` 等）
5. Playwright 缓存（`~/Library/Caches/ms-playwright` 或 `~/.cache/ms-playwright`）中的 `chrome-headless-shell`

如果找不到，会在构造函数中抛出。

<h4 id="existing-chrome">
  连接到已在运行的 Chrome
</h4>

默认情况下，在生成之前，Bun 会通过从标准 profile 目录读取 `DevToolsActivePort` 文件来检查某个 Chrome 家族浏览器是否已经在运行并启用了远程调试。如果找到，Bun 会通过 WebSocket 连接到该浏览器，而不是再生成一个 — 你的视图会作为标签页打开在现有浏览器中。

要在运行中的 Chrome 中启用远程调试，访问 `chrome://inspect/#remote-debugging` 并切换开关，或使用 `--remote-debugging-port=9222` 启动 Chrome。每次使用 `chrome://inspect` 开关连接到新的会话时，Chrome 都会在请求权限时提示。

要显式控制这一行为，请使用 `backend` 的对象形式：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// 总是生成一个新的无头 Chrome；永远不自动连接
new Bun.WebView({
  backend: { type: "chrome", url: false },
});

// 连接到特定的 DevTools WebSocket URL
new Bun.WebView({
  backend: {
    type: "chrome",
    url: "ws://127.0.0.1:9222/devtools/browser/abc123...",
  },
});
```

如果自动探测发现过期的 `DevToolsActivePort` 文件（Chrome 崩溃或重启），WebSocket 连接将失败，并且 Bun 会透明地回退到生成自己的 Chrome。显式指定 `url: "ws://..."` **不会**回退 — 连接失败会直接抛出异常。

<Note>
  传入 `path` 或 `argv` 会隐含进入生成模式并跳过自动探测。`url: "ws://..."` 不能与 `path` 或
  `argv` 组合使用。
</Note>

#### 启动参数

生成时，Bun 会传入一组最小的参数：

```
--user-data-dir=<temp> --remote-debugging-pipe --headless --no-first-run
--no-default-browser-check --disable-gpu --disable-extensions
--disable-background-networking --disable-background-timer-throttling
--disable-backgrounding-occluded-windows --disable-renderer-backgrounding
--disable-ipc-flooding-protection --no-startup-window
```

用 `argv` 把你自己的参数追加进去 — Chrome 处理重复开关时采用“最后生效”，因此你可以覆盖任意默认值：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
new Bun.WebView({
  backend: {
    type: "chrome",
    argv: ["--headless=new", "--lang=ja-JP", "--window-size=1920,1080"],
  },
});
```

#### 子进程输出

默认情况下会静默浏览器子进程的 stdout/stderr。特别是 Chrome 的 stderr 很“吵”（字体配置警告、GCM 注册、更新检查）。如果你想看到它（在 Chrome 静默崩溃时尤其有用），请这样转发：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
new Bun.WebView({
  backend: { type: "chrome", stderr: "inherit", stdout: "inherit" },
});
```

`"webkit"` 后端也接受相同的 `stdout`/`stderr` 选项。

***

## 导航

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.navigate("https://example.com");
await view.navigate("data:text/html,<h1>hello</h1>");
await view.navigate("file:///path/to/index.html");
```

当主框架的 `load` 事件触发时，`navigate()` 才会解析完成。解析完成后，`view.url` 和 `view.title` 会反映新页面，且 `view.loading` 为 `false`。

如果导航失败（DNS 失败、连接被拒绝、URL 无效），该 promise 会以描述失败原因的 `Error` 形式拒绝。

每个视图在任一时刻只能进行一个正在进行的导航。若在另一个 `navigate()` 仍待处理时再次调用 `navigate()`，会同步抛出 `ERR_INVALID_STATE`。

### 历史记录

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.goBack(); // 类似浏览器的返回按钮
await view.goForward(); // 类似浏览器的前进按钮
await view.reload(); // 重新加载当前页面
```

在历史记录的起点（或终点）调用 `goBack()`（或 `goForward()`）会直接解析为 `undefined`，不进行导航 — 不会拒绝。

### 导航回调

设置 `onNavigated` 和 `onNavigationFailed` 来观察每一次导航，包括页面自身触发的导航（点击链接、`location.href = ...`、重定向）以及由 `reload()`/`goBack()`/`goForward()` 触发的导航：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
view.onNavigated = (url, title) => {
  console.log("Loaded", url);
};

view.onNavigationFailed = error => {
  console.error("Navigation failed:", error.message);
};
```

这些回调会在对应的 `navigate()` promise 完成（settle）之前触发，因此当你 `await view.navigate(...)` 返回时，你的回调已经运行过了。设置为 `null` 可移除。

***

## 计算 JavaScript

在页面的主框架中运行一个表达式，并把结果作为原生 JavaScript 值返回：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const title = await view.evaluate("document.title");
const items = await view.evaluate("[...document.querySelectorAll('li')].map(li => li.textContent)");
const user = await view.evaluate("({ name: 'bun', ok: true })");
```

脚本会被包装为 `await (<your script>)`，因此：

* 它必须是一个**表达式**，而不是语句序列。要包含多条语句请用 IIFE 包裹：`evaluate("(() => { let x = foo(); return x + 1 })()")`。
* 如果它计算为一个 `Promise`，会等待该 promise 并返回其解析值。

结果会在页面侧通过 `JSON.stringify`，在 Bun 侧通过 `JSON.parse` 来往返传递。数组和普通对象会以真实结构返回；`undefined`、函数和 symbol 会解析为 `undefined`；循环引用会导致拒绝。

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.evaluate("42"); // 42
await view.evaluate("[1, 2, 3]"); // [1, 2, 3]
await view.evaluate("undefined"); // undefined
await view.evaluate("() => 1"); // undefined（函数不会序列化）
await view.evaluate("fetch('/api').then(r => r.json())"); // 已等待
```

如果脚本抛出异常（或返回被拒绝的 promise），`evaluate()` 会拒绝，并把消息作为页面侧异常信息的一部分返回一个 `Error`。

每个视图在任一时刻只能进行一个正在进行的 `evaluate()`；第二个并发调用会同步抛出 `ERR_INVALID_STATE`。

***

## 截图

把当前视口捕获成图片：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const png = await view.screenshot();
await Bun.write("page.png", png);
```

### 图片格式

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.screenshot({ format: "png" }); // 无损（默认）
await view.screenshot({ format: "jpeg", quality: 90 }); // 有损，quality 0-100（默认 80）
await view.screenshot({ format: "webp", quality: 75 }); // 仅 Chrome 后端
```

PNG 会忽略 `quality`。`"webp"` 只在 `backend: "chrome"` 时可用 — WebKit 后端会抛出异常。

### 返回类型

`encoding` 选项控制图片字节如何返回给你：

| `encoding`      | 返回内容                             | 说明                                                                      |
| --------------- | -------------------------------- | ----------------------------------------------------------------------- |
| `"blob"` *(默认)* | `Blob`                           | MIME 类型自动设置。WebKit 上基于 mmap 的零拷贝。可与 `Bun.write()`、`new Response()` 一起使用 |
| `"buffer"`      | `Buffer`                         | Node `Buffer`。WebKit 上基于 mmap 的零拷贝                                      |
| `"base64"`      | `string`                         | Base64 编码。Chrome 上无需解码（CDP 原生返回 base64）                                 |
| `"shmem"`       | `{ name: string, size: number }` | POSIX 共享内存段名称。调用方负责 `shm_unlink`。Windows 不支持                            |

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const buf = await view.screenshot({ encoding: "buffer" });
console.log(buf[0] === 0x89); // PNG 魔术字节

const b64 = await view.screenshot({ encoding: "base64" });
console.log(`<img src="data:image/png;base64,${b64}">`);
```

#### 终端图形的共享内存

`encoding: "shmem"` 专为 Kitty 的 [终端图形协议](https://sw.kovidgoyal.net/kitty/graphics-protocol/) `t=s` 传输模式设计 — Bun 会把图片写入一个 POSIX 共享内存段并返回其名称；终端直接读取，并在完成后取消链接（unlinks）。无需通过管道复制。

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const { name, size } = await view.screenshot({ encoding: "shmem" });
process.stdout.write(`\x1b_Gf=100,t=s,a=T,S=${size};${btoa(name)}\x1b\\`);
// Kitty 从共享内存读取 PNG 并取消链接该段
```

在 WebKit 上，shm 名称看起来像 `/bun-webview-<pid>-<seq>`；在 Chrome 上是 `/bun-chrome-<pid>-<seq>`。如果你请求 `"shmem"`，但没有把名称交给某个会执行 `shm_unlink` 的逻辑，那么共享内存段会在你的进程退出前一直泄漏。

***

## 输入模拟

所有输入方法都会分发**原生**浏览器事件。页面会收到 `pointerdown`/`mousedown`/`keydown`/`wheel` 事件，且带有 `isTrusted: true`；CSS `:active` 与 `:hover` 状态会生效，并且默认行为（表单提交、链接导航、文本选择）会像真实用户操作那样准确触发。

### 点击

在视口坐标处点击：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.click(150, 200);
await view.click(150, 200, { button: "right" });
await view.click(150, 200, { clickCount: 2 }); // 双击
await view.click(150, 200, { modifiers: ["Shift", "Meta"] });
```

该 promise 会在页面处理完整的 `mousedown` → `mouseup` → `click` 序列之后才解析完成，包括任何 JavaScript 处理器。无需轮询 — 随后的 `evaluate()` 会看到结果。

#### 通过选择器点击

传入 CSS 选择器而不是坐标，Bun 会等待该元素变为**可操作（actionable）**，然后点击它的中心：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.click("#submit");
await view.click("button.primary", { timeout: 5000 });
```

当元素满足以下条件时，它才是可操作的：

* 存在于 DOM 中
* 拥有非零的边界框
* 位于视口内
* 已保持**稳定**（在连续两帧动画中边界框未改变）
* 在其中心点处是最上层元素（没有被覆盖物遮挡）

该检查会在页面侧以 `requestAnimationFrame` 的频率运行。如果该元素在 `timeout` 毫秒内始终未变为可操作（默认 `30000`），promise 会以类似 `timeout waiting for '#submit' to be actionable` 的错误拒绝。

选择器作为数据传递，而不是插值进脚本，因此包含引号或 JavaScript 语法的选择器是安全的。

### 输入文本

把文本插入到当前聚焦的元素中：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.click("input#email"); // 先聚焦
await view.type("hello@example.com");
```

`type()` 使用浏览器的 `InsertText` 编辑命令（与粘贴使用同一路径），而不是逐字符的按键模拟。它会触发带 `isTrusted: true` 的 `beforeinput`/`input` 事件，但**不会**触发 `keydown`/`keyup` 事件。不会进行 IME 处理，也不会进行智能引号替换 — 文本会被按原样精确写入。

### 按下按键

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.press("Enter");
await view.press("Escape");
await view.press("ArrowDown");
await view.press("a", { modifiers: ["Meta"] }); // Cmd+A / Ctrl+A
```

命名的虚拟按键：`Enter`、`Tab`、`Space`、`Backspace`、`Delete`、`Escape`、`ArrowLeft`、`ArrowRight`、`ArrowUp`、`ArrowDown`、`Home`、`End`、`PageUp`、`PageDown`。

任意单字符（例如 `"a"`）结合 `modifiers` 会发送一个按键组合键（chord）。

在 WebKit 后端，大多数命名按键（不带修饰键）会映射到编辑命令（`DeleteBackward`、`MoveLeft`、`InsertNewline` 等），并在页面应用之后解析完成。`Escape`、`Space`，以及任何带修饰键的按键会回退到原始的 `keydown`/`keyup` 事件 — 这些事件会触发一个页面可观察到的 `keydown`，但没有“完成屏障”，所以如果你需要观察其效果，请在后面跟一个 `evaluate()`。

修饰键名称：`"Shift"`、`"Control"`（或 `"Ctrl"`）、`"Alt"`（或 `"Option"`）、`"Meta"`（或 `"Cmd"` / `"Command"`）。

### 滚动

按像素增量滚动 — 在视口中心触发一个原生 `wheel` 事件：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.scroll(0, 500); // 向下滚动 500px
await view.scroll(-100, 0); // 向左滚动 100px
```

正的 `dy` 会向下滚动（内容向上移动），与 `window.scrollBy` 一致。如果视口中心下方有可滚动元素，它会接收到 wheel 事件，而不是整个文档。

通过选择器把元素滚入视野：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.scrollTo("#footer"); // 居中显示（默认）
await view.scrollTo("#hero", { block: "start" }); // 让元素顶部对齐视口顶部
await view.scrollTo(".card", { block: "nearest" }); // 最小滚动
```

`scrollTo()` 会以 `requestAnimationFrame` 的频率等待元素存在，然后调用 `element.scrollIntoView({ block, behavior: "instant" })`。它会滚动所有可滚动祖先，而不仅仅是文档。默认 `timeout` 为 `30000` ms。

### 调整大小

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.resize(1920, 1080);
```

宽度和高度分别必须在 `1` 到 `16384` 之间。

***

## 控制台捕获

通过在构造函数中传入 `console` 选项，把页面侧的 `console.*` 调用转发到你的 Bun 进程。

### 镜像到 Bun 的控制台

传入 `globalThis.console`（实际对象，按引用）后，页面侧 `console.log("hi")` 会以 Bun 的格式化器把 `hi` 输出到你的 stdout；`console.error` 输出到 stderr。此路径会直接通过 Bun 的控制台实现分发，不会为每次调用引入额外的 JavaScript 开销。

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const view = new Bun.WebView({
  console: globalThis.console,
});
```

### 自定义处理器

传入一个函数来接收每次调用：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const view = new Bun.WebView({
  console: (type, ...args) => {
    // type 是 "log" | "warn" | "error" | "info" | "debug" | ...
    if (type === "error") reportError(args);
  },
});
```

原始参数（字符串、数字、布尔值、`null`、`undefined`）会解包为它们的原始值。对象参数会以序列化描述符的形式到达：

* **Chrome 后端**：原始的 CDP [`RemoteObject`](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject) — 一个包含 `type`、`className`、`description`，以及（在可用时）`preview.properties` 数组的对象。
* **WebKit 后端**：对象经过 `JSON.stringify` 往返后的结果。函数、循环引用以及其他不可序列化值会回退为 `String(...)` 的强制转换。

如果你不传 `console`，页面侧的控制台输出会被丢弃。

<Note>
  顺序保证：在你传给 `evaluate()` 的脚本内部执行的 `console.log(...)` 会在该 `evaluate()` promise 解析完成之前交付给你的处理器。两者会通过同一条 IPC 连接传输。
</Note>

***

<h2 id="cdp">
  原始 Chrome DevTools Protocol
</h2>

当使用 `backend: "chrome"` 时，如果高层 API 没有覆盖你想要的能力，你可以直接切换到原始的 [CDP](https://chromedevtools.github.io/devtools-protocol/) 命令。

### 发送命令

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const view = new Bun.WebView({ backend: "chrome" });
await view.navigate("https://example.com"); // 必需：设置 CDP 会话

await view.cdp("Emulation.setUserAgentOverride", {
  userAgent: "MyBot/1.0",
});

const { root } = await view.cdp("DOM.getDocument");
const { nodeId } = await view.cdp("DOM.querySelector", {
  nodeId: root.nodeId,
  selector: "input[name=q]",
});
await view.cdp("DOM.focus", { nodeId });
```

`cdp(method, params?)` 会返回 CDP 响应中的 `result` 对象。如果 Chrome 返回错误（未知方法、参数错误），该 promise 会以 `error.message` 拒绝。

命令会被限定在此视图的会话范围内（目标是此标签页）。在调用 `cdp()` 之前至少要 `await navigate(...)` 一次 — 第一次导航会建立会话。若在那之前调用 `cdp()` 会抛出 `ERR_INVALID_STATE`。

`params` 必须是可 JSON 序列化的对象；对于不带参数的命令可省略。每个视图同一时刻最多只能有一个 `cdp()` 调用正在进行。

### 订阅事件

`Bun.WebView` 继承自 `EventTarget`。在 Chrome 后端下，CDP 事件会作为 DOM 事件分发：`type` 为 CDP 方法名，`data` 为解析后的 `params` 对象：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
await view.navigate("about:blank");
await view.cdp("Network.enable"); // 仅 Chrome：只有启用了域才会发出事件

view.addEventListener("Network.responseReceived", event => {
  console.log(event.data.response.status, event.data.response.url);
});

await view.navigate("https://example.com");
```

如果没有为某个事件注册监听器，它会在 JSON `params` 解析之前就被丢弃，因此如果你只监听一两种事件类型，开启“啰嗦”的域（例如 `Network`）也不会太贵。

在 WebKit 后端上，`cdp()` 会抛出 `ERR_METHOD_NOT_IMPLEMENTED` — 没有 DevTools Protocol 的桥接。`EventTarget` 接口仍然可用于你自己对 `dispatchEvent()` 的调用。

***

## 生命周期

### 关闭视图

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
view.close();
```

关闭是同步且可重复调用（幂等）的。它会销毁页面的渲染器进程，并用 `Error("WebView closed")` 拒绝视图上所有待处理的 promise，同时让之后对该视图的所有方法调用都会抛出 `ERR_INVALID_STATE`。

`view[Symbol.dispose]` 和 `view[Symbol.asyncDispose]` 都指向 `close()`，所以 `using` / `await using` 可以正常工作。

### 杀死所有浏览器

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
Bun.WebView.closeAll();
```

强制终止（`SIGKILL`）所有浏览器子进程（包括 Chrome 子进程和 WebKit 宿主子进程）。每个视图上的待处理 promise 会在下一次事件循环 tick 时拒绝。之后的 `new Bun.WebView()` 调用会在需要时重新生成。

Bun 会在进程退出时自动调用它，因此浏览器子进程不会在你的脚本之外存活。

### 事件循环行为

浏览器子进程不会自行保持 Bun 的事件循环存活。只要存在打开的 `WebView` 且它还有未完成的操作（例如未 settle 的 `navigate()`、`evaluate()` 等），进程就会保持运行。你关闭最后一个视图 — 或最后一个待处理操作完成（settle）之后 — Bun 会自然退出。

### 子进程死亡

如果浏览器子进程意外死亡（崩溃、OOM-kill、`SIGKILL`），则所有视图上的待处理 promise 都会以描述死亡原因的错误拒绝（例如 `"Chrome killed by signal 9"`、`"WebView host process died"` 等），并且对这些视图的后续操作会抛出异常。

***

## 并发模型

每个视图都有少量用于并发的独立“操作槽位”。同一种操作在任一时刻最多只有一个在飞行（in flight）：

* 一个 `navigate()`（在 Chrome 后端与 `reload()`/`goBack()`/`goForward()` 共享）
* 一个 `evaluate()`
* 一个 `screenshot()`
* 一个 `cdp()`（仅 Chrome）
* 一个“简单”操作 — `click()`、`type()`、`press()`、`scroll()`、`scrollTo()`、`resize()`（以及 WebKit 后端的 `reload()`/`goBack()`/`goForward()`）共享此槽位

当某个槽位已被占用时，在启动第二个该类型操作会同步抛出 `ERR_INVALID_STATE` — 不会排队。在实际使用中，直接对每次调用 `await` 即可。

不同视图之间的操作完全独立，并会并行运行 — 每个视图都有自己的渲染器进程。

***

## 参考

### `new Bun.WebView(options?)`

| 选项          | 类型                                              | 默认值                               | 描述                                                                     |
| ----------- | ----------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------- |
| `width`     | `number`                                        | `800`                             | 视口宽度（CSS 像素）。范围 1-16384。                                               |
| `height`    | `number`                                        | `600`                             | 视口高度（CSS 像素）。范围 1-16384。                                               |
| `url`       | `string`                                        | —                                 | 立即开始导航到该 URL。                                                          |
| `headless`  | `boolean`                                       | `true`                            | 仅实现 `true`；`false` 会抛出异常。                                              |
| `backend`   | `"webkit"` \| `"chrome"` \| [object](#backends) | macOS 为 `"webkit"`，其他为 `"chrome"` | 渲染引擎。                                                                  |
| `console`   | `typeof console` \| `(type, ...args) => void`   | —                                 | 捕获页面侧的 `console.*` 调用。见 [控制台捕获](#console-capture)。                     |
| `dataStore` | `"ephemeral"` \| `{ directory: string }`        | `"ephemeral"`                     | cookies / localStorage / IndexedDB 的存储。见 [持久化存储](#persistent-storage)。 |

#### `backend` 对象形式

| 选项       | 类型                        | 描述                                                                                                  |
| -------- | ------------------------- | --------------------------------------------------------------------------------------------------- |
| `type`   | `"chrome"` \| `"webkit"`  | **必填。** 使用哪个引擎。                                                                                     |
| `path`   | `string`                  | （`chrome` only）Chrome/Chromium 可执行文件的路径。强制进入生成模式。                                                   |
| `argv`   | `string[]`                | （`chrome` only）额外的启动参数，会在默认参数之后追加。强制进入生成模式。                                                         |
| `url`    | `string` \| `false`       | （`chrome` only）现有 Chrome 的 DevTools 端点 `ws://` URL；或传 `false` 跳过自动探测并始终生成。见 [上文](#existing-chrome)。 |
| `stdout` | `"inherit"` \| `"ignore"` | 把子进程的 stdout 转发到 Bun。默认 `"ignore"`。                                                                 |
| `stderr` | `"inherit"` \| `"ignore"` | 把子进程的 stderr 转发到 Bun。默认 `"ignore"`。                                                                 |

### 实例属性

| 属性                   | 类型                                               | 描述                                      |
| -------------------- | ------------------------------------------------ | --------------------------------------- |
| `url`                | `string` *(readonly)*                            | 当前 URL。导航完成后更新。首次导航前为空字符串。              |
| `title`              | `string` *(readonly)*                            | 页面 `<title>`。导航完成后更新。                   |
| `loading`            | `boolean` *(readonly)*                           | 当正在进行导航时为 `true`。                       |
| `onNavigated`        | `((url: string, title: string) => void) \| null` | 每次成功导航后触发：在 `navigate()` promise 解析前触发。 |
| `onNavigationFailed` | `((error: Error) => void) \| null`               | 每次失败导航后触发：在 `navigate()` promise 拒绝前触发。 |

### 实例方法

| 方法                             | 返回值                                                 | 描述                                                                               |
| ------------------------------ | --------------------------------------------------- | -------------------------------------------------------------------------------- |
| `navigate(url)`                | `Promise<void>`                                     | 加载一个 URL。会在主框架的 `load` 事件触发时解析完成。                                                |
| `evaluate(script)`             | `Promise<unknown>`                                  | 在页面中运行一个 JS 表达式，并返回其 JSON 序列化后的结果。                                               |
| `screenshot(options?)`         | `Promise<Blob \| Buffer \| string \| {name, size}>` | 截取视口。见 [截图](#screenshots)。                                                       |
| `click(x, y, options?)`        | `Promise<void>`                                     | 在视口坐标处进行原生点击。                                                                    |
| `click(selector, options?)`    | `Promise<void>`                                     | 等待元素变为可操作，然后点击其中心。                                                               |
| `type(text)`                   | `Promise<void>`                                     | 通过 `InsertText` 编辑命令向聚焦元素插入文本。                                                   |
| `press(key, options?)`         | `Promise<void>`                                     | 按下一个命名虚拟按键或单字符组合键。                                                               |
| `scroll(dx, dy)`               | `Promise<void>`                                     | 在视口中心触发一个原生 `wheel` 事件。`dx`/`dy` 必须是有限值。                                         |
| `scrollTo(selector, options?)` | `Promise<void>`                                     | 等待元素存在，然后调用 `scrollIntoView`。                                                    |
| `resize(width, height)`        | `Promise<void>`                                     | 修改视口尺寸。每个维度 1-16384。                                                             |
| `goBack()`                     | `Promise<void>`                                     | 在会话历史中向后导航。在起点为无操作（no-op）。                                                       |
| `goForward()`                  | `Promise<void>`                                     | 在会话历史中向前导航。在终点为无操作（no-op）。                                                       |
| `reload()`                     | `Promise<void>`                                     | 重新加载当前页面。                                                                        |
| `cdp(method, params?)`         | `Promise<unknown>`                                  | （仅 Chrome）向该标签页发送一个作用域限定的原始 CDP 命令。见 [原始 CDP](#cdp)。                             |
| `addEventListener(type, fn)`   | `void`                                              | 从 `EventTarget` 继承而来。对于 Chrome，`type` 可能是一个 CDP 事件名；`event.data` 是解析后的 `params`。 |
| `close()`                      | `void`                                              | 销毁页面。拒绝所有待处理 promise。幂等。                                                         |

#### `click()` 选项

| 选项           | 类型                                            | 默认值      | 描述                    |
| ------------ | --------------------------------------------- | -------- | --------------------- |
| `button`     | `"left"` \| `"right"` \| `"middle"`           | `"left"` | 鼠标按键。                 |
| `modifiers`  | `("Shift" \| "Control" \| "Alt" \| "Meta")[]` | `[]`     | 点击时按住的修饰键。            |
| `clickCount` | `1` \| `2` \| `3`                             | `1`      | 双击/三击的点击次数。           |
| `timeout`    | `number`                                      | `30000`  | （仅选择器重载）等待可操作性的最长毫秒数。 |

#### `press()` 选项

| 选项          | 类型                                            | 默认值  | 描述           |
| ----------- | --------------------------------------------- | ---- | ------------ |
| `modifiers` | `("Shift" \| "Control" \| "Alt" \| "Meta")[]` | `[]` | 按下按键时按住的修饰键。 |

#### `scrollTo()` 选项

| 选项        | 类型                                                | 默认值        | 描述            |
| --------- | ------------------------------------------------- | ---------- | ------------- |
| `block`   | `"start"` \| `"center"` \| `"end"` \| `"nearest"` | `"center"` | 滚动后的垂直对齐方式。   |
| `timeout` | `number`                                          | `30000`    | 等待元素存在的最长毫秒数。 |

#### `screenshot()` 选项

| 选项         | 类型                                                | 默认值      | 描述                            |
| ---------- | ------------------------------------------------- | -------- | ----------------------------- |
| `format`   | `"png"` \| `"jpeg"` \| `"webp"`                   | `"png"`  | 图片格式。`"webp"` 需要 Chrome 后端。   |
| `quality`  | `number`                                          | `80`     | 0-100。仅 JPEG/WebP；PNG 会忽略。    |
| `encoding` | `"blob"` \| `"buffer"` \| `"base64"` \| `"shmem"` | `"blob"` | 返回类型编码。Windows 不支持 `"shmem"`。 |

### 静态方法

| 方法                   | 描述                                                   |
| -------------------- | ---------------------------------------------------- |
| `WebView.closeAll()` | `SIGKILL` 每个浏览器子进程。下一 tick 上拒绝所有待处理 promise。退出时自动调用。 |
