Skip to main content
Bun.WebView 是内置于运行时的无头浏览器。使用它可以加载页面、在其中运行 JavaScript、模拟真实的用户输入以及截取屏幕截图 — 无需 Puppeteer、Playwright 或单独的浏览器下载。
此 API 是实验性的,可能在未来的版本中发生变化。
在 macOS 上,Bun.WebView 使用系统的 WKWebView — 无需安装任何东西。在 Linux 和 Windows 上,它通过 Chrome DevTools Protocol 驱动已安装的 Chrome、Chromium、Edge 或 Brave。 每个视图在其独立的渲染器进程中运行页面。所有输入方法(clicktypepressscroll)分派原生浏览器事件,因此页面看到 isTrusted: true — 与真实用户相同。

创建视图

构造函数是同步的 — 它会立即返回并在后台生成浏览器子进程。您 await 的第一个操作(例如 navigate()evaluate())会等待浏览器准备就绪。 如果传递了 url,视图会在构造函数返回之前开始导航。这等同于在下一行调用 view.navigate(url)

使用 using 自动清理

Bun.WebView 实现了 Symbol.disposeSymbol.asyncDispose,因此您可以使用 usingawait using 在视图超出作用域时自动关闭它:

持久化存储

默认情况下,每个视图使用临时内存存储 — cookies、localStorage、IndexedDB 和缓存在视图关闭时被丢弃。要在运行之间持久化状态,请传递一个目录:
共享相同 directory 的视图共享 cookies 和存储。传递 dataStore: "ephemeral"(默认值)可明确恢复到内存存储。
使用 Chrome 后端时,dataStore.directory 映射到 --user-data-dir 并应用于整个 Chrome 进程,而不是按视图。由于 Chrome 在每个 Bun 进程中只启动一次,第一个视图的目录将用于所有后续视图。
使用 WebKit 后端时,持久化存储需要 macOS 15.2+。在较旧的 macOS 版本上,请使用 dataStore: "ephemeral"(默认值)。

后端

Bun.WebView 支持两种渲染引擎。默认值取决于您的平台: 在 macOS 上默认是 "webkit";在其他平台上默认是 "chrome"。在非 macOS 平台上请求 backend: "webkit" 会抛出异常。

WebKit 后端的工作原理

Bun 生成一个轻量级的宿主子进程(bun 二进制本身,在特殊模式下重新执行),该进程在其主线程上拥有 WKWebView。您的 Bun 进程通过 Unix socket 使用紧凑的二进制协议与其通信。宿主进程只生成一次,并由程序中所有 "webkit" 视图共享。

Chrome 后端的工作原理

Bun 要么连接到已运行的 Chrome(通过 WebSocket),要么生成一个无头 Chrome 子进程并通过管道(--remote-debugging-pipe)与其通信。无论哪种方式,通信都使用 Chrome 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. $PATHgoogle-chrome-stablegoogle-chromechromium-browserchromiumbrave-browsermicrosoft-edgechrome
  4. 标准安装位置(/Applications/Google Chrome.app~/Applications/.../usr/bin/.../snap/bin/...
  5. Playwright 的缓存(~/Library/Caches/ms-playwright~/.cache/ms-playwright)中的 chrome-headless-shell
如果找不到,构造函数会抛出异常。

连接到已运行的 Chrome

默认情况下,在生成之前,Bun 会通过读取标准配置文件目录中的 DevToolsActivePort 文件,检查 Chrome 系列浏览器是否已在运行并启用了远程调试。如果找到,Bun 会通过 WebSocket 连接到该浏览器,而不是生成新的 — 您的视图会作为标签页在现有浏览器中打开。 要在运行的 Chrome 中启用远程调试,请访问 chrome://inspect/#remote-debugging 并切换开关,或使用 --remote-debugging-port=9222 启动 Chrome。当您使用 chrome://inspect 开关时,Chrome 会对每个新连接提示权限。 要显式控制此行为,请使用 backend 的对象形式:
如果自动检测发现过时的 DevToolsActivePort 文件(Chrome 崩溃或重启过),WebSocket 连接会失败,Bun 会透明地回退到生成自己的 Chrome。显式的 url: "ws://..." 不会回退 — 连接失败会直接抛出异常。
传递 pathargv 意味着生成模式,并跳过自动检测。url: "ws://..." 不能与 pathargv 组合使用。

启动标志

生成时,Bun 传递最小的标志集:
使用 argv 添加自己的标志 — Chrome 对重复的开关以后者为准,因此您可以覆盖任何默认值:

子进程输出

浏览器子进程的 stdout/stderr 默认被静音。Chrome 在 stderr 上特别嘈杂(字体配置警告、GCM 注册、更新检查)。要查看这些输出(在 Chrome 静默崩溃时很有用),请传递 "inherit"
"webkit" 后端也接受相同的 stdout/stderr 选项。

导航

navigate() 在主框架的 load 事件触发时 resolve。resolve 后,view.urlview.title 反映新页面,view.loadingfalse 如果导航失败(DNS 失败、连接被拒绝、无效 URL),promise 会以一个描述失败的 Error reject。 每个视图一次只能有一个进行中的导航。在另一个导航还在进行中时调用 navigate() 会同步抛出 ERR_INVALID_STATE

历史记录

在历史记录的开始处调用 goBack()(或在结尾处调用 goForward())会 resolve 为 undefined 而不导航 — 不会 reject。

导航回调

设置 onNavigatedonNavigationFailed 以观察每次导航,包括由页面本身触发(链接点击、location.href = ...、重定向)以及由 reload()/goBack()/goForward() 触发的导航:
这些回调在对应的 navigate() promise 安顿之前触发,因此当 await view.navigate(...) 返回时,您的回调已经运行完毕。设置为 null 以移除。

执行 JavaScript

在页面的主框架中运行表达式,并将结果作为原生 JavaScript 值返回:
脚本被包装为 await (<your script>),因此:
  • 必须是一个表达式,而不是一系列语句。对于多个语句,请包装在 IIFE 中:evaluate("(() => { let x = foo(); return x + 1 })()")
  • 如果计算结果为 Promise,则会等待该 promise 并返回其 resolve 的值。
结果通过页面中的 JSON.stringify 和 Bun 中的 JSON.parse 往返。数组和纯对象作为真实结构返回;undefined、函数和符号 resolve 为 undefined;循环引用会 reject。
如果脚本抛出(或返回被 reject 的 promise),evaluate() 会 reject 并返回一个 Error,其消息来自页面端的异常。 每个视图一次只能有一个 evaluate() 在进行中;第二个并发调用会抛出 ERR_INVALID_STATE

截图

将当前视口捕获为图像:

图像格式

quality 对 PNG 无效。"webp" 仅当 backend: "chrome" 时可用 — WebKit 后端会抛出异常。

返回类型

encoding 选项控制图像字节的返回方式:

终端图形的共享内存

encoding: "shmem" 专为 Kitty 的终端图形协议 t=s 传输模式设计 — Bun 将图像写入 POSIX 共享内存段并返回其名称;终端直接读取它,完成后解除链接。无需通过管道复制。
在 WebKit 上,shm 名称类似于 /bun-webview-<pid>-<seq>;在 Chrome 上,类似于 /bun-chrome-<pid>-<seq>。如果您请求 "shmem" 但没有将名称交给会执行 shm_unlink 的进程,该段会泄漏直到您的进程退出。

输入模拟

所有输入方法分派原生浏览器事件。页面接收 pointerdown/mousedown/keydown/wheel 事件,isTrusted: true,CSS :active:hover 状态生效,默认操作(表单提交、链接导航、文本选择)完全如同用户执行一样触发。

点击

在视口坐标处点击:
promise 在页面处理完完整的 mousedownmouseupclick 序列(包括任何 JavaScript 处理器)之后 resolve。无需轮询 — 后续的 evaluate() 就能看到结果。

按选择器点击

传递 CSS 选择器而不是坐标,Bun 会等待元素变为可操作,然后点击其中心:
元素可操作的条件:
  • 存在于 DOM 中
  • 具有非零的边界框
  • 在视口内
  • 在两个连续的动画帧期间保持稳定(边界框未变化)
  • 是其中心点处最顶层的元素(未被覆盖层遮挡)
该检查在页面端以 requestAnimationFrame 速率运行。如果元素在 timeout 毫秒内(默认 30000)始终未能变为可操作,promise 会以类似 timeout waiting for '#submit' to be actionable 的错误 reject。 选择器作为数据传递,而不是插值到脚本中,因此包含引号或 JavaScript 语法的选择器是安全的。

输入文本

将文本插入当前聚焦的元素:
type() 使用浏览器的 InsertText 编辑命令(与粘贴相同的路径),而不是逐字符按键。它会触发 beforeinput/input 事件,isTrusted: true,但不会触发 keydown/keyup 事件。没有 IME 处理,也没有智能引号替换 — 文本完全按照给定的内容显示。

按键

命名虚拟按键:EnterTabSpaceBackspaceDeleteEscapeArrowLeftArrowRightArrowUpArrowDownHomeEndPageUpPageDown 任何单个字符(例如 "a")结合 modifiers 会发送一个键盘和弦。 在 WebKit 后端上,大多数命名按键(不带 modifiers)映射到编辑命令,如 DeleteBackwardMoveLeftInsertNewline,并在页面应用它们后 resolve。EscapeSpace 以及任何带有 modifiers 的按键回退到原始 keydown/keyup 事件 — 这些会触发页面可以观察到的 keydown,但没有完成屏障,因此如果需要观察效果,请跟随一个 evaluate() 修饰符名称:"Shift""Control"(或 "Ctrl")、"Alt"(或 "Option")、"Meta"(或 "Cmd" / "Command")。

滚动

按像素增量滚动 — 在视口中心触发原生 wheel 事件:
dy 向下滚动(内容向上移动),与 window.scrollBy 一致。如果视口中心下方有可滚动元素,它将接收 wheel 事件而不是文档。 按选择器将元素滚动到视图中:
scrollTo() 会等待(以 requestAnimationFrame 速率)元素存在,然后调用 element.scrollIntoView({ block, behavior: "instant" })。它会滚动所有可滚动的祖先元素,而不仅仅是文档。默认 timeout30000 ms。

调整大小

宽度和高度必须在 116384 之间。

控制台捕获

通过将 console 选项传递给构造函数,将页面中的 console.* 调用转发到您的 Bun 进程。

镜像到 Bun 的控制台

传递 globalThis.console(实际对象,通过引用),页面端的 console.log("hi") 会将 hi 打印到您的 stdout,使用 Bun 的格式化器;console.error 会输出到 stderr。此路径直接通过 Bun 的控制台实现分派,没有每次调用的 JavaScript 开销。

自定义处理器

传递一个函数来自己接收每个调用:
基本参数(字符串、数字、布尔值、nullundefined)展开为其原始值。对象参数作为序列化描述符到达:
  • Chrome 后端:原始的 CDP RemoteObject — 一个包含 typeclassNamedescription 和(可用时)preview.properties 数组的对象。
  • WebKit 后端:对象的 JSON.stringify 往返结果。函数、循环引用和其他不可序列化的值回退到其 String(...) 强制转换。
如果您不传递 console,页面端的控制台输出会被丢弃。
排序保证:您传递给 evaluate() 的脚本内的 console.log(...) 会在该 evaluate() resolve 之前 到达您的处理器。两者通过同一个 IPC 连接传输。

原始 Chrome DevTools Protocol

当使用 backend: "chrome" 时,您可以降级到原始的 CDP 命令,以处理高级 API 未覆盖的任何内容。

发送命令

cdp(method, params?) 返回 CDP 响应中的 result 对象。如果 Chrome 返回错误(未知方法、错误参数),promise 会以其 error.message reject。 命令作用域为此视图的会话(它们针对此标签页)。在调用 cdp() 之前,您必须至少 await navigate(...) 一次 — 第一次导航会建立会话。在此之前调用 cdp() 会抛出 ERR_INVALID_STATE params 必须是可 JSON 序列化的对象;对于不接受参数的命令,可以省略。每个视图一次只能有一个 cdp() 调用在进行中。

订阅事件

Bun.WebView 扩展了 EventTarget。使用 Chrome 后端时,CDP 事件作为 DOM 事件分派,其 type 是 CDP 方法名称,data 是解析后的 params 对象:
没有注册监听器的事件会在 JSON params 甚至被解析之前丢弃,因此启用一个消息密集型域(如 Network)在您只监听一两种事件类型时是廉价的。 在 WebKit 后端上,cdp() 会抛出 ERR_METHOD_NOT_IMPLEMENTED — 没有 DevTools Protocol 桥接。EventTarget 接口仍然可以用于您自己的 dispatchEvent() 调用。

生命周期

关闭视图

关闭是同步且幂等的。它会销毁页面的渲染器进程,reject 视图上所有待处理的 promise(使用 Error("WebView closed")),并使每个后续方法调用抛出 ERR_INVALID_STATE view[Symbol.dispose]view[Symbol.asyncDispose] 都指向 close(),因此 using / await using 都能正常工作。

杀死所有浏览器

强制杀死(SIGKILL)Chrome 子进程和 WebKit 宿主子进程。所有视图上的待处理 promise 会在下一个事件循环 tick 上 reject。后续的 new Bun.WebView() 调用会根据需要重新生成。 Bun 在进程退出时自动调用此方法,因此浏览器子进程永远不会超出您的脚本的生命周期。

事件循环行为

浏览器子进程不会单独保持 Bun 的事件循环存活。打开的 WebView 仅在有挂起的操作(如未安顿的 navigate()evaluate())时保持进程存活。一旦您 close() 最后一个视图 — 或最后一个挂起操作安顿 — Bun 会正常退出。

子进程死亡

如果浏览器子进程意外死亡(崩溃、OOM 杀死、SIGKILL),所有视图上每个待处理的 promise 都会 reject 并附带描述死亡方式的错误("Chrome killed by signal 9""WebView host process died"),并且对这些视图的进一步操作会抛出异常。

并发模型

每个视图有少量独立操作”槽位”。每种操作一次只能有一个在进行中:
  • 一个 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?)

backend 对象形式

实例属性

实例方法

click() 选项

press() 选项

scrollTo() 选项

screenshot() 选项

静态方法