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 会通过从标准 profile 目录读取 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 选项。

导航

当主框架的 load 事件触发时,navigate() 才会解析完成。解析完成后,view.urlview.title 会反映新页面,且 view.loadingfalse 如果导航失败(DNS 失败、连接被拒绝、URL 无效),该 promise 会以描述失败原因的 Error 形式拒绝。 每个视图在任一时刻只能进行一个正在进行的导航。若在另一个 navigate() 仍待处理时再次调用 navigate(),会同步抛出 ERR_INVALID_STATE

历史记录

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

导航回调

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

计算 JavaScript

在页面的主框架中运行一个表达式,并把结果作为原生 JavaScript 值返回:
脚本会被包装为 await (<your script>),因此:
  • 它必须是一个表达式,而不是语句序列。要包含多条语句请用 IIFE 包裹:evaluate("(() => { let x = foo(); return x + 1 })()")
  • 如果它计算为一个 Promise,会等待该 promise 并返回其解析值。
结果会在页面侧通过 JSON.stringify,在 Bun 侧通过 JSON.parse 来往返传递。数组和普通对象会以真实结构返回;undefined、函数和 symbol 会解析为 undefined;循环引用会导致拒绝。
如果脚本抛出异常(或返回被拒绝的 promise),evaluate() 会拒绝,并把消息作为页面侧异常信息的一部分返回一个 Error 每个视图在任一时刻只能进行一个正在进行的 evaluate();第二个并发调用会同步抛出 ERR_INVALID_STATE

截图

把当前视口捕获成图片:

图片格式

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

返回类型

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

终端图形的共享内存

encoding: "shmem" 专为 Kitty 的 终端图形协议 t=s 传输模式设计 — Bun 会把图片写入一个 POSIX 共享内存段并返回其名称;终端直接读取,并在完成后取消链接(unlinks)。无需通过管道复制。
在 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 处理程序)后解析。无需轮询 — 后续的 evaluate() 可以看到结果。

通过选择器点击

传入 CSS 选择器而不是坐标,Bun 会等待元素变为可操作,然后点击其中心:
当元素满足以下条件时,它才是可操作的:
  • 存在于 DOM 中
  • 拥有非零的边界框
  • 位于视口内
  • 已保持稳定(在连续两帧动画中边界框未改变)
  • 在其中心点处是最上层元素(没有被覆盖物遮挡)
该检查会在页面侧以 requestAnimationFrame 的频率运行。如果该元素在 timeout 毫秒内始终未变为可操作(默认 30000),promise 会以类似 timeout waiting for '#submit' to be actionable 的错误拒绝。 选择器作为数据传递,而不是插值进脚本,因此包含引号或 JavaScript 语法的选择器是安全的。

输入文本

把文本插入到当前聚焦的元素中:
type() 使用浏览器的 InsertText 编辑命令(与粘贴使用同一路径),而不是逐字符的按键模拟。它会触发带 isTrusted: truebeforeinput/input 事件,但不会触发 keydown/keyup 事件。不会进行 IME 处理,也不会进行智能引号替换 — 文本会被按原样精确写入。

按下按键

命名的虚拟按键:EnterTabSpaceBackspaceDeleteEscapeArrowLeftArrowRightArrowUpArrowDownHomeEndPageUpPageDown 任意单个字符(例如 "a")与 modifiers 组合使用时,会发送键盘组合键。 在 WebKit 后端中,大多数命名按键(不带修饰键)会映射到 DeleteBackwardMoveLeftInsertNewline 等编辑命令,并在页面应用这些命令后解析。EscapeSpace 以及任何带修饰键的按键会回退为原始的 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") 会以 Bun 的格式化器把 hi 输出到你的 stdout;console.error 输出到 stderr。此路径会直接通过 Bun 的控制台实现分发,不会为每次调用引入额外的 JavaScript 开销。

自定义处理器

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

原始 Chrome DevTools Protocol

当使用 backend: "chrome" 时,如果高层 API 没有覆盖你想要的能力,你可以直接切换到原始的 CDP 命令。

发送命令

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 对象:
如果没有为某个事件注册监听器,它会在 JSON params 解析之前就被丢弃,因此如果你只监听一两种事件类型,开启“啰嗦”的域(例如 Network)也不会太贵。 在 WebKit 后端上,cdp() 会抛出 ERR_METHOD_NOT_IMPLEMENTED — 没有 DevTools Protocol 的桥接。EventTarget 接口仍然可用于你自己对 dispatchEvent() 的调用。

生命周期

关闭视图

关闭是同步且可重复调用(幂等)的。它会销毁页面的渲染器进程,并用 Error("WebView closed") 拒绝视图上所有待处理的 promise,同时让之后对该视图的所有方法调用都会抛出 ERR_INVALID_STATE view[Symbol.dispose]view[Symbol.asyncDispose] 都指向 close(),所以 using / await using 可以正常工作。

杀死所有浏览器

强制终止(SIGKILL)所有浏览器子进程(包括 Chrome 子进程和 WebKit 宿主子进程)。每个视图上的待处理 promise 会在下一次事件循环 tick 时拒绝。之后的 new Bun.WebView() 调用会在需要时重新生成。 Bun 会在进程退出时自动调用它,因此浏览器子进程不会在你的脚本之外存活。

事件循环行为

浏览器子进程本身不会让 Bun 的事件循环保持运行。打开的 WebView 仅会在存在待处理操作(例如尚未完成的 navigate()evaluate())时让进程保持运行。一旦你关闭最后一个视图,或最后一个待处理操作完成,Bun 就会自然退出。

子进程死亡

如果浏览器子进程意外终止(崩溃、因 OOM 被杀死、收到 SIGKILL),每个视图上的所有待处理 promise 都会拒绝,并附带描述其终止方式的错误("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() 选项

静态方法