3つのブラウザを制御する:ブラウザ抽象化レイヤー
前提知識
- ›第1・2回:アーキテクチャとプロトコルレイヤー
- ›Chrome DevTools Protocol(CDP)の基本概念
- ›TypeScript における抽象クラスパターンの理解
3つのブラウザを制御する:ブラウザ抽象化レイヤー
第1回・第2回では、ユーザーの API 呼び出しがプロトコルメッセージに変換されてサーバーへ届くまでの流れを追いました。では、メッセージがサーバーに届いた後はどうなるのでしょうか。サーバーはそのメッセージを、Chromium・Firefox・WebKit それぞれに固有のコマンドへ変換しなければなりません。3つのブラウザはリモートコントロールのプロトコルが根本的に異なります。本記事では Playwright のブラウザ抽象化レイヤーを掘り下げます。クラス階層、その中核を担う PageDelegate インターフェース、Chromium の CDP 統合、新たに加わりつつある BiDi サポート、インストゥルメンテーションシステム、そしてブラウザレジストリまでを順に見ていきましょう。
サーバーサイドのクラス階層
Playwright のサーバーサイドは、ブラウザ制御を以下のような明快な階層で整理しています。
BrowserType → Browser → BrowserContext → Page → Frame
各レベルには packages/playwright-core/src/server/ に抽象基底クラスが存在し、ブラウザ固有の実装には CR(Chromium)、FF(Firefox)、WK(WebKit)というプレフィックスが付きます。
すべてのブラウザ操作の起点となる BrowserType 基底クラスは packages/playwright-core/src/server/browserType.ts#L48-L56 で定義されています。
export abstract class BrowserType extends SdkObject {
private _name: BrowserName;
constructor(parent: SdkObject, browserName: BrowserName) {
super(parent, 'browser-type');
this.attribution.browserType = this;
this._name = browserName;
}
packages/playwright-core/src/server/browserType.ts#L66-L72 の launch() メソッドは、オプションのバリデーション、Selenium Hub のオーバーライド確認を行い、ブラウザ固有の起動ロジックへ処理を委譲します。
classDiagram
class BrowserType {
<<abstract>>
+launch()
+launchPersistentContext()
+executablePath()
}
class Chromium {
-_bidiChromium: BrowserType
}
class Firefox
class WebKit
class Browser {
<<abstract>>
+newContext()
+close()
}
class CRBrowser {
+_connection: CRConnection
+_session: CRSession
}
class Page {
+_delegate: PageDelegate
+click()
+fill()
}
BrowserType <|-- Chromium
BrowserType <|-- Firefox
BrowserType <|-- WebKit
Browser <|-- CRBrowser
Browser *-- BrowserContext
BrowserContext *-- Page
packages/playwright-core/src/server/playwright.ts#L50-L66 のサーバーサイド Playwright ルートでは、すべてのブラウザタイプをインスタンス化しています。
this.chromium = new Chromium(this, new BidiChromium(this));
this.firefox = new Firefox(this, new BidiFirefox(this));
this.webkit = new WebKit(this);
BidiChromium と BidiFirefox がコンストラクタ引数として従来の実装に渡されている点に注目してください。Playwright は既存の CDP・独自プロトコルパスに加えて、WebDriver BiDi のサポートを段階的に追加しています。
PageDelegate:抽象化の要
Playwright のブラウザ抽象化において最も重要なインターフェースが PageDelegate です。packages/playwright-core/src/server/page.ts#L55-L105 で定義されており、汎用の Page クラスがブラウザ固有の実装へ処理を委譲する接点となっています。
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
readonly rawKeyboard: input.RawKeyboard;
readonly rawTouchscreen: input.RawTouchscreen;
reload(): Promise<void>;
goBack(): Promise<boolean>;
goForward(): Promise<boolean>;
navigateFrame(frame: Frame, url: string, referrer: string | undefined): Promise<GotoResult>;
takeScreenshot(progress: Progress, format: string, ...): Promise<Buffer>;
// ... 20+ more methods
}
ブラウザ固有のページクラス(CRPage・FFPage・WKPage)はすべてこのインターフェースを実装します。基底の Page クラスは _delegate: PageDelegate への参照を保持し、ブラウザ固有の操作はすべてこれを経由して呼び出します。
このデザインの巧みさは、ブラウザ間の差異を示すコメントにも表れています。
// Work around WebKit's raf issues on Windows.
rafCountForStablePosition(): number;
// Work around Chrome's non-associated input and protocol.
inputActionEpilogue(): Promise<void>;
// Work around for asynchronously dispatched CSP errors in Firefox.
readonly cspErrorsAsynchronousForInlineScripts?: boolean;
各ブラウザには固有の癖があります。PageDelegate はそれぞれに対応するフックを提供することで、コードベース全体に条件分岐が散らばるのを防いでいます。
classDiagram
class PageDelegate {
<<interface>>
+rawMouse: RawMouse
+rawKeyboard: RawKeyboard
+reload()
+goBack()
+navigateFrame()
+takeScreenshot()
+rafCountForStablePosition()
+inputActionEpilogue()
}
class CRPage {
-_client: CRSession
+reload()
+navigateFrame()
}
class FFPage {
-_session: FFSession
+reload()
+navigateFrame()
}
class WKPage {
-_session: WKSession
+reload()
+navigateFrame()
}
PageDelegate <|.. CRPage
PageDelegate <|.. FFPage
PageDelegate <|.. WKPage
ヒント: ブラウザ固有の問題をデバッグするときは、
crPage.ts・ffPage.ts・wkPage.tsから該当するPageDelegateメソッドを探してみましょう。各ブラウザがその操作をどう処理しているかをすぐに確認できます。
Chromium:CDP とセッション多重化
Chromium の制御には Chrome DevTools Protocol(CDP)を使います。packages/playwright-core/src/server/chromium/chromium.ts#L54-L60 の Chromium クラスは BrowserType を継承し、プロセスの起動と CDP 接続のセットアップを担います。
packages/playwright-core/src/server/chromium/crBrowser.ts#L43-L57 の CRBrowser は、セッション多重化を伴う CDP 接続を管理します。
export class CRBrowser extends Browser {
readonly _connection: CRConnection;
_session: CRSession;
readonly _contexts = new Map<string, CRBrowserContext>();
_crPages = new Map<string, CRPage>();
_serviceWorkers = new Map<string, CRServiceWorker>();
各ブラウザタブは固有の CDP セッションを持ち、複数ページとの同時通信が可能です。CRConnection はこれらのセッションを単一の WebSocket トランスポート上で多重化します。
sequenceDiagram
participant PW as Playwright Server
participant Conn as CRConnection
participant Root as Root CDP Session
participant S1 as Session (Tab 1)
participant S2 as Session (Tab 2)
PW->>Conn: Connect via WebSocket
Conn->>Root: Target.getTargets()
Root-->>Conn: [page1, page2]
Conn->>S1: Target.attachToTarget(page1)
Conn->>S2: Target.attachToTarget(page2)
PW->>S1: Page.navigate(url)
PW->>S2: Runtime.evaluate(expr)
packages/playwright-core/src/server/chromium/crBrowser.ts#L59-L69 の静的メソッド CRBrowser.connect() は接続を確立し、既存のターゲットにアタッチします。注目すべき点は、Playwright が CDP を直接公開するのではなく、ラップしているところです。これにより、ブラウザ間の挙動を正規化し、CDP がネイティブに提供しない自動待機などの機能を追加できます。
BiDi:新たな選択肢
Playwright は WebDriver BiDi プロトコルのサポートも段階的に追加しています。BiDi はベンダーをまたいでブラウザ自動化を標準化することを目指したプロトコルで、CDP の思想をすべてのブラウザに広げたものとも言えます。BiDi の実装は packages/playwright-core/src/server/bidi/ に置かれています。
| ファイル | 役割 |
|---|---|
bidiChromium.ts |
Chromium 向け BiDi-over-CDP |
bidiFirefox.ts |
Firefox ネイティブ BiDi |
bidiBrowser.ts |
BiDi ブラウザ共通ロジック |
bidiPage.ts |
BiDi ページ実装 |
bidiConnection.ts |
BiDi WebSocket プロトコル |
bidiOverCdp.ts |
CDP 経由でトンネルされた BiDi |
前述のとおり、BiDi サポートはコンストラクタ引数として従来のブラウザタイプに渡されます。この設計により、既存のプロトコルを丸ごと置き換えることなく、起動時に BiDi をオプトインできます。
BidiChromium(CDP 経由でトンネルされた BiDi)と BidiFirefox(ネイティブ BiDi)の違いは、現時点のエコシステムの状況をそのまま反映しています。Chrome は CDP の上に BiDi を実装しているのに対し、Firefox はネイティブで BiDi を実装しています。
SdkObject・インストゥルメンテーション・ProgressController
サーバーサイドのすべてのオブジェクトは、packages/playwright-core/src/server/instrumentation.ts#L47-L71 で定義された SdkObject を継承しています。
export class SdkObject extends EventEmitter {
guid: string;
attribution: Attribution;
instrumentation: Instrumentation;
SdkObject が提供するのは3つです。グローバルに一意な ID(guid)、アトリビューションチェーン(そのオブジェクトが playwright → browserType → browser → context → page → frame のどこに属するか)、そして Instrumentation インターフェースへのアクセスです。
packages/playwright-core/src/server/instrumentation.ts#L80-L93 の Instrumentation インターフェースは、トレース・デバッグ・レコーダーを支えるライフサイクルフックを定義しています。
export interface Instrumentation {
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onPageOpen(page: Page): void;
onPageClose(page: Page): void;
onBrowserOpen(browser: Browser): void;
onBrowserClose(browser: Browser): void;
onDialog(dialog: Dialog): void;
onDownload(page: Page, download: Download): void;
}
packages/playwright-core/src/server/instrumentation.ts#L108-L128 の実装では Proxy を使って登録済みリスナーへ動的にディスパッチし、スコープは BrowserContext 単位で絞り込まれます。これにより、トレースレコーダーは自身がアタッチされたコンテキストのイベントだけを受け取る仕組みになっています。
packages/playwright-core/src/server/progress.ts#L26-L56 の ProgressController は、ユーザー向けのすべての操作をタイムアウト管理でラップします。第2回で見た Dispatcher._runCommand() メソッドのとおり、プロトコルコマンドごとに専用の ProgressController が割り当てられます。
export class ProgressController {
readonly metadata: CallMetadata;
private _forceAbortPromise = new ManualPromise<any>();
async run<T>(task: (progress: Progress) => Promise<T>, timeout?: number): Promise<T> {
const deadline = timeout ? monotonicTime() + timeout : 0;
// ... timeout and abort management
}
flowchart TD
A["User calls page.click()"] --> B["Dispatcher._runCommand()"]
B --> C["ProgressController.createForSdkObject()"]
C --> D["controller.run(task, timeout)"]
D --> E{"Timeout reached?"}
E -->|No| F["Execute task"]
E -->|Yes| G["Throw TimeoutError"]
F --> H{"Abort signal?"}
H -->|No| I["Return result"]
H -->|Yes| J["Reject with error"]
ヒント: Playwright のタイムアウト問題をデバッグするときは、サーバーサイドのメソッドに渡される
Progressオブジェクトのlog()関数に注目しましょう。これらのログはエラーの「Call log」セクションに出力され、タイムアウト発生時にサーバーが何をしていたかを正確に追えます。
ブラウザレジストリとカスタムビルド
packages/playwright-core/src/server/registry/index.ts#L1-L48 のブラウザレジストリは、ブラウザバイナリのダウンロードと実行パスの管理を担います。具体的には次の処理を行います。
- 現在のプラットフォームに合った実行ファイルの解決
- CDN ミラーからのブラウザダウンロード(フォールバックあり)
- カスタムブラウザビルドの管理
Playwright は複数の CDN ミラーを保持しています。
const PLAYWRIGHT_CDN_MIRRORS = [
'https://cdn.playwright.dev/dbazure/download/playwright',
'https://playwright.download.prss.microsoft.com/dbazure/download/playwright',
'https://cdn.playwright.dev',
];
Playwright のアプローチで重要なのは、Firefox と WebKit が browser_patches/ ディレクトリで管理されたカスタムパッチ済みビルドを必要とする点です。Chrome のみをサポートする Puppeteer とは異なり、Playwright は Firefox と WebKit にパッチを当て、必要な自動化 API を公開しています。playwright install で特定バージョンのブラウザがダウンロードされるのはこのためです。ダウンロードされるのは通常のリリースビルドではなく、カスタムビルドです。
次回予告
サーバーが3つのブラウザをどう抽象化しているかを理解したところで、次回はブラウザページの内側に踏み込みます。注入スクリプトのアーキテクチャ、セレクターエンジンシステム、Locator がセレクター文字列を遅延合成する仕組み、そして Playwright のインタラクションを信頼性の高いものにする自動待機のリトライロジックを詳しく解説します。