首页
/ Puppeteer Browser.wsEndpoint() 详解:获取浏览器 WebSocket 端点并实现断线重连

Puppeteer Browser.wsEndpoint() 详解:获取浏览器 WebSocket 端点并实现断线重连

2026-09-04 09:28:07作者:钟日瑜

Browser.wsEndpoint() 是 Puppeteer 中用于获取与当前浏览器实例通信的 WebSocket URL 的核心 API,也是 puppeteer.connect() 进行"连接已有浏览器"或"断线重连"的关键输入。本文以 wsEndpoint 官方文档 为主体,结合仓库源码,讲清楚该方法的签名与返回值、标准用法、端点格式约定,以及 CDP / WebDriver BiDi 两种协议下它在源码中的真实实现路径,帮助你掌握"获取端点 → 断开 → 重连"的完整实战方案。

方法签名与返回类型

按照 API 文档,该方法定义在抽象类 Browser 上,是一个同步方法:

class Browser {
  abstract wsEndpoint(): string;
}

返回值: string,即连接该浏览器实例的 WebSocket URL。

文档明确给出端点格式约定:

The format is always ws://HOST:PORT/devtools/browser/<id>.

即返回的 URL 始终遵循 ws://HOST:PORT/devtools/browser/<id> 的形式,其中 <id> 是浏览器自身的调试目标标识。这也意味着你拿到返回值后无需再做解析拼接,可以直接透传给 puppeteer.connect()

抽象声明位于 api/Browser.ts

/**
 * Gets the WebSocket URL to connect to this Browser.
 * ...
 * @remarks The format is always `ws://HOST:PORT/devtools/browser/<id>`.
 */
abstract wsEndpoint(): string;

Browser 的构造函数标记为内部(internal),第三方代码不应直接实例化或继承该类,而应通过 puppeteer.launch()puppeteer.connect() 获取 Browser 实例——参见 Browser 类文档

标准用法:断开连接后用端点重连

wsEndpoint() 最典型的场景是"先记录端点、断开、再重连"。这是 Browser 类文档示例 2 给出的官方用法,也是 api/Browser.ts 中 JSDoc 的原始示例:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
// 先保存端点,以便之后重连。
const browserWSEndpoint = browser.wsEndpoint();
// 断开 Puppeteer 与浏览器的连接(浏览器进程仍在运行)。
await browser.disconnect();

// 使用端点重新建立连接
const browser2 = await puppeteer.connect({browserWSEndpoint});
// 关闭浏览器。
await browser2.close();

这里有两个容易混淆的语义,值得对照 disconnect() 文档注意区分:

  • browser.disconnect():仅断开 Puppeteer 侧的连接,浏览器进程继续存活,因此之后可以用 wsEndpoint() 拿到的 URL 重新接上;
  • browser.close():关闭浏览器进程及其所有页面,端点随之失效。

也就是说,"可重连"的前提是浏览器进程没有被 close(),这一点正是 wsEndpoint() + disconnect() 组合的价值所在:让 Node 侧会话可断开、可迁移(例如跨进程、跨 Worker 传递端点字符串)。

不从 Puppeteer 启动时如何获得端点

当浏览器不是由 puppeteer.launch() 拉起的(例如你自己用命令行启动 Chrome、或浏览器跑在远程机器 / Docker 容器里),就无法调用 browser.wsEndpoint(),此时需要按文档说明手动获取调试器 URL:

You can find the debugger URL (webSocketDebuggerUrl) from http://HOST:PORT/json/version.

即向浏览器调试 HTTP 端点发送 GET 请求,从 JSON 响应中取出 webSocketDebuggerUrl 字段。这一做法在源码中同样得到印证:BrowserConnector.tsgetWSEndpoint() 内部就是 fetch('/json/version') 并读取 data.webSocketDebuggerUrl

对应到 puppeteer.connect() 上,有等价的两种方式:

// 方式一:直接给 WebSocket 端点
const browser = await puppeteer.connect({
  browserWSEndpoint: 'ws://HOST:PORT/devtools/browser/<id>',
});

// 方式二:只给 HTTP 端点,Puppeteer 自动走 /json/version 换取 ws 端点
const browser = await puppeteer.connect({
  browserURL: 'http://HOST:PORT',
});

源码纵深:端点在两种协议下的实现

wsEndpoint() 在抽象层只声明契约,真实返回的 URL 来自"当前连接"持有的端点地址。仓库中 packages/puppeteer-core/src 下共有三处涉及 wsEndpoint 的实现/声明,分别对应抽象基类和 CDP、BiDi 两套协议:

CDP 实现

cdp/Browser.ts

override wsEndpoint(): string {
  return this.#connection.url();
}

CDP 版 Browser 直接返回内部 Connection 对象记录的服务端 URL。而这个 URL 最初从哪来?看 node/BrowserLauncher.tscreateCdpSocketConnection()

const browserWSEndpoint = await browserProcess.waitForLineOutput(
  CDP_WEBSOCKET_ENDPOINT_REGEX,
  opts.timeout,
);
const transport = await WebSocketTransport.create(
  browserWSEndpoint,
  undefined,
  opts.logger,
);
return new Connection(browserWSEndpoint, transport, ...);

puppeteer.launch() 启动浏览器子进程后,用正则 CDP_WEBSOCKET_ENDPOINT_REGEX(定义于 packages/browsers/src/launch.ts)从浏览器 stdout 首行输出中提取 ws://... 端点,再以该端点构造 Connection。所以"launch 时的 wsEndpoint() 返回值"本质上是从子进程输出捕获到的那个 URL,与 http://HOST:PORT/json/version 返回的 webSocketDebuggerUrl 是同一个东西。

WebDriver BiDi 实现

bidi/Browser.ts

override wsEndpoint(): string {
  return this.connection.url;
}

BiDi 版 Browser 返回 BiDi 连接持有的 URL。注意从源码结构看,BiDi 场景下的端点路径形态由底层 BiDi 实现决定,文档中"always ws://HOST:PORT/devtools/browser/<id>"的强格式约定主要针对 CDP(Chrome)场景;Firefox 等走 WebDriver BiDi 协议时,端点语义相同(可用于 puppeteer.connect() 重连),但路径细节以实际连接对象为准。

Puppeteer.connect() 如何消费这个端点

wsEndpoint() 的返回值作为 browserWSEndpoint 传给 puppeteer.connect() 后,BrowserConnector.tsgetConnectionTransport() 处理它,其中有两条值得注意的源码事实:

  1. 互斥校验browserWSEndpointbrowserURLtransportchannel 四个选项必须且只能指定一个,否则抛出:

    Exactly one of browserWSEndpoint, browserURL, transport or channel must be passed to puppeteer.connect
    
  2. 协议分流:建立 WebSocket 传输后,_connectToBrowser() 会根据 protocol 选项选择 _connectToBiDiBrowser_connectToCdpBrowser。因此用 wsEndpoint() 重连时,如果原浏览器是 BiDi 协议启动的,重连也应带上相同的 protocolConnectOptions.tsprotocol 默认按 CDP 连接处理)。

此外,ConnectOptions 还支持 headers(仅 Node.js 环境生效,用于给 WebSocket 握手附加请求头,便于反向代理鉴权)等参数,完整参数说明见 connect() 文档

测试用例佐证

仓库测试直接验证了 wsEndpoint() 驱动的"重连后行为"语义。browser.test.ts 中的 should not return child_process for remote browser

const browserWSEndpoint = browser.wsEndpoint();
using remoteBrowser = await puppeteer.connect({
  browserWSEndpoint,
  protocol: browser.protocol,
});
expect(remoteBrowser.process()).toBe(null);

该用例印证了两点:其一,launch() 得到的端点可以直接用于 connect();其二,通过 connect() 建立的 Browser 不持有子进程句柄process() 返回 null——这与 process() 文档 的说明一致。换言之,"重连回来的 Browser 管连接,管不了进程",如果你需要终止浏览器进程,只能调用 browser2.close()(向浏览器发送关闭命令)而非依赖本地 ChildProcess。

实战要点小结

场景 获取端点的方式 备注
puppeteer.launch() 启动 browser.wsEndpoint() 端点来自子进程 stdout 捕获,可直接重连
自建 / 远程 / 容器化浏览器 GET http://HOST:PORT/json/versionwebSocketDebuggerUrl 等价于 connect({browserURL}) 的自动流程
重连 puppeteer.connect({browserWSEndpoint}) 注意带上与原浏览器一致的 protocol
断开后再连 disconnect() 而非 close() close() 会终结进程,端点失效

使用 wsEndpoint() 时请牢记:它返回的是纯字符串同步取值,没有任何异步开销;它反映的是 Puppeteer 当前实际连接的服务端地址,因此在"launch → 断连 → 重连"生命周期中是最可靠的连接凭据。配合 disconnect()connect()Browser 类总览,即可覆盖绝大多数分布式或长生命周期浏览器会话管理需求。

登录后查看全文
热门项目推荐
相关项目推荐