HerokuにPuppeteerの実行環境を構築する

HerokuにPuppeteerの実行環境を構築する

# はじめに 
本文は [Node.js 2 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/nodejs2) 25日目の投稿です。
またクリスマス直前で~~寂しく~~記事を書いていますが、皆さんの役に立つ情報であれば嬉しいです。

# 主要なキーワード 
## Puppeteer 
[Puppeteer](https://github.com/GoogleChrome/puppeteer)はGoogleが開発したChromeのヘッドレス版のNode.js APIです。
ややこしいと思いますが、[Phantom.js](http://phantomjs.org/)のChrome版と思っていただくとわかりやすいです。

## Heroku
[Heroku](https://www.heroku.com/)はsalesforce.com傘下のPaaS提供者であり、条件付きで無料のDocker Runtimeも提供しています。

# Herokuの上にPuppeteer実行環境を展開
Herokuは公式でも[Node.js実行環境の構築方法(英語)](https://devcenter.heroku.com/articles/getting-started-with-nodejs#introduction)を書いていますが、その環境でPuppeteerをインストールすると、様々な不具合(例えば日本語フォントが消えるとか)が生じるという報告がありました。
嬉しいことは、この問題はすでに[有志](https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack)により解決はしました。
それで早速、試してみましょう。

まず、[こちらのページ](https://devcenter.heroku.com/articles/getting-started-with-nodejs#set-up)を参考して、Herokuのアカウントにログインしましょう。

そして、アプリ用レポジトリを作りましょう。

```
$ mkdir MYAPP # ここは自分のappの名前です
$ cd MYAPP
$ git init
$ npm init -y

# Herokuプロジェクトの作成
$ heroku create MYAPP
$ git push heroku master

# メインビルドパックをNode.jsに設定
$ heroku buildpacks:set heroku/nodejs

# Puppeteer用追加パケージのインストール
$ heroku buildpacks:add https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack
```

注意点は、`puppeteer-heroku-buildpack`系のビルドパックは`heroku/nodejs`という土台が必要で、直接は使えません。

# Puppeteerのインストール
```
$ npm i puppeteer
```
上記コマンド叩くと、`node_module`に最新のChromeがインストールされますので、少々時間かかります。

# ローカルでPuppteerを動かしてみる
Googleさんのデモコードを少し変更してみました。

```puppteer_demo.js
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://news.ycombinator.com', { waitUntil: 'networkidle2' });
  const newsTitles = await page.evaluate(() => {
    const elenemts = document.querySelectorAll('.itemlist .title > a');
    return [].map.call(elenemts, el => el.innerText);
  });
  console.log(newsTitles.slice(0, 5));
  await browser.close();
})();
```

実行してみると、https://news.ycombinator.com から、トップ5の記事のタイトルが戻ってきます。

```
$ node puppteer_demo.js

[ 'The Effect of Atmospheric Nuclear Testing on American Mortality Patterns [pdf]',
  'How to Print Integers Really Fast',
  'Papercraft with Blender',
  'Karnaugh map',
  'A preview of the U.S. without pensions' ]
```

ご覧のように、Phatom.jsと比べるとモダーンな構文で非同期処理が書けます。

# PuppeteerをHerokuにデプロイ
## Koa.jsでHeroku用入り口の追加
別にKoaにこだわることはありませんが、`async/await`スタイルで統一したいのでこちらを使わせていただきます。

```
$ npm i koa koa-router koa-bodyparser
```

`index.js`を用意し、下記のようにロジックを追加します。

```index.js
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');

const app = new Koa();
const router = new Router();

router.get('/', async (ctx, next) => {
  ctx.body = 'Hello World';
});

app.use(router.routes());
app.use(router.allowedMethods());
app.use(bodyParser());
app.listen(process.env.PORT || 3000);
```

下記コマンドを実行すると、`localhost:3000`で`Hello World`が表示されます。

```
$ node index.js
```

## Puppeteer Launch Optionの修正
HerokuでPuppeteerを実行するには、少し工夫が必要です。
まず、真・ヘッドレス環境で実行する時、デフォルトのLaunch Optionだとエラーが発生します。
また、ローカルで動かす時、ヘッドレスよりも画面表示した方がデバグしやすいので、`puppteer_demo.js`の一部修正して、`index.js`に追記します。

```index.js
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');

const app = new Koa();
const router = new Router();

router.get('/', async (ctx, next) => {
  ctx.body = await crawler(); // クローラーの実行
});

app.use(router.routes());
app.use(router.allowedMethods());
app.use(bodyParser());
app.listen(process.env.PORT || 3000);

// ここからはクローラーのロジック
const puppeteer = require('puppeteer');
// Heroku環境かどうかの判断
const LAUNCH_OPTION = process.env.DYNO ? { args: ['--no-sandbox', '--disable-setuid-sandbox'] } : { headless: false };

const crawler = async () => {
  const browser = await puppeteer.launch(LAUNCH_OPTION); // Launch Optionの追加
  const page = await browser.newPage();
  await page.goto('https://news.ycombinator.com', { waitUntil: 'networkidle2' });
  const newsTitles = await page.evaluate(() => {
    const elenemts = document.querySelectorAll('.itemlist .title > a');
    return [].map.call(elenemts, el => el.innerText);
  });
  await browser.close();
  return newsTitles;
}
```

修正した`index.js`を実行すると、`localhost:3000`でタイトル一覧が(json形式で)表示されます。

## Herokuにプッシュ
Herokuの環境は`Procfile`というファイルで本番環境で実行するコマンド記載する必要があるので、MYAPP直下で作ります。

```Procfile
web: node index.js
```

そしてHerokuのプロジェクトは結局Gitのレポジトリなので、gitでプッシュすれば更新されます。

```
$ git add .
$ git commit -m 'init crawler'
$ git push heroku master
```

Herokuにプッシュするたびに、実行環境が作り直されるので、完成まで待ちます。

環境の作成が完成できたら、下記コマンドを叩くとブラウザーが開き、`https://MYAPP.herokuapp.com/`でタイトル一覧が(json形式で)表示されます。

```
$ heroku open
```
もし何か異常が出されたら、`heroku logs --tail`でログが確認できます。


# 参考
https://devcenter.heroku.com/
https://elements.heroku.com/buildpacks/jontewks/puppeteer-heroku-buildpack
https://timleland.com/headless-chrome-on-heroku/