Google Apps Script でも require()したい

こんにちは、村上 (@fossamagna) です。

今回はQiitaの Google Apps Script Advent Calendar 2015 に投稿した 記事 の転載です。
gasifyというツールを作成したので合わせてご紹介します。


Google Apps Script でも require()したい

普段、Node.jsのようなサーバサイドJavaScriptだったり、ブラウザ上のJavaScriptだったりをターゲットにして開発していると、require()使ってモジュール化してコードを作っていきますよね。
Google Apps Scriptでも同じようなrequire()使えたらなーと思い試してみたので、その内容を紹介します。

Google Apps Script のモジュール機能

Google Apps Scriptでは、公式に提供されているライブラリがモジュール機能に当たります。ライブラリの機能は ScriptEditor で読み込みたい別のスクリプトのIDを指定して読み込むことができます(読み込みたいスクリプトに対する閲覧権限が必要です)。

ライブラリの困った点

このライブラリ機能は使いたいスクリプトのIDを指定するだけで簡単にコードを再利用できるのですが、ある程度の規模のコードを書くようになると、次のような困った点が出てきます。

さらに、ライブラリの機能ではないのですが、Google Apps Scriptの困った点としてファイルの読み込み順を指定できないという問題があります。機能毎にgsファイルを分割して、それらに依存関係がある場合、gsファイルの読み込み順が重要になってきます。しかし、Google Apps Scriptはgsファイルの読み込み順を指定できないので読み込み順によっては期待した動作をしないという問題が起こります。

Browserifyでビルド

そんな時、require()がネイティブで使えないのってブラウザも同じじゃないか?と思ったので、ブラウザでrequire()するための Browserify を使えば、Google Apps Scriptでもrequire()できるかもと思い試してみました。

動作を確認するために次のようなサンプルを用意しました。

main.js:

1
2
3
4
var hello = require('./hello');
function callHello() {
return hello();
}

hello.js:

1
2
3
module.exports = function () {
return 'Hello!';
};

main.jsからhello.jsrequireして呼び出すだけのシンプルなものです。

これをBrowserifyで1つのjsファイルにしてみます。以下のようにするとmain.jsをエントリーポイントとして依存するjsファイルを結合してbundle.jsにまとめてくれます。

1
$ browserify main.js -o bundle.js

Google Apps Scriptで動かしてみる

さて、ScriptEditorを開いてをCode.gsに上記のコマンドで作成した bundle.js の内容をコピペして、callHello()関数を実行してみようとしたのですが、ScriptEditorの関数を選択するコンボボックスにcallHello()関数が表示されません。

どうも、require()を実現するためにBrowserifyの関数でラップされてしまっているのでScriptEditorから見えないようです。ScriptEditorから見えないだけで実行はできるんじゃないと思いクライアントサイドのJavaScriptからgoogle.script.runを使って呼び出してみましたが、callHello()関数は見つけられず、実行できませんでした。

いろいろ試した結果。グローバルオブジェクトに関数宣言文で関数を定義 しないとScriptEditorからもgoogle.script.runからも呼び出せないことがわかりました。

以下のようなコードなら大丈夫ということです。

1
2
3
4
function callHello() {
var hello = require('./hello');
return hello();
}

Google Apps Script 特有の対応

困りました、Browserifyで結合したらBrowserify関数でラップされてしまうのでグローバルオブジェクトに関数宣言文で関数を定義できません。しかし、Browserifyを使いながらなんとか関数を定義できないか試して次の条件なら呼び出せることがわかりました。

  • Browserifyで生成したbundle.jsより前に実行されるコードで関数宣言文を使って関数を定義(中身は空っぽでOK)する。
  • bundle.js内で上記の関数を上書きする。

先ほどの例をこの条件に合うように直してみます。

main.js:

1
2
3
4
var hello = require('./hello');
global.callHello = function () { // `global`オブジェクトに関数を代入する
return hello();
}

hello.jsは変更ありません。

この内容でbundle.jsを生成します。さらに、生成されたbundle.jsの先頭に以下のコードを追加します。

bundle.js:

1
2
3
var global = this;  // グローバルオジェクトを`global`変数で参照できるようにする
function callHello() { // Google Apps Scriptが呼び出せるように空の関数を定義する
}

このようにすることで、ScriptEditorからもgoogle.script.runからも呼び出せるようになります。

つまり、bundle.jsを生成したら、

  • var global = this;を追加する
  • 呼び出したい関数の関数宣言文を追加する。
    をしてあげれば万事OKです

って、これじゃ面倒くささ倍増です、素直にライブラリ使ったほうがマシです。bundle.jsを生成するたびにそれを編集するなんてやってられません。

少し工夫して追記が必要な内容だけをstub.jsに書いてbundle.jsを生成するコマンドを以下のようにすれば、修正の頻度はビルド毎から呼び出す関数が増える毎に減らすことはできます。

1
$ browserify main.js | cat - stub.js > bundle.js

でも、main.jsとかに関数やロジックを追加して、さらにstub.jsにも同じ名前の関数宣言文を追加するのも面倒です。きっと追加するのを忘れて、呼び出せなくて「あぁーー」ってなります。

gasify

そこで、gasifyという Browserify のプラグインを作りました。gasifyを使うとGoogle Apps Scriptで動作させるために必要なコードもbundle.jsに出力してくれます。これで呼び出す関数が増えても安心です。もうstub.jsに追記は必要ありません。というかstub.js自体必要ないです。

gasifyは以下のコマンドを実行してインストールできます。

1
$ npm install gasify

使い方はbrowserifyのプラグインとしてgassifyを指定するだけです。オプションなどはありません。

1
$ browserify main.js -p gasify -o bundle.js

これで、「あぁーー」とならずに Google Apps Scriptでrequire()を使えますね

gas-managerという便利なツールがあります。gas-managerを使えばコマンドでbundle.jsの内容をGASプロジェクトに反映させることができるので、コピペせずにさらに楽になります。

まとめ

  • Browserify + gasify で Google Apps Scriptでもrequire()が使える
  • gas-manager を使うとより便利