ちょうど1年前、「Google Apps Scriptのモダンな開発環境を求めて」という記事を書きました。 これにより、モダンな開発環境によってDeveloper Experienceが高いGASプロジェクト開発ができたかというと、残念ながらそうではありませんでした。 実際に運用していくうちに、いつくか課題が見えてきました。

今回はそれらの課題と向き合いどのように対応したのかを紹介します。

この記事はFOLIO Advent Calendar 2019の12月25日の記事でもあります。

前回のおさらい

Google Apps Scriptのモダンな開発環境を求めて」では、スクリプトエディタ上でJavaScriptを記述していく一般的な方法ではなく、claspを使う方法を紹介しました。 claspはTypeScriptをサポートしていますが、ES Modulesをサポートしていないため、npmライブラリを使いたい場合はBabelwebpackを組み合わせてビルドする必要がありました。

ちなみに、babel-preset-google-apps-scriptというプリセットを用意しましたので、Babelを使う場合ぜひご利用ください。

見えてきた課題

GASのモダンな開発環境を作りましたが、いくつかの課題が見えてきました。

npmライブラリが種類によってGASでは動かない問題

理想を言うのであれば、もちろんどんなアプリケーションを作成したとしても、使用するライブラリがどのようなしくみで動作しているのかを理解するべきではあります。 やりたいことを高速に実現し、自分たちのアプリケーションに注力するためにライブラリを使用するという目的もあるため、実際問題として隅から隅まで実装を理解および把握するのは難しく、しばしば大まかな挙動のみの把握および理解するにとどまります。

前回の記事では、車輪の再発明はつらいですし、せっかくnpmライブラリが使用できるなら使わない手はないと考え、npmライブラリを使用できる環境を構築しました。 しかし、GASプロジェクトでnpmライブラリを使用する際は、隅から隅までとは言いませんが、それでも普段のアプリケーション開発よりはnpmライブラリがどのように動作しているかを理解および把握する必要がでてきました。

なぜなら、npmライブラリ内でGASがサポートしていないNode.jsのしくみが使われていると、当然ながらGASでは動作しないからです。

たとえば、使用するライブラリの内部にPromiseが使われていた場合、GASでは動作しないため実行時エラーになってしまいます。Babelを使用したとしても、Promiseをトランスパイルできるわけではないため、Promoseが使用されていないライブラリを探すか自身で実装する必要があります。

ゆえに、もしnpmライブラリを使いたい場合は、使用するライブラリおよびその依存の数が少なく、それらの実装を理解および把握できる場合に限り使用をしてもよいと考えています。

polyfillが巨大すぎてスクリプトエディタ開かない問題

npmライブラリは物によってはES61などで記述されており、そのままwebpackでバンドルしてしまうとGASでは動作しません。 GASで動作させるには、core-jsなどのpolyfillをインポートする必要があります。

多くのしくみをES3相当までトランスパイルする必要があるため、minifyしたとしてもpolyfillでインポートするコードは数十から数百KBになります。 GASのスクリプトエディタはこのような巨大なファイルを開くのが苦手です。実際に開いてみるとコードが表示されてUIを操作できるまでに数十分を要します。

claspを導入しているので、「CLIでやればWeb UIは必要ないじゃん」と思われるかもしれませんが、claspではWebアプリケーションのリリースはできてもデプロイはできません。2

そのため、どうしてもスクリプトエディタを開くまたは対象の操作のHTTPリクエストを特定してリクエストを模倣する必要がでてきます。 また、各種スクリプトエディタでないと実現が難しい機能を使いたい場合にも、この副作用が開発に支障をきたす可能性もあります。

ファイル分割するとimport/export使えない問題

Babelやwebpackの力を借りずになるべくシンプルにやりたいことを実現したい。 そう思って、「claspで標準サポートされているTypeScriptのみで実現しよう」と思うこともあります。 そして、開発を進めていくとファイルを分割したくなります。

以下のような2つのファイルがあるとします。

import { someUseCase } from './someUseCase';

interface PostEvent {
  queryString: string;
  parameters: { [index: string]: [string | undefined] };
  contentLength: number;
  contentPath: string;
}

export function doPost(
  event: PostEventFromSlack
): GoogleAppsScript.Content.TextOutput {
  const response = someUseCase.run();

  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}
interface SomeUseCaseResponseDto {
  test: string;
}

class SomeUseCase {
  public run(): SomeUseCaseResponseDto {
    return {
      test: "This is test response."
    };
  }
}

export const someUseCase = new SomeUseCase();

これを実際にclasp pushしてもimport部がコメントアウトされるだけで、実際に実行すると

ReferenceError: someUseCase is not defined

となり、動作しないことがあります。 これはGAS側で複数ファイルある場合は独自に結合して実行をするため、実行順番が保証されずこのようなことが起こります。

この問題に対応するには公式で以下の3つが提案されています。

  1. exportsに値が入っているのでdeclare const exportsしてexports.xxxとして呼び出す
  2. TypeScriptのnamespaceを使用する
  3. webpackなどのサードパーティのビルドツールを使用する

3つ目の方法はすでに紹介および実行した方法です。 1つ目を試してみましたが、動作が不安定でした。 2つ目の、

TypeScriptのnamespaceを使用する

は動作することは確認しています。具体的には前述の例は以下のようになります。

import { SomeUseCase } from './SomeUseCase';

interface PostEvent {
  queryString: string;
  parameters: { [index: string]: [string | undefined] };
  contentLength: number;
  contentPath: string;
}

export function doPost(
  event: PostEventFromSlack
): GoogleAppsScript.Content.TextOutput {
  const response = SomeUseCase.run();

  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}
interface SomeUseCaseResponseDto {
  test: string;
}

namespace SomeUseCase {
  export function run(): SomeUseCaseResponseDto {
    return {
      test: "This is test response."
    };
  }
}

しかし、この方法はいくつかの不安要素を抱えています。

  • classが使えないまたは使うにはテクニックがいる
  • namespaceはTypeScript界隈では使用しないほうが良いという流れ3になっている

これらの不安要素がある状態を認識したうえであれば、この手法を選択できると考えています。

我々はどうしていけばよいのか

たくさんの課題がある中で、我々はどのようにしてGASをモダンな開発していけばよいのでしょうか。 正直答えは持ち合わせていないですが、個人的には

  • claspとTypeScriptで開発をする
    • コードはGitなどのバーション管理ツールで管理するため
    • TypeScriptで型の恩恵をうけることにより、なるべく安全なアプリケーションを作成する
  • npmライブラリは使わない
  • 1ファイルを超えないようにする
    • 超えるのであればGASとして運用すべきではない可能性が高い

と考えています。 つまり、GASでは無理をしないということです。

終わりに

モダンな開発環境によってDeveloper Experienceが高いGASプロジェクトを開発できたわけではなく課題があり、その課題に対する対応方法を紹介しました。

もう一点気になることとしては、中々Merge Requestがマージされない状況からclaspが最近開発が活発ではないという印象を受けています。 執筆時点では2019年9月25日を最後に更新されていません。

このような状況を把握しつつGASの開発していくとよいでしょう。

  1. ECMAScript2015の略称であり、ES2015とも呼ばれ、Ecma Internationalによって標準化されている 

  2. GASに該当するAPIがないため実現ができていない Clasp deploy doesn’t update the deployed version with Google Apps Script Web App 

  3. Is TypeScript Namespace feature deprecated?