23
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GASをイケイケな環境で開発したい!claspを使ってみる

Last updated at Posted at 2022-07-16

TL; DR

  • clasp + esbuild で、GASのアプリケーションを...
    • バージョン管理できる!(GitHubでコード管理)
    • 型安全にできる!(TypeScript使用)
    • ユニットテストできる!(Jest使用)
  • が、つらみも感じたのでご利用は計画的に

作ったもの: SwitchBot 温湿度計プラスで取得した温度、湿度をスプレッドシートに記録

はじめに

Google Apps Script (GAS) を使うと、ちょっとした処理を手軽に自動化できます。エディタがWeb上にあるので、サクッと書いてすぐに動かせます。

そのままでも使いやすいのですが、もっとイケイケな開発環境を作りたい!(欲張り)

  • ローカルで、好きなエディタで開発したい!
  • Gitで管理したい/GitHubに公開したい!
  • TypeScriptで型安全にしたい!
  • ディレクトリ構成を整理して、クラスを疎結合にしたい!
  • ユニットテストを書きたい!

というわけで、本記事ではGASプロジェクトで上記の願いを叶えるためにやったことを紹介します。

(TypeScriptもGASも初心者なので、「こうした方が良いよ」等ありましたらコメントいただけるとありがたいです!)

バージョン

名前 バージョン
Node.js v16.16.0
npm 8.11.0
TypeScript 4.7.4
clasp 2.4.1
Jest 28.1.4
esbuild 0.14.49
esbuild-gas-plugin 0.4.0
talkback 3.0.1

作ったもの

まずは今回作ったものです。

SwitchBot温湿度計プラスのデータを定期取得し、スプレッドシートに記録しています(導入手順は上記Readme参照)。

meter_recorder.png

買った次の週にプライムデーでセールになった :sob: 良い子のみんなは事前に確認しようね!

実装には、以下の記事を参考にさせていただきました。当記事はn番煎じです

温湿度計自体の解説は上記記事がとても分かりやすかったので、本記事では割愛します。

claspを使ったローカル開発環境の構築

「はじめに」で上げた種々の煩悩希望は、claspを使うことで解決しました。

claspは、GASのソースコードをローカル環境で開発できるツールです。GASのプロジェクトをnpmで開発できるので、Git管理もユニットテストもし放題です。

コードを変更したら

$ clasp push

するだけで、gsファイルにトランスパイルしてデプロイしてくれます(TypeScriptも対応)。

参考にさせていただいた記事

また、.clasp.json ファイルにpush先のスクリプトIDが記載されているので、スクリプトIDを差し替えるだけで別環境にもデプロイ可能です。

(逆にいうと、スクリプトIDが分かると第三者にデプロイされてしまうので、.clasp.json.gitignore に入れておくことをおススメします)

ディレクトリ構成を整理して、クラスを疎結合にする

続いて、ディレクトリ構成です。変更に強いアプリケーションにするため、オーバーキル気味に DDD + クリーンアーキテクチャで設計しました1

ディレクトリ構成
- src
  - domain
  - usecase
  - adapter
  - infrastructure
  - di
  - config

しかし、GASはimport文を認識できません。gsファイルに変換される際にコメントアウトされてしまいます。

import.png

公式では3通りの解決策が紹介されており、今回は一番ソースコードの変更が少なくて済む ビルドして1ファイルにまとめる 方法を採用しました。

ビルドして1ファイルにまとめる

ビルドツールには esbuild を使用しています。爆速で感動 :rocket:

全ソースコードをビルドし、./dist/main.js にまとめます。clasp push ではこの1ファイルだけデプロイすることで、importができない問題を解消しています。

:warning: デプロイ対象が変わったので、appscript.json./dist 配下に移動してください

ハマった点 1: ハンドラ関数が呼べなくなってしまう

GASでは、gsファイルのトップレベルで定義されている関数を実行できます。
しかし、esbuildの生成物は即時関数に包まれてしまうので、関数が見えなくなってしまいます。そこで、ビルドにesbuild-gas-pluginを使って関数が見えるようにしました。

esbuild-gas-pluginの使い方

まず、ビルドスクリプトを build.js に記載します。

build.js
const { GasPlugin } = require('esbuild-gas-plugin');

require('esbuild').build({
  entryPoints: ['src/main.ts'],
  bundle: true,
  outfile: 'dist/main.js',
  plugins: [GasPlugin]
}).catch(() => process.exit(1))
package.json
{
  "scripts": {
    "build": "node build.js"
  },
  // ...
}

次に、呼び出したい関数を global のプロパティに持たせます。

src/main.ts
// exportしていても、ビルド後は即時関数に包まれるので見えなくなってしまう
export function handler() {
  // ...
}

// そこで、handler関数をglobalに持たせて参照可能にする
declare let global: any;
global.handler = handler;

ビルドすると、handler がトップレベルに定義され呼び出し可能になります。

dist/main.js
// ビルド済みファイル

var global = this;
// コードは全て即時関数に包まれるので参照不可
(() => {
  // もちろん、本来handlerも参照不可能
  function handler() {
    // ...
  }

  // だが、globalに代入した(上記)のでthis.handlerとしてトップレベルに見えるようになった!
  global.handler = handler;
})();

参考にさせていただいた記事

ハマった点 2: DIコンテナが動かない

tsyringe を使う予定でしたが、GAS上でうまく動作しなかったのでやむなく手書きしました。

明示的に呼び出すと、DIコンテナクラスのimportが汚くなり、実装クラスもexportしないといけないのが辛いです...(ソースコード)

とはいえ、モジュールを多用すると生成ファイルが肥大化してデバッグしづらくなるので手書きでちょうどよかったかもしれません。

テスト作成

テストにはJestを使用しています。

ローカルでTDDできるので心安らかに開発できます。デプロイしてぶっつけ本番は調査がつらい
クリーンアーキテクチャで各クラスは抽象に依存しているので、ダミークラスに差し替えればユニットテストも簡単に作れます。

VSCodeを使っているなら、vscode-jestのインストールもしておきましょう。ファイル保存のたびにテストが流れてくれます

SwitchBot APIリクエストのユニットテスト

コーナーケースも検証できるよう、実際のHTTPリクエスト、レスポンスを使用しています。とはいえ、テストのたびに本家のAPIを叩くのは迷惑なので、talkbackのプロキシサーバーを利用しています。

APIリクエストの際にtalkbackを経由すると、リクエスト、レスポンスを記録してファイル(テープ)に保存します。2回目以降は、talkbackが記録したテープをもとにレスポンスを返し、本家にはリクエストが飛びません
結果、APIに負荷をかけず、かつネットワークの安定性に左右されずユニットテストが実行できます。

詳細は以下の記事をご覧ください。

ハマった点1: APIリクエスト

node-fetchとJestの相性

Node.jsでは fetch APIが使えなかった2ので、node-fetchを導入しました。

しかし、最新の3系とJestの相性が悪くimportができませんでした。

 SyntaxError: Cannot use import statement outside a module

    > 1 | import fetch from 'node-fetch';
        | ^
      2 |
      3 | test('dummy', () => {
      4 |   fetch('httpbin.org/ip').then((res) => {

2系にダウングレードしたところ上手く動作しました。
モジュールの形式が違うのが原因のようですが、私のTypeScript力が足りず対処できませんでした...

そもそもGASではfetch APIが使えない

さらに根本的な問題として、GASでは fetch ではなく UrlFetchApp.fetch を使う必要がありました。一方 UrlFetchApp.fetch はローカルで動作しないので

  • テスト: fetch
  • 本番: UrlFetchApp.fetch

を差し替えて使っています。

微妙にシグネチャも違うので、Fetcher というインターフェースでラップして、UrlFetchApp.fetchの腐敗防止層を設けています。(かなり無理やりな実装)

TypeScriptでは関数の戻り値が共変なので救われました...3

ハマった点2: スプレッドシート処理のテストができない

スプレッドシートに出力する実装は、GASのクラスなのでローカルではテストできません。しかしモック化するとテストしたい対象自体なくなってしまうので、やむなくテストなしで実装しました。

テストがないので、処理をなるべく減らしています。

スプレッドシートは1ファイルのみ(スクリプトプロパティでIDを決め打ち指定)で、実行のたびに行を追記しています。

スプレッドシートには1000万セルまで作成可能なので、5分に一度実行(3列あるので3セル追記)してもあふれるのに30年かかります。シートを分ける必要は特になさそうです。

おわりに

以上、claspを使ってGASの開発環境を整える方法の紹介でした。

管理しやすくなった部分もある一方、JavaScriptとGASの実装差分に躓いて消化不良の部分もありました...(スプレッドシート追記のユニットテストが書けない等)

いつかJavaScript/TypeScript力を上げてリベンジしたいです。

ここまでお読みいただきありがとうございました!

  1. ドメインモデルは改善の余地がありそうです...(温度、湿度を、タイムスタンプをIDとする集約Environmentにまとめました(公式サイトSwitchBot Meter monitors the environment から採用)が、値オブジェクトの方が適切な気もしています)

  2. v16系現在。v18系から使えるようになる?

  3. GoやJavaのように不変だったら、fetch APIもラップして戻り値を変換する必要がありました。

23
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
23
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?