Google Apps Scriptでrequire()してみる

# Google Apps Script でも `require()`したい

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

## Google Apps Script のモジュール機能

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

## ライブラリの困った点

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

* ライブラリを追加する度に ScriptEditor の UI からID追加の操作しないといけない
* [スクリプトの起動に時間がかかる](https://developers.google.com/apps-script/best_practices#avoidLibraries)

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

## Browserifyでビルド

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

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

```js:main.js
var hello = require('./hello');
function callHello() {
  return hello();
}
```

```js:hello.js
module.exports = function () {
  return 'Hello!';
};
```

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

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

```bash
$ 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`からも呼び出せないことがわかりました。

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

```js:bundle.js
function callHello() {
  var hello = require('./hello');
  return hello();
}
```

## Google Apps Script 特有の対応

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

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

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

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

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

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

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

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

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

* `var global = this;`を追加する
* 呼び出したい関数の関数宣言文を追加する。

をしてあげれば万事OKです:v:

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

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

```bash
$ browserify main.js | cat - stub.js > bundle.js'
```

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

## gasify

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

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

```bash
$ npm install gasify
```

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

```bash
$ browserify main.js -p gasify -o bundle.js
```

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

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

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