Google Apps Scriptの開発をモダンに行う方法

株式会社Speeeの山本です。皆様、こんにちは!

今回ご紹介させていただくのは、Speeeで実践しているGoogle Apps Script(以下 GAS) を用いたモダンな開発手法についてです。この記事を通してGASって「便利だし使えるかも!」と思っていただければ幸いです。

Google Apps Scriptとは

Google Apps Scriptは、言わずと知れたGoogleが提供するサーバサイドのスクリプト環境です。 基本的にはWebブラウザを通して開発を行います。 f:id:bino98ty:20160428185259p:plain

作業効率化に威力を発揮するGAS

Speeeでは特に、管理部門の作業効率化でGASを使用しているケースが多く、例えば

  • Slackの制限付きユーザを各種チャネルに招待するアプリケーション
  • メーリングリストの文面生成を自動化するアプリケーション
  • Speeeラウンジの使用状況を閲覧するアプリケーション

など、様々な用途でGASが使用されております。 また、GASはGoogle ドキュメントやGoogle スプレッドシートなどへの強力なAPIが用意されており、Google Driveを日常的に使用している環境では、特にGASを利用する利便性を見込めるのではないかと思います。

こんなに便利なGASですが、単なる作業効率化ツールとしての枠を超え、1つのアプリケーション開発に、GASを使用するケースも登場します。

開発事例の紹介

経理業務支援のアプリケーション開発にGASを採用

現在私は経理業務支援のアプリケーションを開発しているのですが、メイン機能はGoogle スプレッドシートとGASのみで開発しています。 経理業務支援のアプリケーションでは、 - クライアント様への売上/請求の一覧化 - 入金情報の管理 - 請求書発行と送付 という業務の自動化や効率化を行っています。

秘匿性の高い情報をGoogle スプレッドシートに預けることに対しての不安に思うかもしれませんが、 Google Appsが持つ強力なユーザ管理機能や、端末<->サーバ間の通信のセキュリティ対策を十分に行っているGoogle Appsを利用する方が、 オンプレでサービスを設置したり、他のクラウドサービスへホスティングしたりするより、信頼性が高いと思っております。

技術選定の場に於いて、下記のような理由でGoogle スプレッドシートとGASに利用が決定しました。

  • ユーザインターフェースを作成する工数の大幅カットが見込める。
  • Google スプレッドシートは 経理担当者 が直接システムを変更する余地がある。
    • (経理業務ドメインは難解のため、エンジニアと現場の方と共同で開発していきたかった)
  • 経理の方にとってGoogle スプレッドシートはExcelに似ているため新システムの学習コストが低い。

すべてWebブラウザで完結できることは上記のような利点もありますし、良いことのように思えます。 しかしエンジニアとしては、自分の好きなエディタを使って開発をしたいし、バージョン管理したいし、テストも書きたいし…と思うわけです。

Google Apps Scriptの開発をモダンに行う方法

Google Apps Scriptをローカル環境で…

今年の1月のことです。Googleのデベロッパーブログに下記の記事が公開されました。

コマンドラインから Apps Script プロジェクトをアップデートするためのコマンドラインインターフェース(CLI) が公開したとのこと。早速導入してみました。

使い方

本CLIを利用するためには、下記が必要です。必要に応じてインストールしておいてください。

  • node v0.12.x 以上

早速インストールして使ってみましょう。

$ npm install -g node-google-apps-script

Google の Developers Console から gappsで使うための OAuth2、その他のアプリケーション としてキーを発行します。詳しくはnode-google-apps-scirptのGitHubを見ていただければと思います。 https://github.com/danthareja/node-google-apps-script#11-default-apps-script-developer-console-project

無事に発行ができたら、キー情報が含まれたJSONを落としてください。あとは、インストールしたてのgappsコマンドを用い、認証をします。

$ gapps auth ./client_secret_<client_secret_key>.json

上記コマンドを打つと、

Please visit the following url in your browser (you'll only have to do this once): https://accounts.google...

などと表示されるはずなので、URLを踏んで認証してください。

Successfully Authenticated with Google Drive!

と、表示されるはずです。 gapps authで行った認証の情報については、ホームディレクトリ以下の.gappsに保存されます。

認証を無事に終えたら、gappsコマンドで管理するGASのファイルを指定します。 Google Drive上でGASのファイルを生成し、 ブラウザ上のエディタでスクリプトを保存してください。 f:id:bino98ty:20160428185257p:plain

そのURLにある Project ID をコピーして、下記のコマンドに貼り付けてから、実行してください。 f:id:bino98ty:20160428185258p:plain

$ gapps init <Project ID>
# Example
# gapps init jiofe3g4jHfr5dekD5deNIO9e...

いかがでしょう。ローカルに下記のGASのファイル達が落とされたと思います。おめでとうございます!

.
├── gapps.config.json
└── src
    └── コード.js

あとは、よしなにファイルを編集して、下記のコマンドを実行すると、ブラウザ上のGASも更新されているかと思います。

$ gapps upload

また、gappsコマンドを使うとき、下記のことを知っておくと良いかもしれません…!

  • GASにアップロードされるファイルは、jsファイル、htmlファイルのみです。
  • jsファイルは、アップロードのタイミングで、gsファイルに自動で変換されます。
  • リモートからローカルにファイルをダウンロードできるのは、この時だけです。例えば、gapps init した後に、ブラウザのエディタでコードを編集しても、ローカルには落としてこれません。

ローカルに落とせたということは…

GASのファイル郡をローカルに落とせるようになったということは、下記のことが可能になりました。

  • GitHubによるバージョン管理が可能になりました。
  • テストも書くことが出来るようになりました。
  • タスクランナーを用いることで、ECMAScript2015(以下 ES2015)記法やTypeScriptをローカルで書いて -> ES5に変換 -> アップロードというフローでの作業が可能になり、Googleへのアップロードを自動で行うこともできるようになります。

ここからは、応用編として上記3つの方法の実施についてもご紹介します。

応用編1: GitHubによるバージョン管理

早速やっていきましょう。 まずは、GitHubにRepositoryを作成し、リモートを追加します。

$ git remote add origin git@github.com:ユーザー名/プロジェクト名.git

次に、Gitの初期化を行い、管理対象のファイルをコミットします。

# GASを落としてきたディレクトリ上で...
$ git init

あとは、ファイルをよしなに編集し、add/commit/push!

$ git add .
$ git commit -m 'initail commit'
$ git push origin master

いかがでしょう。GitHubのリポジトリページを覗いてみてください。GASのファイル達がお待ちです。

応用編2: テストを実施

先述のGoogleのブログにもあるように、スクリプトをローカルに落とせれば、機能テストを書くことも可能です。

今回は、テストフレームワークにmocha、アサーションライブラリにchaiを導入してテストを書いてみます。

$ npm install -g mocha
$ npm install -g chai

次に、テスト用のjsを置くディレクトリを作ります。

$ mkdir test

あとは、テストを書いてあげてください。

プレーンなJavaScriptでnodeのrequire()を利用するため、今回は Browserify + Gasify を使ってみます。Gasifyはjs2gsのBrowserifyプラグインとなります。

$ npm install -g browserify
$ npm install -g gasify

GASにアップロードされるのはデフォルトの設定ではsrcに入っているjs、htmlファイルだけなので、

.
├── gapps.config.json
├── dev
│   └── hello.js # テスト実行先のJS
│   └── main.js  # browserify元JS
├── src
│   └── main.js  # browserify先JS
└── test
    └── hello_spec.js  # テスト元のJS

というディレクトリ構成を作り、コードの実態をdevディレクトリ以下に書き、テストをtestディレクトリ以下に記述していきます。

まずは、下記のようにhelloメソッドに対するテストを書きます。

var chai = require('chai');
var should = chai.should();
var hello = require('../dev/hello');

describe('code.js', function() {
    context('echo', function() {
        it('should return Hello yamamoto when the value is yamamoto', function() {
            hello('yamamoto').should.equal('Hello yamamoto');
        });
    });
});

そして、実態を用意します。

module.exports = function(name) {
  return 'Hello ' + name;
}
var hello = require('./hello');
global.callHello = function () {
  Logger.log(hello('yamamoto'));
}

さて、ここでテストを実行してみます。

$ mocha
  code.js
    echo
      ✓ should return Hello yamamoto when the value is yamamoto


  1 passing (7ms)

無事にテストが通りました!

そして、browserifyにsrc/main.jsを出力させアップロードし、GASとして使えることも確認します。

$ browserify dev/main.js -p gasify -o src/main.js
$ gapps upload

GASのエディタ上からcallHelloを選択、実行すると、

f:id:bino98ty:20160428185300p:plain

ツールバーから 表示 > ログ で出てきた画面上に、Hello yamamotoと表示されていますね。

f:id:bino98ty:20160428185301p:plain

これで、helloメソッドの機能テストがかけるようになりました!良かった。

応用編3: Gulp連携

さて、ES2015で盛り上がる昨今、GASもES2015で書きたいと思うエンジニアの方も多いはずです。 プロトタイプベースのオブジェクト指向であったES5と比べ、Rubyや他の言語で見られる クラスベースのオブジェクト指向を取りいれたES2015ですが、 普段からRubyやJavaを書いているサーバサイドエンジニアにとっても、Javascriptを書く障壁が低くなること、間違いないです。

さて今回は、Babel + GulpでES2015 -> ES5変換する手法を取り入れます。

まずは、Gulpのインストールからです。 1つ1つ$ npm install...するのがめんどくさいので、今回はこちらでインストール時に発行されたpackage.jsonを使います。 gapps initしたディレクトリに下記のJSONを保存します。

{
  "devDependencies": {
    "babel": "^6.5.2",
    "babel-core": "^6.7.4",
    "babel-preset-es2015": "^6.6.0",
    "babel-register": "^6.7.2",
    "gulp": "^3.9.1",
    "gulp-babel": "^6.1.2",
    "gulp-exec": "^2.1.2",
    "gulp-load-plugins": "^1.2.0",
    "gulp-plumber": "^1.1.0",
    "gulp-mocha": "^2.2.0",
    "browserify": "^13.0.0",
    "babelify": "^5.0.5",
    "gasify": "^0.0.1",
    "vinyl-source-stream": "^1.1.0",
    "mocha": "^2.4.5",
    "chai": "^3.5.0"
  },
  "babel": {
    "presets": ["es2015"]
  }
}

今回、ES2015で書かれたコードは、browserify -> babelify(+ gasifyプラグインを適用)という流れで、GASへのアップロードを行うファイルを生成します。 babel6系を使用する最新のbabelifyを使い、トランスパイルするとGASのアップロードする際に、失敗してしまうことがあります。 ですので、今回はbabel5系を使用するbabelify5.0.5を採用しています。

そして、同一階層上でインストールを実行します。

$ npm install

さて、あとでES2015を記述するディレクトリと、ES5をしまうディレクトリを作成します。また、ついでにgapps uploadも同じタイミングで実行してもらいます。

先述の通り、gapps uploadは、srcディレクトリのみをアップロードしようとするため、GASへ出力するJSの吐き出し先はsrcとします。

今回は下記のようなディレクトリ構成で実行していきます。

.
├── gapps.config.json
├── gulpfile.babel.js # gulpのタスクを実装するJS
├── dev_es2015   
│   └── hello.js # テスト対象のJS
│   └── main.js  # browserify元JS
├── src
│   └── main.js  # browserify先JS
└── test
   └── code.js   # テスト元のJS

下記のファイルをリポジトリルートに置いてください。

import gulp from 'gulp'
import gulpLoadPlugins from 'gulp-load-plugins'
import browserify from 'browserify'
import source from 'vinyl-source-stream'
import runSequence from 'run-sequence'

const $ = gulpLoadPlugins()
const src_es2015_file = 'dev_es2015/**/*.js'

gulp.task('gas-upload', ['browserify'], () =>
  gulp.src('.')
    .pipe($.exec('gapps upload'))
)

gulp.task('test', () =>
  gulp.src(['test/**/*.js'], { read: false })
    .pipe($.mocha({ reporter: 'spec' }))
)

gulp.task('browserify', ['test'], () =>
  browserify({
    entries: ['dev_es2015/main.js']
  }).transform('babelify')
    .plugin('gasify')
    .bundle()
    .pipe(source('main.js'))
    .pipe(gulp.dest('src'))
)

gulp.task('watch', () =>
  gulp.watch(src_es2015_file, ['test', 'browserify', 'gas-upload'])
)

また、hello.jsも、下記の通りES2015ライクな書き方にし、それに合わせてtestも修正します。

export default (name) => 'Hello ' + name
import chai from 'chai'
import hello from '../dev_es2015/hello'
var should = chai.should()

describe('hello.js', () => {
    context('echo', () => {
        it('should return Hello yamamoto when the value is yamamoto', () => {
            hello('yamamoto').should.equal('Hello yamamoto')
        })
    })
})

それでは最後に、gulpを走らせてみましょう。

$ gulp watch

gulpを走らせているときに、dev_es2015の中身に変更があれば自動でテスト/browserify/babelifyを実施。さらに自動でGoogleにコードを送るようになりました。

f:id:bino98ty:20160428185256p:plain

このgulpfileでは、下記を走らせています。

  • watchタスクで、dev_es2015ディレクトリに変更があるかを常にチェック
    • 変更があったら下記のタスクを実行
  • testタスクでは、mochaによるテストを実行
  • browserifyタスクでは、browserify -> babelify(+ gasifyプラグインを適用)を実施
  • gas-uploadタスクでは、gapps uploadを実行

これにて、めでたく、GASをES2015で記述できるようになりました。

まとめ

いかがでしたでしょうか。 Google Apps ScriptはGoogle Driveの連携が非常に簡単に行えることに加え、GASをより便利にするようなライブラリもGoogleが幾つか提供してくれています。(例えば、OAuth2認証を行うこちらなど)

また、Slack連携も簡単に行えるので、普段からDriveを利用しているかたなど、ちょっとした効率化を行う際の何かの参考になれば良いと願っております。

それでは、良きGASライフを!