たゆたふ。

定まる所なく揺れ動き、いろいろやってみたメモ。など

GAS を Webpack + Babel でやってみた

久々に、Google Sheets の Add-on で欲しいものができたので作った。 良い機会なので、Webpack + Babel を使ってビルドする構成を試してみた。 また、Google Apps Script (GAS)周りでもいくつか変化があったので取り入れてみた。

その記録。

作ったもの

会社で開発に使った時間を資産管理の関係で申請する必要があるのだが、Redmine のチケットに時間を記録すれば OK となっている。 一方、私は Togglで日々の時間管理をしている。

チケット終了ごとに Redmine に登録したり、締め日に Toggl を見ながら手作業で Redmine に登録したりしてみたのだけど、どちらも面倒でやってられなかった。 やはり、普段の時間管理は Toggl だけにしたい。 ということで、Toggl から Redmine にデータ移行できるツールを作ることにした1

コマンドラインツールでも良かったのだけど、Redmine に登録する前にちょっと確認したり、調整したりできたほうが便利かと考えた。 となると Google Sheets に一旦書き出して、それを登録できるものがよかろう、それならば Add-on として実装するのが最も便利に使えそうだと考えた。

それで作ったのは toggl2rm 。 次の機能を備えている。

以下に、toggl2rmを作る過程でキーとなった部分や新たに調べたことを記述する。

Webpack + Babelの設定

ざっと、Toggl と RedmineAPI を叩く部分を GAS の Web エディタで検証がてら実装した後、gapps コマンド(node-google-apps-script)を使って、ローカルにソースを持ってきて、Webkit + Babel でビルドする設定した。

webpack.config.babel.jsは次の通り設定した。

import GasPlugin from 'gas-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import path from 'path';

export default {
  context: path.resolve(__dirname, 'src'),
  entry: {
    code: './code',
    props: './props',
    utils: './utils',
  },
  output: {
    path: path.resolve(__dirname, 'dest'),
    filename: '[name].js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      {
        test: /\.json$/,
        exclude: /node_modules/,
        use: {
          loader: 'file-loader',
          options: {
            name: './[name].[ext]',
          },
        },
      },
    ],
  },
  plugins: [
    new GasPlugin(),
    new HtmlWebpackPlugin({
      filename: 'sidebar.html',
      template: 'sidebar.html',
      inject: false,
    }),
    new HtmlWebpackPlugin({
      filename: 'setting_dialog.html',
      template: 'setting_dialog.html',
      inject: false,
    }),
  ],
};

.babelrcは次の通り。

{
  "presets": ["env", "gas"]
}

この設定では次のプラグインを利用している。

gas-webpack-pluginbabel-preset-gas で ES6 な JS をいい感じの GAS 向けコードにトランスパイルしてくれる。

html-webpack-plugin は結局 srcディレクトリからdestディレクトリに HTML ファイルをコピーするのにしか使っていない。 本当は HTML に含まれる CSS と JS を別ファイルで実装し、ビルドによって 1 つの HTML にまとめたかった。 html-webpack-pluginプラグインを更にインストールしていろいろ試したが、どうもうまくいかなかった。 最初からdestディレクトリに HTML だけ置いたり、Webpack 使わずコピーしても良かったのだけど、ソースとビルド成果物をはっきり分けたい+設定はwebpack.config.babel.jsに集約したいのでこうした。

また、JS を 1 つのファイルにビルドしてまとめても良かったのだけど、なんとなく大きな 1 ファイルを作りたくないなぁと思って、3 つに分けた。 しかし、1つにしたほうが、グローバルに晒したくない関数を隠せるので今後見直すかもしれない。

ESLint

ESLintも導入してみた。 ベースの設定は eslint-config-airbnb-baseを利用した。 これだけだと GAS 特有のオブジェクトの参照でエラーが出るので、それはeslint-plugin-googleappsscriptで解決した。

自分で実装したグローバルオブジェクトと console.error() 等のログ出力系(後述の Stackdriver Logging のため)を個別に許可設定した。

私は Atom エディタを使っているので、エディタの Linter が実装時にリアルタイムでチェックしてくれ、便利だった。

上記を設定したのものはこんな感じ。

extends: airbnb-base

plugins:
  - googleappsscript

env:
  googleappsscript/googleappsscript: true

globals:
  Props: false
  Utils: false

rules:
  no-console: ["error", { allow: ["info", "warn", "error"] }]

clasp

最初は gapps コマンド(node-google-apps-script)を使っていた。 しかし、複数の GAS のプロジェクトがある場合、個別に Google Drive API の認証が必要なのにその認証情報の管理は~/.gapps 1 ファイルで行われ、切り替えるのが不便だった。

汎用的な不満だと思うので認証情報ファイルをコマンド実行時に指定する方法があるのではと、リポジトリを久々に見るとディスコンしていた。 代わりに Google 謹製の google/claspを使えと書かれていた。

clasp を調べてみて gapps と比べ以下が優れていると思ったので乗り換えた。

  • GAS プロジェクト毎の認証設定が不要(1 回のログインですべての GAS プロジェクトの管理が可能)
  • GAS スクリプトの作成も可能(gapps は先に GAS スクリプトを作っておく必要がある)
  • ドキュメントに紐づくスクリプトの管理も可能(gapps では不可)
  • Google 謹製という安心感

使い方はこちらを参照 => GAS のGoogle謹製CLIツール clasp - Qiita

Stackdriver Logging

確か以前は console.log() は GAS では使えなかったと思うのだが、今は使える。 GAS はサーバサイドで実行されるスクリプトなので、ブラウザの Dev Tool には何も出力されない。 どこに出力されるかというと、Stackdriver という Google Cloud 上のログサービスに出力される。

どうやら、去年の夏頃こうなったようだ3。 以前から GAS にちゃんとしたロギングがほしいと思っていた。 Logger.log() では最後の実行分しか見られないので、どうしても必要な場合にはスプレッドシートに書いたりしたものだ。 しかし、これからは Stackdriver Logging が使える。 DEBUG、INFO、WARN、ERROR という一般的なログレベルを設定して出力できるのも嬉しい。

Stackdriver は無料のサービスではないが、毎月無料枠があり、個人ユースではそれで足りるのではないかと思う。 今回はエラーログの記録に利用している。

ざっと調べてみた結果はこちら => GAS でStackdriver Loggingを使う - Qiita

最後に

久々に GAS での開発をやってみて、自分なりのモダンな開発構成を確立できたと思う。 gas-webpack-plugin が Webpack4 に対応できていないのかどうもうまくトランスパイルできなかったので、実は今回は Webpack3 を使っている。 そのうち対応されればアップデートしようと思う。

しばらく見ないうちに GAS を取り巻く環境も色々変わっていた。 以前は自分にだけ自作 Add-on をインストールする方法があったのだが、それがなくっている。代わりにテスト設定を作成してそこから起動すれば動かせる様に変更されようだ。少々めんどくさくなった。 その一方で、謹製 CLI ツールが用意されたり、ログ周りが強化されていた。
更に、GAS 専用のマネジメントコンソールもできた。Google Drive では他のドキュメントとごちゃまぜの管理だけど、こちらだとドキュメントに埋め込まれた GAS も含め、すべての GAS のみを管理できる。 まだ、あまり調べていないが、バージョン管理と公開周りも開発版と公開版を別々に動かせるようにするためにちょっと変わったようだ。 このあたりの変更は Chrome アプリのプラットフォームとして GAS を使っていくためであろうか?

何れにせよ、以前より開発しやすくなったと思う。 便利なサーバレス環境なので今後も活用していこうと思う。


  1. 個人的には全員で Toggl に移行したほうが効率的なのではと思うものの、今、回っている仕組みを私の我儘トリガーで変更するほどでもないし、Toggl も有料プランが必要になりそうで費用もいくらか発生してしまうので個人的に解決する道を選択した。

  2. CSV でダウンロードする機能を Toggl は提供している。しかし有料プランの機能。同等のデータは API を使えば取得でき、それを利用している。無料で有料機能相当を提供していることに気を使ってライセンスは CC-BY-NC-4.0 としている。

  3. G Suite Developers Blog: Stackdriver Logging for Google Apps Script is now available

Severlessをやってみた

AWS でちょっとした処理を実行するのに Lambdaはとても便利。

これまで、ちょっとしたことを Lambda で実装したことはあったのだが、 ちょっとしたこと以上のコードを AWS コンソール上で書くのはなかなかつらい。

そこで、Serverless Frameworkを使ってみようと思った。

当初、Serverless を使うメリットとして考えていたのは次のとおり。

  • ローカルの使い慣れたエディタで Lambda を実装できる。
  • Webpack + Babel プラグインを使えば、モダンな JavaScript の機能を使える。
  • デプロイも楽になりそう

試しに簡単なアプリケーションを書いてみた。

サンプルアプリのシナリオ

試してみる課題シナリオは次のとおりとした。

  1. Github にプルリクを作ったり、プルリクにプッシュすると CodeBuild でビルドを実行する。
  2. プルリクには Github の Status API で状態を通知する
  3. ビルドの開始を Slack にも通知する
  4. CodeBuild の処理終了を CloudWatch Event で受け、その結果を Github の Status API で更新する
  5. 結果を Slack にも通知する。

作ってみたコードはこちら => HeRoMo/ServerlessSample: My First Serverless Application Sample

やってみて

やってみてどうだったのか?感想を以下に述べる。

関連リソースも一緒に定義可能

予備知識少なめで取り掛かったのでドキュメントを読みながら進めた。 最初はトリガーとなる Amazon SNS 等は別に登録して用意しないといけないのかな? と思っていたのだが、 SNS を始め、API Gateway など、Lambda をトリガーできるリソースは serverless.yml の関数定義で一緒に定義できる。 これはとても便利。 また、Lambda から他の AWS のリソース・サービスにアクセスするのに必要な権限も serverless.yml で Lambda の実行ロールに追加できる。 とにかく、Lambda の実行に必要なものが serverless.yml で一元管理できるのが管理しやすくてよかった。 最初にデプロイした時、裏で Cloud Formation のスタックが作成、実行されてちょっと驚いたが、こういうことなら納得。 Cloud Formation の定義記法で、Lambda とは直接やり取りしない設定も定義できそうなので Cloud Formation も覚えねばと思った。

やはり async/await は便利

Webpack プラグインと babel を追加して、ES2015 から Node6.10 相当にトランスパイルするように設定した。 Lambda を使う動機としては AWS のサービス間の連携を開発するためのグルーコードとして便利というのもあると思う。 実際、AWSSDK は組み込みで動作するので適切な権限が Lambda の実行ロールに付与されていれば、別サービスにアクセスするのは それほど難しくない。 しかし、AWS の実態は HTTP ベースの API なので、呼び出しはどうしても非同期となる。非同期を多用する必要がある場合には もう、async/await が使えないと辛い体になってしまっているので、これは助かった。 AWS SDK の関数は現状、コールバック式なので、早く Promise を返すようになればと思う。

全般的な感想

前述したメリットはすべて享受できたと思う。 私は JS を書くときには Atom エディタを使うことが多いが、やはり使い慣れたエディタだと実装しやすい。 webpack でビルドできると、babel でトラスアイルできるだけでなく、自由に node モジュールを使うこともできる。 そのため Github API や Slack API のライブラリを利用でき、その点でも効率的に実装できた。 デプロイも sls deploy ワン・コマンドで済むのはとても楽だった。

テンプレートも作った

今後別のアプリケーションを実装するのに便利なように、webpack+babel を設定したアプリケーションの雛形を作った。 Serverless には Serverless アプリを GitHub から取ってきてそれをひな形に別のアプリを作成する機能が備わっている。 そこもよく考えられているなぁと感心した。

テンプレートはこちら。=> HeRoMo/sls-template: Serverless Framework application template

サイトをチェックするツールを作ってみた

ボランティアで防災・減災関連の web サイトの情報を収集している。そのようなサイトは災害時にわっと作られるが、その後、閉鎖されたり、メンテナンスされなくなってしまうサイトも少なくない。同様な地方自治体のホームページも少なからずある。

時々そのようなサイトの状況をチェックするのに便利なツールがなかったので自分で作ることにした。

最初は HTTP でアクセスしてレスポンスをチェックするだけと思っていたので curl 等を使ってちょっとしたスクリプトで済ませようと思った。だた実際に確認してみると個人の作った Web サイトは思ったより続けられていないことが分かった。 同じドメインが全く違うサイトに変わっていたり、閉鎖されて Not Found ページが返ってくるのにレスポンスコードは 200 だったり。ドメインも管理者も変わってなさそうだけど、内容が全く変わっているものもあった。

また、文章ではなく画像に情報が書いてあったり、地図等の外部コンテンツを埋め込んでいるサイトも多く、レスポンスのテキストにキーワードが含まれるかどうかだけではどうも判別が難しい。ということで、チェックは目視ということになるが、いちいち URL にアクセスしていチェックしなくても良いようにスクリーンショットをとる機能を実装することにした。

その他、必要な機能を検討し、以下のようなツールを作ることとした。

  • URL が有効かどうか実際にアクセスして確認する
  • その際、リダイレクトにも追従できる
  • レスポンスのステータスコードが 200 ならスクリーンショットを取る
  • 単一の URL でなく、URL のリストを読ませて複数のサイトを一度にチェックできる
  • 結果を再利用しやすい形で出力する

最初は Phantom.js を使って作った

HTTP のリクエスト/レスポンスだけなら色々方法はあるが、スクリーンショットを取るとなると使えるツールが限られてくる。 実行中にブラウザがぱたぱた動くのも鬱陶しいし、できれば GUI を持たないサーバで動かせるようにしたい。 となると、Phantom.js を使うのが最も手軽かと思い、最初のプロトタイプを作った。 実際には、Phantom.js を使いやすくラップしてくれる CasperJS を利用した。

最低限、動くものを作って、さあ、使いやすくブラッシュアップしていこうかなと思った矢先、Chrome 59 にて ヘッドレスブラウジング がサポートされた。それに伴い、Phantom のメンテナがやる気を失ったらしい(参考:Phantom.jsのメンテナー、プロジェクトの将来に疑問を呈し、その座を降りる

ということで、PhantomJS をインストールしなくても使えるような実装のほうが良いし、いまやシェアトップとなった Chrome を使うほうが、スクリーンショットの結果も普段使っているブラウザとを乖離せず良かろうとこの実装は捨てることにした。

ヘッドレスChromeへの変更

Chrome をプログラムから操るには、DevTool のリモートプロトコルを使えば良いようで、そのためのライブラリchrome-remote-interface を利用して実装を変更した。 このライブラリ、Chrome DevTools Protocolの機能をほぼカバーしているけれど少々プリミティブ過ぎて、使いづらかった。

例えば、通信が1つづつハンドリングできる。ページのレスポンスコードを取るのにページからリンクされる画像や CSS その他へのリクエストの中からドキュメントへのリクエストを探して得る必要がある。それぞれリクエスト ID が取れるのでその ID で再度結果を取得する必要があるとか、とにかく煩雑でやりたいことに対して実装しないといけないことが多かった。おまけに、Chrome そのものを起動する機能は持っていないので、GoogleChrome/lighthouseを使って Chrome の起動を実装する必要があった。

このまま実装を続けても、この先の機能追加とか辛そうと思っていた矢先、GoogleChrome/puppeteer を知った。

puppeteer に切り替え

puppeteer だと、次のようにほんの数行書くだけで、Chrome をヘッドレスで起動して、URL にアクセスしてスクリーンショットを撮ってブラウザを終了してくれる。

// puppeteer でスクリーンショットのサンプル
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  const response = await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});
  console.log(response.status)

  browser.close();
})();

同じことを chrome-remote-interface を利用すると次のようになる。スクリーンショットの保存も自前で、しかもこれには Chrome の起動/終了が含まれていない。

// chrome-remote-interfaceでのスクリーンショットのサンプル。
// Chromeの起動/終了は別途必要。
const CDP = require('chrome-remote-interface');
const fs = require('fs');

CDP(async (client) => {
    const {Page} = client;
    try {
        await Page.enable();
        await Page.navigate({url: 'https://example.com'});
        await Page.loadEventFired();
        const {data} = await Page.captureScreenshot();
        fs.writeFileSync('scrot.png', Buffer.from(data, 'base64'));
    } catch (err) {
        console.error(err);
    } finally {
        await client.close();
    }
}).on('error', (err) => {
    console.error(err);
});

また、puppeteer だと、レスポンスコードも簡単に取れるが、chrome-remote-interface だと更に Network クラスを使って Page.navigate のレスポンスコードを探して取り出す必要がある。

これだけ比べても、puppeteer に乗り換えるメリットを感じたし、Chrome チーム謹製というのも安心感があった。 それで、再度実装を変更した。

site-checker として公開

で、作ったツールをこの度、公開した。

我ながらセンスのないベタなネーミングに嫌気がさすが、これというのが思いつかなかったし、npm ライブラリとしても使われていない名前だったので、この名前にした。

次のコマンドでインストールできる。

$ npm install -g site-checker

インストールしたら、次のコマンドで、スクリーンショットが取れる。

$ site-checker -u http://hero.hatenablog.jp

できることは次の通り。

特に、フルページとエミュレーションは puppeteer に切り替えてとても簡単に実装できた。

詳しい使い方はこちらを参照 => 使い方 · HeRoMo/site-checker Wiki