--headless時代の本命? Chrome を Node.jsから操作するライブラリ puppeteer について


--headless時代の本命? Chrome を Node.jsから操作するライブラリ puppeteer について

puppeteer はHeadless Chrome をNode.jsで操作しやすくしたライブラリです。今日(※ 2017/8/17)一日で凄い勢いでGitHubのトレンド入りしており、TLでも話題になっていたので、早速触ってみました。

Node.jsでChromeを操作するというコンテキストにおいては、Nightmare.jsと同じレイヤに属するプロダクトですね。Nightmare.jsはElectronを介在させることで、Chromeの操作を実現していましたが、今年の5月にChromeでheadlessモードが利用可能になって以降1、headless Chromeを直接操作するライブラリが色々と出始めていますね。この系統は、chromyや、やはり先日GitHubでトレンド入りしていたchromelessなどが挙げられます。

puppeteerがこれらのライブラリと一線を画すのは、なんと言っても本家ChromeのDevTool開発チームが作成・メンテナンスしている、という点でしょう。

仕組み

puppeteerは、WebdriverIOのようなクロスブラウザ対応のツールとは異なり、Chrome DevTools Protocol2 を利用してNode.jsからChromeの開発者ツールへ接続して操作を行います。

触ってみる

この手のツールの定番、スクリーンショットの取得をやってみます。

まずはインストール。

npm i puppeteer

postinstallフックにより、chromiumもインストールされます。インストールされるchromiumのバージョンはpuppeteerのpackage.jsonで一意に定まるようになっているため、npmやyarnのロックファイルと併用することで、利用するchromiumのバージョンをコントロール可能です。

続いてブラウザを操作するスクリプトを書いていきます。

script.js
const fs = require('fs');
const assert = require('assert');
const puppeteer = require('puppeteer');

(async() => {

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://github.com/Quramy');
  await page.screenshot({path: 'example.png'});

  browser.close();
  assert(fs.existsSync('example.png'));
  console.log(' 🎉 ');
})();

特に説明不要ですね。GitHubのプロフィールページを表示してキャプチャを撮るスクリプトです。

下記のコマンドで動作させ、example.pngに画像が出力されていれば成功です。

node script.js

CIでの動作

実際のところ、ローカルでキャプチャを取得しても面白みもへったくれもありません。やはりCIで動かしてこそでしょう。

TravisCI

特に難しい点はありません。通常のNode.jsプロジェクトと同様にymlを書くだけで簡単に動作します。

.travis.yml
os:
- linux
language: node_js
node_js:
- '8'

script:
- node script.js

WebDriverやNightmare.jsの場合は仮想フレームバッファの設定など、ブラウザをCIで動作させるための特殊な設定が必要なのですが、それすらも不要なのは気分がいいですね。

WerckerCI

折角なので、DockerベースのCIでも試してみました。普段から使っているという理由でOracle WerckerCIを選択。
Node.js公式のDocker imageを利用したところ、

(node:227) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Failed to connect to chrome!
(node:227) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
npm info lifecycle [email protected]~posttest: [email protected]

というエラーが表示されてしまっため、多少のworkaroundを施しています3。どうやら、Debian系のOSで動作させる場合には、追加でlibxcb関連をインストールした上で、SUID Sandboxを無効化4する必要があるらしいです。

wercker.yml
box: node:8

build:
  steps:
    - script:
        name: Workaround for GoogleChrome/puppeteer#290
        code: sh ./fix_290.sh
    - npm-install
    - npm-test
fix_290.sh
#!/bin/bash

apt-get update
apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
  libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
  libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
  libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
  ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
script.js(変更部分)
  // argsを明示
  const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });

とすることで、動作しました。Travisに比べると少しややこしいですね...。
真面目にやるのであれば、上記の依存関係を焼き付けたimageを作ってしまった方が良さそうな気もします5

Werckerだけでなく、CircleCI 2.xのようなDocker baseのCIでpuppeteerを動作させる際は同様の考慮が必要になりそうなので、何かの助けになれば幸いです。

なお、動作検証に利用したレポジトリは https://github.com/Quramy/puppeteer-example です。参考まで。

おわりに

API一覧をざっと眺めてみると、Nightmare.jsで実現できていたことはほぼ全てpuppeteerでも実現できそうな印象です。

スクリプトを送り込むinjectFileや、ブラウザ上でscriptを評価するevaluate、そして、Node.js上で定義した関数をブラウザ上に公開する exposeFunctionあたりを駆使すればほぼ何でも出来るんじゃないかこれは、という予感です。

飽くまでChrome専用の操作には限定されますが、e2eテスト環境であったり、お手軽なDOM環境が欲しいときには重宝しそうです。

僕のチームでは、開発しているWebアプリケーションについては、ブラウザで動作させて画像のキャプチャを取得し、前回との差分を検知することで回帰テストを回しており、これに特化した reg-suit というツールを作成したりしています。
このテストフローについては、別途まとめた記事にしたいと思っていますが、最終的には「如何にCIでキャプチャ画像を取得するか」という点に難しさが収束していく感があり、puppeteerのようなブラウザ操作ツールを上手く活用できる色々と捗りそう。

それでは、また。