WebDriver BiDi: 新しいプロトコルとCDPブリッジ
前提知識
- ›第1〜3回の記事
- ›WebDriver BiDi仕様の基本的な理解
WebDriver BiDi: 新しいプロトコルとCDPブリッジ
WebDriver BiDiは、ブラウザ自動化プロトコルの未来です。オリジナルのWebDriver仕様を継承する双方向・イベント駆動型の後継として、CDPに近い機能をクロスブラウザの標準にもたらすことを目指して設計されています。PuppeteerのBiDi実装は、その機能だけでなく構造的な面でも注目に値します。仕様の関心事、ライブラリの関心事、互換性の関心事をそれぞれ明確に分離した、意図的な3層アーキテクチャを採用しているのです。
第3回では、PuppeteerがCDP経由でChromeを自動化する仕組みを解説しました。この記事では代替となるパスと、両者をつなぐブリッジについて説明します。
BiDiコアレイヤー:仕様に忠実なオブジェクトモデル
packages/puppeteer-core/src/bidi/core/ の中には、WebDriver BiDi仕様に厳密に従ったレイヤーが存在します。このレイヤーはPuppeteerのAPIニーズではなく、仕様そのものの形に沿って設計されています。
コアオブジェクトはBiDi仕様を反映した階層を構成しています:
classDiagram
class Session {
+connection: Connection
+browser: Browser
+send(method, params)
+subscribe(events)
}
class Browser {
+session: Session
+userContexts: UserContext[]
+addPreloadScript()
+close()
}
class UserContext {
+browser: Browser
+browsingContexts: BrowsingContext[]
+createBrowsingContext()
}
class BrowsingContext {
+userContext: UserContext
+navigate()
+reload()
+captureScreenshot()
}
class Realm {
+browsingContext: BrowsingContext
+evaluate()
+callFunction()
}
class Navigation {
+browsingContext: BrowsingContext
+request: Request
}
Session --> Browser
Browser --> UserContext
UserContext --> BrowsingContext
BrowsingContext --> Realm
BrowsingContext --> Navigation
Session クラスはトップレベルのエントリーポイントです:
packages/puppeteer-core/src/bidi/core/Session.ts#L23-L52
44行目の connection アクセサに付いている @bubble() デコレータに注目してください。これにより、connectionが発行するすべてのイベントがsessionを通じてバブルアップします。このデコレータパターンについては第6回で詳しく解説します。
Session.from() ファクトリメソッドは、session.new BiDiコマンドをケイパビリティ要件とともに送信し、Browser コアオブジェクトを初期化します。この設計により、コアレイヤーは理論上Puppeteerから独立して使用できます。
BrowsingContextとNavigationモデル
BrowsingContext はCDPの「target」に相当するBiDiの概念で、タブやフレームを表します。ただしBiDiのモデルはより豊かです。navigationが第一級オブジェクトとして扱われます。
packages/puppeteer-core/src/bidi/core/BrowsingContext.ts#L7-L25
Navigation クラスは、BiDiで規定されたナビゲーションの完全なライフサイクルをモデル化しています:
packages/puppeteer-core/src/bidi/core/Navigation.ts#L25-L50
Navigationは browsingContext.navigationStarted、browsingContext.domContentLoaded、browsingContext.load などのBiDiイベントを監視します。さらに browsingContext.navigationFailed や browsingContext.fragmentNavigated も対象です。
#matches() メソッドは巧妙な遅延マッチング戦略を実装しています。最初に一致したナビゲーションIDがそのNavigationのIDとして採用されます。この仕組みにより、作成時点ではIDが不明であるというレースコンディションを解決しています。
packages/puppeteer-core/src/bidi/core/Navigation.ts#L99-L149
ヒント:
core/をその他のbidi/から分離することには、デバッグ上の実践的なメリットがあります。BiDiの機能が動作しない場合、問題が仕様レイヤー(イベントの誤り、ステートマシンの誤り)にあるのか、アダプターレイヤー(PuppeteerのAPIへのマッピングの誤り)にあるのかをすばやく切り分けられます。この分離は自身のプロジェクトにも取り入れる価値があります。
Puppeteerアダプターレイヤー
コアレイヤーの上に位置するのがアダプター、すなわち BidiBrowser、BidiPage、BidiFrame です。ここでBiDiの概念がPuppeteerの抽象APIにマッピングされます。BrowsingContext が Page に、Realm が evaluate() の実行コンテキストに、BiDiイベントがPuppeteerイベントに変換される場所です。
BidiBrowser はコアの Session と Browser をラップします:
packages/puppeteer-core/src/bidi/Browser.ts#L58-L59
BidiPage はコアの BrowsingContext をラップします:
packages/puppeteer-core/src/bidi/Page.ts#L28-L32
概念のマッピングは次のとおりです:
| BiDi概念 | Puppeteer概念 |
|---|---|
| Session | (内部管理。BidiBrowserが管理) |
| UserContext | BrowserContext |
| BrowsingContext | PageまたはFrame |
| Realm (WindowRealm) | IsolatedWorld / 実行コンテキスト |
| Navigation | Frame.goto()の戻り値の一部 |
| Request/Response | HTTPRequest/HTTPResponse |
classDiagram
class BidiBrowser {
-session: Session (core)
-browser: Browser (core)
+protocol = 'webDriverBiDi'
+newPage()
+close()
}
class BidiPage {
-browsingContext: BrowsingContext (core)
+goto()
+evaluate()
+screenshot()
}
class BidiFrame {
-browsingContext: BrowsingContext (core)
+evaluate()
+waitForSelector()
}
BidiBrowser --|> Browser : extends abstract
BidiPage --|> Page : extends abstract
BidiFrame --|> Frame : extends abstract
BidiBrowser --> BidiPage : creates
BidiPage --> BidiFrame : contains
シリアライゼーション:JavaScriptとBiDiの橋渡し
CDPとBiDiの最も具体的な違いの一つは、JavaScriptの値がプロトコル境界を越える方法です。CDPは複雑な値に対してオブジェクトIDを持つ「リモートオブジェクト」を使用します。一方BiDiは、配列、マップ、セット、日付、正規表現などをインラインで表現できるリッチなシリアライゼーション形式を採用しています。
BidiSerializer はJavaScript → BiDi変換を担います:
packages/puppeteer-core/src/bidi/Serializer.ts#L19-L40
BidiDeserializer はBiDi → JavaScript変換を担います:
packages/puppeteer-core/src/bidi/Deserializer.ts#L14-L40
デシリアライザーはBiDiの型付き値として array、set、object、map、regexp、date、node、window、そしてプリミティブを処理します。CDPのアプローチよりも概念的にすっきりしている一方で、すべての値がシリアライゼーション境界を通過することになります。効率的に受け渡せる「リモートオブジェクト参照」は存在しません(ただしBiDiにはDOMノード用の handle 参照があります)。
flowchart LR
subgraph "Node.js"
A["JavaScript value"] --> B["BidiSerializer.serialize()"]
end
B -->|"BiDi LocalValue"| C["WebDriver BiDi Protocol"]
C -->|"BiDi RemoteValue"| D["BidiDeserializer.deserialize()"]
subgraph "Node.js (return)"
D --> E["JavaScript value"]
end
BiDi-over-CDPブリッジ
アーキテクチャ上最も興味深い部分が、BiDi-over-CDPブリッジです。CDPの対応は成熟している一方でネイティブのBiDiサポートがまだ発展途上にあるChromeに対しても、これによってPuppeteerはBiDi APIを利用できます。
packages/puppeteer-core/src/bidi/BidiOverCdp.ts#L25-L64
データフローは非常に巧妙です:
sequenceDiagram
participant Puppeteer as Puppeteer (Node.js)
participant BidiConn as BidiConnection
participant NoOp as NoOpTransport
participant BidiMapper as BidiMapper (in-process)
participant CdpAdapter as CdpConnectionAdapter
participant CdpConn as CDP Connection
participant Chrome
Puppeteer->>BidiConn: page.goto(url)
BidiConn->>NoOp: send(BiDi JSON)
NoOp->>BidiMapper: emitMessage(parsed)
BidiMapper->>CdpAdapter: sendCommand('Page.navigate', ...)
CdpAdapter->>CdpConn: send('Page.navigate', ...)
CdpConn->>Chrome: CDP message
Chrome-->>CdpConn: CDP response
CdpConn-->>CdpAdapter: response
CdpAdapter-->>BidiMapper: response
BidiMapper-->>NoOp: sendMessage(BiDi response)
NoOp-->>BidiConn: onmessage(JSON)
BidiConn-->>Puppeteer: result
ブリッジは内部的に3つのパーツで構成されています:
-
NoOpTransport(179〜208行目):BidiMapperの出力をPuppeteerのBidiConnectionに接続するためだけに存在するtransportです。BidiMapperがレスポンスを生成するとsendMessage()が呼ばれ、bidiResponseイベントが発行されます。そのイベントが捕捉され、BidiConnectionのonmessageに転送されます。 -
CdpConnectionAdapter(70〜107行目):PuppeteerのCDP Connectionをラップし、chromium-bidiのBidiMapperが期待するインターフェースを満たします。セッションごとのCDPクライアントアダプターを管理します。 -
CDPClientAdapter(115〜172行目):個々のCDPセッションをラップし、PuppeteerのCDPSessionからBidiMapperのイベントシステムへイベントを転送します。
55行目の BidiMapper.BidiServer.createAndStart() 呼び出しで、chromium-bidi(BiDi-to-CDPトランスレーターのリファレンス実装)がプロセス内で起動します。これはブラウザが将来ネイティブに実装するのと同じコードベースです。
CDPとBiDiの機能比較
Puppeteerの現在の進化段階では、2つのプロトコルバックエンドは異なる機能プロファイルを持っています:
| 機能 | CDP | BiDi | 備考 |
|---|---|---|---|
| ページナビゲーション | ✅ 完全 | ✅ 完全 | BiDiはよりクリーンなナビゲーションモデルを持つ |
| JavaScript評価 | ✅ 完全 | ✅ 完全 | BiDiのシリアライゼーションはより豊富 |
| ネットワークインターセプト | ✅ 完全 | ✅ 完全 | 内部的には異なるAPI |
| スクリーンショット | ✅ 完全 | ✅ 完全 | |
| PDF生成 | ✅ 完全 | ✅ ネイティブ | BiDiはネイティブのbrowsingContext.printを持つ |
| デバイスエミュレーション | ✅ 完全 | ⚠️ CDPフォールバック経由 | goog:cdp拡張またはCDP接続を使用 |
| コードカバレッジ | ✅ 完全 | ⚠️ CDP経由 | CDP Profilerドメインを使用 |
| Firefoxサポート | ❌ 削除済み | ✅ 必須 | FirefoxはBiDiのみサポート |
BidiPage のソースを見ると、BiDiがCDPにフォールバックしている箇所がわかります。../cdp/ からのインポートを探してみましょう:
packages/puppeteer-core/src/bidi/Page.ts#L33-L39
Coverage と EmulationManager はCDP実装から直接インポートされています。CDP接続が利用可能な場合(ChromeをBiDi-over-CDPで実行している場合)、BidiPageはこれらを再利用します。
BiDi接続は goog:cdp 拡張もサポートしており、goog:cdp.sendCommand や goog:cdp.getSession といったカスタムコマンドを提供します:
packages/puppeteer-core/src/bidi/Connection.ts#L34-L47
このChrome固有の拡張はエスケープハッチを提供します。BiDiがネイティブに機能をサポートしていない場合、PuppeteerはBiDi接続を通じてCDPコマンドをトンネリングできます。
なぜデュアルプロトコルなのか?
デュアルプロトコル戦略は、Puppeteerの長期的なビジョンを反映しています。CDPはChrome固有であり、標準化トラックには乗っていません。BiDiはW3Cの仕様であり、Firefoxはすでにネイティブにサポートし、Chromeも実装を進めています。第1回で見たように、同一の抽象APIを通じて両プロトコルをサポートすることで、PuppeteerはCDPからBiDiへと標準の成熟に合わせて段階的に移行できるクロスブラウザ自動化ライブラリとして自身を位置づけています。
BiDi-over-CDPブリッジは過渡的な技術です。ChromeのネイティブなBiDiサポートが追いつくまでの間、ユーザーは今日からBiDiのAPIを利用できます。PUPPETEER_WEBDRIVER_BIDI_ONLY フラグにより、チームやユーザーはBiDiだけで動作するものと、まだCDPフォールバックが必要なものをテストで切り分けられます。
次回予告
これで両プロトコルバックエンドを解説しました。次回はプロトコルからユーザー向けの機能へとテーマを移し、Puppeteerがページ上の要素をどのように検索するかを探ります。P-selectorパーサー、ブラウザページ内で動作するinjected scriptランタイム、そしてRxJS observableをベースに構築されたモダンなLocator APIについて解説します。