🖊️

Google App Scriptを使ったツール運用の知見をまとめる

2022/01/27に公開

社内ツールのデータ入力にGoogle App Script(GAS)を使っています。
その際の知見をまとめておきます。

概要

  • 処理のほとんどをライブラリに入れることで、個々のスプレッドシートの管理を不要にした
  • GASの仕様的にV8エンジンをOFFにしないと更新可能な共有可能なライブラリはできない
  • V8でないために色々な回避方法が必要になる

要件

  • リリース後も頻繁にフィードバックがあるため、変更可能な形にしておく
  • スプレッドシートの件数は数十件、利用ユーザーも10人以上いる
  • 開発者以外でも処理を実行できる(メニューに出す)

詳細

スプレッドシートの件数的に修正があった場合に1件ずつ修正するのは、現実的ではありませんでした。(1回やったけど、数が多いかつ更新漏れがあってめっちゃ大変)
そのため、処理を可能な限りライブラリとして切り出して運用しています。

最終的にスプレッドシート側のGASの処理としては以下のようにシンプルになりました。

main.ts
// ファイルが開いたときに呼び出される関数
function onOpen() {
  // LIBはライブラリ
  const item = LIB.getMenu();
  // メニューの右端に連携のメニューを追加
  SpreadsheetApp.getActiveSpreadsheet().addMenu('連携', item);
}

LIB.initialize(this);

ユーザーから実行可能な処理はすべて連携のメニューとして出しています。
メニューを選択したときの処理はすべてライブラリ側に存在しています。

編集可能なライブラリにする

後述の記事でも紹介しましたが、ライブラリのバージョンをHEAD(開発モード)にしている場合、V8のエンジンでは、スクリプトを実行するユーザーがライブラリを編集可能でないとエラーになります。
悪意のあるユーザーはいないとは思いますが、編集可能であると意図せず内容を修正されてしまう可能性があるため、ランタイムのバージョンを DEPRECATED_ES5 にしています。
逆に言うと、DEPRECATED_ES5の場合は許可したライブラリの内容が、Viewの権限なくても勝手に更新されていくってことになるのだが、大丈夫なんだろうか。。

V8のランタイムとDEPRECATED_ES5の差分は以下にまとまっていますので参考までに。
https://developers.google.com/apps-script/guides/v8-runtime

メニューを後から変更できるようにする

地味な点なのですが、DEPRECATED_ES5の場合はMenuItemにObjectを経由した関数の呼び出しができません。V8からはできるようになっています。
具体的には以下のような呼び出しができるようになるのはV8からです。

function onOpen() {
  var ui = SpreadsheetApp.getUi(); // Or DocumentApp, SlidesApp, or FormApp.
  ui.createMenu('Custom Menu')
      // menu.item1の呼び出しがV8から。DEPRECATED_ES5ではmenuItem1のようなものを作成しないといけない
      .addItem('First item', 'menu.item1')
      .addSeparator()
      .addSubMenu(ui.createMenu('Sub-menu')
          .addItem('Second item', 'menu.item2'))
      .addToUi();
}

var menu = {
  item1: function() {
    SpreadsheetApp.getUi().alert('You clicked: First item');
  },
  item2: function() {
    SpreadsheetApp.getUi().alert('You clicked: Second item');
  }
}

ライブラリを利用する場合はLIB.xxxのように呼び出し元のライブラリを必ず指定する必要があります。
つまり、MenuItemからはライブラリの関数を直接呼び出すことができません。
この問題に対しては、thisに直接関数を定義することで解決しました。ソースの部分では、コメントを書いていないLIB.initialize(this);の部分になります。

initializeの処理では以下のようなことをしています。

lib.ts
export const initialize = that => {
    console.log('initialize...');
    // 共通
    that.hoge = hoge();
};

function hoge() {
  console.log('hoge');
}

それぞれのfunctionは定義すると、this.xxxのようにthisのメンバとして登録されています。
例えば、スプレッドシートを開いたときに実行されるonOpenの関数はthis.onOpenのようになります。
同様にthisのメンバとして、ライブラリの関数として無理やり登録することで、ライブラリの関数であってもLIB.hogeのようにオブジェクトを経由せずに呼び出しすることができます。
thatって昔はよく使いましたよね。

claspを利用する

jsで一定以上のファイルを管理するのがしんどいのでclasp + Typescriptでソースの管理をしています。
clasp + Typescriptの対応方法については公式にまとまっています。

https://github.com/google/clasp/blob/master/docs/typescript.md

claspを利用するとはまる点が何点かあります。大枠としては以下になるのかなと思います。

  • npmモジュール使うの大変
    • そんなに複雑な処理をしていないので、利用していません。
  • importうまくできない(後述)
  • polyfill(後述)

以下のリンクが参考になりました。
https://qiita.com/nazoking@github/items/5689ba7d27d4cdfda8f5
https://kenchan0130.github.io/post/2019-12-25-1

importうまくできない

公式でいくつか解決方法が紹介されていますが、namespaceを利用しています。
https://github.com/google/clasp/blob/master/docs/typescript.md#modules-exports-and-imports

サンプルとしては以下のような感じです。

lib.ts
exrpot const test = () => {}

export namespace testNamespace {
  export const test = () => ();
}
main.ts
import {test, testNamespace} from './lib.ts'

function sample() {
  // 実行できる
  testNamespace.test()
  // エラーになる
  test();
}

polyfillを用意する

claspでデプロイしても、Array.findなどは変換されずそのまま残っているため、別の方法で作成する必要があります。
直接書く方法もありましたが、今回はpolyfillのライブラリを自作して利用しています。
詳細については以前に書いた記事を参考にしてみてください。
https://zenn.dev/merutin/articles/81a7354e54fdc1

まとめ

V8のエンジンが使えない等、色々工夫しないといけない点がありましたが、現状は問題なく利用できています。
複数のスプレッドシートにわたる関数を利用したい場合は紹介したような設定を検討してみてください。

Discussion