LoginSignup
14
14

More than 5 years have passed since last update.

Container boundなGASプロジェクトをローカル環境で開発する方法

Last updated at Posted at 2016-09-19

この記事は Node.gs: Node.js スタイルでの Google apps script 開発手法 の一部です。

はじめに

スプレッドシートなどのGoogle appsから起動する(Container boundな)GASのプロジェクトを、ローカルPCなどGASの外部で開発する方法です。

さんとりーさんgas-managerや、node-google-apps-scriptなどを使うことで、GASのソースコードをアップロードすることができますが、これはスタンドアロンなスクリプトに対してのみです。

そこでこの記事では、スプレッドシートなどから起動するコンテナバウンドなアプリケーションをローカルで開発し、上記ツールでアップロード可能にするための"ひと工夫"を紹介します。

Google apps script 中級者以上を対象読者として想定しています。

概要

下図は、私の開発環境全体像です。

開発フロー概要

nodejsのエコシステムにお世話になりながら、ローカル開発環境上でCode.gsを生成しています。
詳しくは別記事(Node.gs: Node.js スタイルでの Google apps script 開発手法)を参照ください。

私の場合、このCode.gsは主にスプレッドシートでの情報管理とワークフロー自動化のためのアプリケーションなので、スプレッドシートのメニューやタイムトリガから起動できることが必要です。しかしスプレッドシートのなかにあるGASのプロジェクトへ直接Code.gsをアップロードすることはできません。

そこでまず、Code.gsをスタンドアロンスクリプトとしてアップロードします。これをMyAppとします。

次にスプレッドシート内のCode.gsに、ブリッジ用のスクリプトをコピペで貼り付けます。ここだけ手作業でのアップになりますが、これはプロジェクト開始時に一度行うだけです。このコードはMyAppの内容によらず同一で、基本的に書き換える必要はありません。

そして、このブリッジスクリプトからMyAppをライブラリとして呼び出すことで、ローカル環境上で開発したアプリケーションをスプレッドシートから起動させることができます。

これにより、以下の効果を得られます。

  1. アプリケーション本体を、gas-managerやnode-google-apps-scriptなどのツールを利用してアプリ本体をアップロードすることができる
  2. アプリケーション本体を複数のスプレッドシートで共有できる

概要はこれですべてですが、これを実際に運用するためにはいくつか工夫が必要なので、それを以降で説明します。

  • 「理屈はいいからアプリの開発をはじめたい!」という方は、次章を飛ばしてMyAppの開発へ進んでください。

ブリッジスクリプト

なにはともあれ、ブリッジスクリプトをみてください。

bridge.gs
/**
 *  @fileoverview Spreadsheet side GAS program.
 */

/*
 *  let MyApp know 'this' global object.
 */
if (MyApp.init) {
  MyApp.init(this);
}

/*
 *  Simple triggers
 */
function onOpen(e) {
  return (MyApp.onOpen ? MyApp.onOpen(e) : undefined);
}

function onEdit(e) {
  return (MyApp.onEdit ? MyApp.onEdit(e) : undefined);
}

function doGet(e) {
  return (MyApp.doGet ? MyApp.doGet(e) : undefined);
}

function doPost(e) {
  return (MyApp.doPost ? MyApp.doPost(e) : undefined);
}

/*
 *  Dummy functions
 */
function func0001() {}
function func0002() {}
function func0003() {}
function func0004() {}
function func0005() {}
function func0006() {}
function func0007() {}
function func0008() {}
function func0009() {}
function func0010() {}

function func0011() {}
function func0012() {}
function func0013() {}
function func0014() {}
function func0015() {}
function func0016() {}
function func0017() {}
function func0018() {}
function func0019() {}
function func0020() {}

スクリプトは3つのブロックから構成されています。

  1. MyApp初期化関数へのグローバルオブジェクト引き渡し
  2. シンプルトリガーのブリッジ
  3. ダミー関数

以下この3つのブロックについて、コードがどのように働くのか説明します。

Global object

if (MyApp.init) {
  MyApp.init(this);
}

MyAppライブラリのinit関数を呼び出し、thisを渡しています。
この時点でのthisは、ブリッジスクリプトが実行されるコンテクストでのグローバルオブジェクトです。

GASのライブラリでは、起動時スクリプトのグローバルオブジェクトとライブラリ内部のグローバルオブジェクトが別物です。
MyApp側から起動時スクリプトのグローバル空間を参照することができるように、最初に通知を行っています。

たとえば、Dummy functionsのところで説明しますが、MyApp(ライブラリ)のなかからブリッジスクリプト(起動時スクリプト)のグローバル変数を書き換える際に使っています。

Simple trigger

function onOpen(e) {
  return (MyApp.onOpen ? MyApp.onOpen(e) : undefined);
}

シンプルトリガーは、呼び出される関数名が固定されているので簡単です。

ブリッジ関数は上記のように、ライブラリ内部の同名関数を呼び出すだけです。
(存在しない場合にはなにもしないようにガードしています)

Dummy functions

function func0001() {}

連番の関数が並んでいますが、関数名に意味はありません。
これらは、イベントトリガやメニュー選択によりスクリプトを起動するための仕掛けです。

GASによるスクリプト起動

MyAppでは、スプレッドシート画面にメニューを表示してスクリプトを起動させたり、時間トリガをつかって定期的に処理を実行させたい場合があります。

そのためMyAppのなかに実装した関数をイベントトリガーやメニューに登録したりしたいのですが、これはそのままではうまくいきません。

GASでは、イベントトリガーになる関数やメニューから起動される関数など、実行時に最初に呼び出される関数は起動時スクリプトのグローバル空間で静的に定義されていなければなりません。

GASのシステムは、イベント発生時やメニュー選択時にはまず、ソースコードをパースして起動すべき関数名が存在しているかどうかを確認しています。ここで見つからなければ起動すべき関数が存在しない、として起動エラーになってしまうのです。

ライブラリスクリプトのグローバル空間に存在していても、あるいはMyApp.init()のタイミングでグローバル空間にコピーしても、それは起動対象になりません。

ダミー関数の利用

これを回避するためには、2つの方法があります。

ひとつは、ブリッジスクリプトの中にMyApp内の起動したい関数を呼び出すだけのブリッジ関数を用意して、そのブリッジ関数をイベントトリガーやメニューに登録する方法です。シンプルトリガーのやりかたと同じです。

これは簡単でいいのですが、ブリッジスクリプトとMyAppスクリプトを両方編集しなければならず、関数名を追加・変更するたびに毎回書き直す必要があります。しかも最初に書いたとおり、ブリッジスクリプトはgas-managerなどで自動アップロードできないために手動で編集する必要があります。これはあまりよい方法ではありません。

もうひとつが、ここで採用しているダミー関数を利用する方法です。

この方法では、イベントトリガーやメニューに登録するための関数名をダミー関数としてあらかじめ存在させておき、実際にそのダミー関数が実行される前に、ダミー関数を本来実行したい関数で上書きします。

たとえば、メニューから実行したい関数MyApp.MyScriptがあるとします。MyApp.MyScriptはライブラリ内部の関数なので、そのままではメニューに登録できません。

そこでダミー関数func001を使います。onOpenのなかでメニュー登録する際に、このfunc001を登録します。そして、このfunc001が実際に実行される前に、MyApp.MyScriptで上書きしてしまいます。

実際のコード例は以下のようになります。

MyApp.gs

var GLOBAL_OBJECT;    // 起動時スクリプトのグローバルオブジェクト

function init(global) {
  GLOBAL_OBJECT = global;    // ブリッジ関数から渡された、起動時スクリプトのグローバルオブジェクト
                             // MyApp.init(this); として呼び出される。

  GLOBAL_OBJECT.func0001 = MyScript;    // グローバル空間にあるfunc001を、MyScriptで上書きする
}

function onOpen() {
  var ss = SpreadsheetApp.getActiveSpreadsheet(),
      menuEntries = [
        {name: 'My Script', functionName: 'func001'}
      ];

  ss.addMenu('My Menu', menuEntries);
}

GASのシステムは、スクリプトをパースして起動関数が存在するかどうかは確認しますが、実際に起動される関数本体を記憶するわけではありません。なので、上記のようにダミーで用意した名前にライブラリ内部の関数を代入してしまえば、実行時にはMyApp.MyScriptを呼び出してくれます。

上記はメニューの例でしたが、イベントトリガでも同じ方法が使えます。この方法により、ブリッジスクリプトには手を加えずプロジェクト開始時に一度だけ手動でコピペするだけで、あとはMyAppへの編集だけで開発作業をすすめることができます。

実際にはメニューから起動される関数やイベントトリガで起動される関数は複数あるので、ダミー関数も複数用意してあります。

最後に2つの方法の中間的な方法として、ブリッジ関数を意味のない連番名で複数用意しておく、という方法もあります。
つまり下記のような関数を羅列します。

function func0001() {
  return (MyApp.func0001 ? MyApp.func0001() : undefined);
}

この方法でもうまく行きますし、こうしておくとMyApp.init(this);でグローバルオブジェクトを渡す必要もありません。こっちのほうがブリッジスクリプトの理解は容易になるかもしれません。

ただ個人的にはブリッジスクリプトが長くなるのが嫌なことと、いずれにせよグローバルオブジェクトはMyApp側で知っておきたいために、この方法は使っていません。

MyAppの開発

準備

実際にMyAppを開発するには、以下の準備を行います。

  1. アプリ本体(MyApp)

  1. Google Drive内に、任意の名前でスクリプトを作成する。
  2. スクリプトは空のままでよいので、バージョン番号を付与してライブラリとして公開する。

2. スプレッドシートなどコンテナ側

  1. スプレッドシートなどから、スクリプトエディタを起動する。
  2. Code.gsに、ブリッジスクリプト(ブリッジスクリプトの章で紹介したbridge.gs)をコピペする。
  3. MyAppライブラリをデバッグモードでインポートする。
  4. このとき、実際のアプリのスクリプト名によらず、ライブラリオブジェクト名をMyAppと指定する。

準備は以上です。

あとは自分の開発環境でコードを書き、gas-managerやnode-google-apps-scriptを使ってMyAppライブラリへアップロードします。

MyAppの書き方

Dummy functionsのところで紹介したスクリプトとほぼ同じですが、以下に再掲します。

MyApp.gs

var GLOBAL_OBJECT;    // 起動時スクリプトのグローバルオブジェクト

function init(global) {
  GLOBAL_OBJECT = global;    // ブリッジ関数から渡された、起動時スクリプトのグローバルオブジェクト
                             // MyApp.init(this); として呼び出される。

  GLOBAL_OBJECT.func0001 = MyScript;    // グローバル空間にあるfunc001を、MyScriptで上書きする
}

function onOpen() {
  var ss = SpreadsheetApp.getActiveSpreadsheet(),
      menuEntries = [
        {name: 'My Script', functionName: 'func001'}
      ];

  ss.addMenu('My Menu', menuEntries);
}


function MyScript() {
    // プログラム本体
}

MyApp.gsの出だしは、いつも上記のようになります。
以下、順に説明を加えます。

var GLOBAL_OBJECT;    // 起動時スクリプトのグローバルオブジェクト

function init(global) {
  GLOBAL_OBJECT = global;    // ブリッジ関数から渡された、起動時スクリプトのグローバルオブジェクト
                             // MyApp.init(this); として呼び出される。

  GLOBAL_OBJECT.func0001 = MyScript;    // グローバル空間にあるfunc001を、MyScriptで上書きする
}

最初の数行はいじる必要はありません。
GLOBAL_OBJECT.func001 などダミー関数に、メニューやイベントから起動される関数を代入してください。

function onOpen() {
  var ss = SpreadsheetApp.getActiveSpreadsheet(),
      menuEntries = [
        {name: 'My Script', functionName: 'func001'}
      ];

  ss.addMenu('My Menu', menuEntries);
}

これは、スプレッドシートにメニューを表示させるコードです。
ポイントは、'My Script'メニューから起動される関数として、initのところで代入したfunc001を指定している部分です。

MyApp.MyScriptを直接登録することはできないので、このようにダミー関数を登録します。

function MyScript() {
    // プログラム本体
}

プログラム本体です。ここから先は、いつもの通りです。

これで、ライブラリとして実装したアプリ本体を、あたかもコンテナバウンドなスクリプトと同様に動作させることができます。

Version管理

MyAppの開発:準備のところで、アプリ本体のライブラリをデバッグモードでインポートする、と書きました。

開発中はそれでよいのですが、運用時にはふたつの選択肢があり、それぞれにメリット・デメリットがあるので紹介します。

通常モードとデバッグモード

あるスクリプトからライブラリをインポートする際、ライブラリのバージョン番号を指定します。通常、ライブラリのスクリプトが書き換えられても使用バージョンを書き換えない限り、新しいスクリプトは使用されません。

このときデバッグモードを指定すると、ライブラリは常に最新のソースコードを使用します。バージョン番号の付与も不要で、まさにデバッグモードです。

Googleのドキュメントには記載が見当たりませんが、デバッグモードには他にも条件があります。

スクリプト実行者が、ライブラリスクリプトに対して編集権限を持っていなければならない、というものです。そうでない場合にはデバッグモードスイッチは無視され、通常通り指定されたバージョンが使用されます。

  • デバッグしてるんだから、編集権限あるよね?
  • デバッグ中のコードを(閲覧権限しか持たない)ユーザーに使わせちゃ困るよね?

という理由だと思いますしので、妥当な仕様だと思います。

MyApp開発・運用時のデバッグモード利用

このデバッグモードを、MyAppの開発・運用時に利用することができます。

たとえば以下のケースを考えます。

  • MyAppのを利用するスプレッドシートが、10ファイルあるとします。これを利用するのは、開発者とは別のユーザーです。
  • 10あるスプレッドシート内のスクリプトはすべてブリッジで、MyApp本体はライブラリとしてひとつだけ存在しています。

このMyAppを、デバッグモードを指定せず通常モードで使用した場合、MyAppを更新した際にはスプレッドシートのライブラリ使用バージョンも更新する必要があります。これは面倒です。10ならまだいいかもしれませんが、100や1,000になったらお手上げです。

デバッグモードを指定した場合、この手間を省くことができます。常に最新のスクリプトが利用されるので、MyAppを更新すれば、即時そのスクリプトが利用されるようになります。スプレッドシートが1,000でも10,000でも問題ありません。

デバッグモード利用のデメリット

しかしデバッグモードを利用する場合には、ユーザーに対してライブラリの編集権限を付与しなければなりません。そのユーザーが社内の信頼できるメンバーであるなら問題ないでしょうが、不特定多数を相手にする場合にはこの方法は問題があるでしょう。

もしかしたら、こんなアイデアを思いついたひともいるかもしれません。

  • ブリッジスクリプトから、デバッグモードで「ブリッジライブラリ」をインポートする。
  • ブリッジライブラリは、通常通りにバージョンを指定して、MyAppライブラリをインポートする。
  • ブリッジライブラリのバージョン指定を一箇所変更すれば、すべての更新作業が完了する。

残念ながらこれもダメでした。デバッグモードになりません。

もうひとつ、デバッグモードを利用した際に問題になることがあります。

たとえばスプレッドシートで出張精算を自動化して何年も運用していたとします。この出張精算プログラムの機能追加の際に、スクリプトだけでなくスプレッドシートにも変更が入るケースはわりとあります。費目が増えるとか、列が追加されるなどです。

このとき、もしデバッグモードで運用しているとMyAppの機能追加は過去のスプレッドシートに遡って適用されていまいます。しかし過去の出張精算書のスプレッドシートは古いフォーマットのままですから、ここで問題が生じます。

通常モードであれば、あるファイルの使用するMyAppのバージョン番号は固定されているために、この問題は生じません。

私の場合はいくつか試した結果、自分以外のユーザーが使うファイルにはデバッグモードを利用せず、通常モードで運用するようにしています。自分自身が使うアプリケーションで且つファイルがひとつであるときには、デバッグモードを使用しています。

まとめ

アプリケーション本体をライブラリとして提供し、Google apps内のブリッジスクリプトからインポートすることで、コンテナバウンドなアプリケーションを自分の環境で開発しアップロードする方法を紹介しました。

これによって、自分の環境でコーディングし、ユニットテストを行い、nodejsの資産を利用し、構成管理をしながら開発することができます。

逆に、この方法のデメリットとしては、以下を挙げられます。

  • ブリッジスクリプトの準備と、ダミー関数名・本体関数名の対応付けが面倒
  • 通常モードでのバージョン更新が面倒
  • ライブラリ呼び出しの分、起動が遅い(実測していません。理論的に遅いはず)

それでも得られる効果は大きいので、試してみてください。

間違いの指摘やアドバイスなど、ぜひよろしくお願いします。

14
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
14