Playwrightのリリースノートに、気になる記載がある。

Release v1.4.0 · microsoft/playwright · GitHub
英語が苦手でも読めるよう、みんな大好きdeepl翻訳をかけておこう。

爆速の多言語展開スピードを支える何かがあるらしいことはわかる。
ただ、具体的にどういう仕組みで動いているとか、どうやればクライアントが実装できるよとか、そういう情報はドキュメントには一切書かれていない。 さすがマイクロソフト。 なので、いろいろソースを読んで調べた&実際にRubyからPlaywrightを使うPoCを作ってみた。

PlaywrightはServer/Clientモジュールに分かれている
結論から言うと、
- microsoft/playwright (TypeScript版)にはServerモジュールとClientモジュールの両方が含まれている
- ただ、READMEなどに書かれている動作では、サーバー・クライアント動作は特にせず、1つのプロセスで自動化が動く
- microsoft/playwright-python, microsoft/playwright-java, microsoft/playwright-sharp, mxschmitt/playwright-go はいずれも、PlaywrightのClientモジュールである
サーバーモジュールというのは何かというと、Playwright自身が特定のWebSocketまたはパイプ(標準入力/標準出力)をバインドして、そこで受けた命令をそっくりそのままChromeやFirefoxやSafariに(いい感じに変換して)投げる君。
クライアントモジュールというのは、ユーザが書くスクリプトで実際に使われるPageとかBrowserとかElementHandleとかそのへんのクラスで、サーバーに対して、WebSocketなりパイプなりで、要求を投げる君。
- https://github.com/microsoft/playwright/tree/v1.7.1/src/server
- https://github.com/microsoft/playwright/tree/v1.7.1/src/client

PythonやJavaやC#やGoのクライアントは、内部的にplaywright-cliを使ってサーバーを起動している
リリースノートに、「Nodeじゃなくても」動かせるようにした、と書いてあった部分の話。
In the last release, we introduced an internal protocol to support Playwright in the none-Node environments
タイトルでネタバレしてしまったが、それぞれのクライアントには
がある。
Pythonであれば
- cliをダウンロードする部分→ https://github.com/microsoft/playwright-python/blob/v0.171.1/setup.py
- cli run-driverする部分→ https://github.com/microsoft/playwright-python/blob/v0.171.1/playwright/_transport.py
Goであれば
- cliをダウンロード/run-driverする部分→ https://github.com/mxschmitt/playwright-go/blob/v0.171.1/run.go
試しに、手元で npx playwright-cli run-driver してみたら、なんとそれだけでPlaywrightサーバーが立ち上がるのがわかる。
ただ、playwright-pythonやplaywright-javaなどでダウンロードされているplaywright-cliはNode環境がなくても動くようにシングルバイナリ?っぽい形で配布されたもののようだ。 playwright-pythonやjavaがダウンロードしているURLは https://playwright.azureedge.net/builds/cli/next/playwright-cli-0.180.0-next.1608746109749-cbc13bd-mac.zip こんな感じのもので、実際にそこからダウンロードして中身を見てみた↓

playwright-cli run-driverはPlaywrightサーバーモジュールを起動するだけ!
playwright-cli run-driverの内部実装も一応メモっておく。
実に単純で、
// Implement driver command. if (process.argv[2] === 'run-driver') runServer(); else if (process.argv[2] === 'print-api-json') printApiJson();
playwright-cli/cli.ts at v0.171.0 · microsoft/playwright-cli · GitHub
const { Playwright } = require('playwright/lib/server/playwright');
(中略)
export function runServer() {
installDebugController();
installTracer();
const dispatcherConnection = new DispatcherConnection();
const transport = new Transport(process.stdout, process.stdin);
transport.onclose = async () => {
// Force exit after 30 seconds.
setTimeout(() => process.exit(0), 30000);
// Meanwhile, try to gracefully close all browsers.
await gracefullyCloseAll();
process.exit(0);
};
transport.onmessage = (message: string) => dispatcherConnection.dispatch(JSON.parse(message));
dispatcherConnection.onmessage = (message: string) => transport.send(JSON.stringify(message));
const playwright = new Playwright(__dirname, require('playwright/browsers.json')['browsers']);
(playwright as any).electron = new Electron();
new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright);
}
Playwrightサーバーモジュールを直接インクルードして起動し、通信路としてパイプ(stdin/stdout)を指定しているだけ。
あとは、Playwrightプロトコルをしゃべるクライアントを書けばいい
サーバーの立ち上げ方がわかったところで、あとは標準入力/標準出力を介してPlaywrightのプロトコルをしゃべるクライアントを書けばいいだけ、ということになる。
ただ、playwright-pythonもplaywright-javaもソースを見てみるとちょっと変わった作りをしていて、プロトコルのJSONをもとにAPIクライアントインターフェースを自動生成するようになっている。
npx playwright-cli print-api-json | jq .
こうすると、どっっばーーーーーっとAPIインターフェースを定義したJSONが降ってくる。playwright-pythonもplaywright-javaもこれを頑張って解析してAPIクライアントインターフェースを生成している。
わかりやすいのはPythonで、 このへんでソース生成君がいて、実際に生成されたソースは
- https://github.com/microsoft/playwright-python/blob/v0.171.1/playwright/async_api.py
- https://github.com/microsoft/playwright-python/blob/v0.171.1/playwright/sync_api.py
このあたりだ。
これとは別に、インターフェースの実装部分を、作っていけば、クライアントモジュールが出来上がる。
実際にRubyクライアント書いてみた
プロトコルJSONを読んでコード生成する部分が地味に大変だったけど、そのぶん実装部分はめっちゃ楽。なにこれ。ってかんじ。
require 'playwright'
Playwright.create(playwright_cli_executable_path: '/path/to/playwright-cli') do |playwright|
playwright.chromium.launch(headless: false) do |browser|
page = browser.new_page
page.goto('https://github.com/YusukeIwaki')
page.screenshot(path: './YusukeIwaki.png')
end
end
このくらいの簡単なスクリプトを動かすだけなら、Browser, BrowserType, Page, Frame などの主要なクラスにいくつかのメソッドを実装するだけで、動くようになる。
puppeteer-rubyのときに散々苦しんだ、CDPSessionまわりの並行処理の順序変わっちゃう問題などは一切なく、(そのへんはサーバーモジュールがやってくれるので!)本当に素直にクライアント書くだけだ。
puppeteer-ruby のほうはしばらく開発をゆるめにして、Playwrightのほうを本腰入れて作っていこうかなと思う。