見出し画像

Google Apps Scriptの開発体験を向上させる Now in REALITY Tech #114

こんにちは、REALITYのサーバチームのあさだです。今回のNow in REALITY Tech記事では、以前のALさんの記事に引き続き、Google Apps Script(GAS)について、快適な開発をするためのREALITYでの取り組みをご紹介します。


GASについて

GASは非常に便利なアプリケーション開発プラットフォームで、REALITYでは主に、Google スプレッドシートと組み合わせ、マスタデータの管理やGoogle Cloud BigQueryを使ったイベント結果の集計、Slackなどのコミュニケーションツールと連携してタスクの自動リマインドを行うなど、多岐に渡って利用されています。

開発するうえでは、ブラウザ上でのコードエディタも提供されており、コードベースであるJavaScriptなどの言語知識があれば、簡単な機能ならすぐに実装をすることができます。
ですが、少しでも複雑な機能を実装をしようとしたりすると、現状のブラウザエディタでは開発がしづらい場面が出てきます。
今回の記事で、GASの開発体験が少しでも改善できれば幸いです。

実行環境

以降は、下記のnodeバージョンがインストールされている前提となります。

node -v                                                                                                                                                               ─╯
v20.9.0

npm --version                                                                                                                                                         ─╯
10.1.0

ローカルでの開発とバージョン管理

GASはブラウザ上で開発するためのエディタを提供しています。

しかし、VS CodeやVimなどの使い慣れたエディタで開発したいという人もいると思います。
そこでまず、「Clasp」というツールについてご紹介します。

Claspについて

ClaspはGASプロジェクトをローカル環境で管理するためのコマンドラインツールを提供してくれます。こちらを使うことにより、GASをローカルで開発することができ、バージョン管理したり、任意のエディタを利用して開発を行うことができます。
以下に、使い方の例を少しだけ紹介させていただきます。

Claspのインストール

READMEに記載の通り、下記のコマンドをターミナルに入力して、グローバルにインストールしておきます。

$ npm install -g @google/clasp

こちら(https://script.google.com/home/usersettings)にアクセスし、APIの設定を有効にしておけばClaspを利用する準備が整います。

GAS APIの設定

Claspのセットアップ

下記のコマンドをターミナルに入力すると、既定ブラウザが開き、Clapsツールを利用するためのGoogle アカウントのログインと権限のリクエストが求められますので、問題なければ許可してください。

$ clasp login

ClapsでGASプロジェクトを作成

プロジェクト用のディレクトリ「SampleForNote」を作成し「clasp create」コマンドをターミナルに入力すれば「SampleForNote」という名称でプロジェクトが作成されます。
「Create which script?」と聞かれますので今回は「sheets」を選択します。
こちらを選択することで「Google スプレッドシート」用のGASプロジェクトが用意されます。
正常に実行が完了すれば、スプレッドシートとそれに紐づくGASのURLが出力されますので、後々使うため控えておきます。

$ mkdir SampleForNote
$ cd SampleForNote
$ clasp create SampleForNote
? Create which script?
  standalone
  docs
❯ sheets
  slides
  forms
  webapp
  api
・・・
Created new Google Sheet: https://drive.google.com/open?id=******************************
Created new Google Sheets Add-on script: https://script.google.com/d/******************************/edit
・・・

ClaspでローカルコードをGASに反映する

ここまで問題なければ「SampleForNote」ディレクトリの直下には、以下のようなファイルが生成されています。

SampleForNote/
├── .clasp.json
└── appsscript.json

.clasp.jsonには、以下の内容になっていて、GASへのビルドターゲットの設定などが書かれています。
「rootDir」がこのディレクトリへの絶対パスになっていると思いますので、「./src」などの相対パスに変更しておくとよいかと思います。

{
  "scriptId": "****************************************",
  "rootDir": "./src",
  "parentId": ["****************************************"]
}

appscript.jsonを開くと以下のようなGASの設定ファイルがありますので、
「timeZone」が「America/New_York」になっている場合は「Asia/Tokyo」にしておくと良いです。

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

「SampleForNote」に「src」ディレクトリを作成し、直下に「Code.js」というファイル名で、下記のサンプルコードを保存しておきます。

$ mkdir src
$ cd src
// src/Code.js

function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu("メニュー").addItem("実行", "myFunction").addToUi();
}

function myFunction() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  ss.toast("Hello World!");
}

下記のようなファイル構造になっていると思いますので、「clasp push」コマンドを実行します。

SampleForNote/
├── .clasp.json
├── appsscript.json
└── src/
    └── Code.js
$ clasp push

特にここまで問題がなければ、「clasp create」を実行後に出力された
「https://script.google.com/d/***/edit」の方のURLを開くか、下記コマンドを実行すると、

$ clasp open

GASのプロジェクトが開き、下記のように「Code.gs」というファイルが配置されているはずです。

また、「https://drive.google.com/open?id=***」の方のURLを開けば、
下記のようにGASが適用されたスプレッドシートが開かれます。
「メニュー」が追加され、「実行」を選択すると、「Hello World!」のトーストが表示されます。

これで、GASをローカルで開発するための準備が整いました。

Typescriptでの実装とバンドル

先程のClaspでは、JavaScriptのコードを例に使いました。
JavaScriptはとても柔軟な言語ですが、バグ発生時にコードからバグを見つけるのが難しかったりしますので、TypeScriptでコーディングできると便利です。

Claspを利用すると、TypeScriptで記述されたtsファイルは自動的にJavaScriptのコードにトランスパイルされ、gsファイルに変換されたものをGASに反映することができます。
また、複数のgsファイルを反映することも可能で、GAS上では複数のgsファイルはまとめて1つのファイルのように扱われます。
しかし、逆に「import/export」などを使うことができないため、コードを複数ファイルに分割した際に定義参照などがしづらくなります。
ClaspとTypeScriptの併用の際の注意点はこちらに記載されています。
https://github.com/google/clasp/blob/master/docs/typescript.md

esbuildを使ったバンドル化の例

GASとClaspで、複数のファイルを扱うための解決方法として、最終的にコードを一つのファイルにバンドルしてGASに反映する方法があります。
今回は「esbuild」を使ったバンドルの例をご紹介します。

まずは、先程利用した「SampleForNote」のディレクトリに移動しnpmのプロジェクト環境を用意しておきます。

$ npm init -y 

typescriptとesbuild及び、GAS用のesbuildのプラグインとGASの型定義パッケージをインストールします。

$ npm install -D typescript esbuild esbuild-gas-plugin @types/google-apps-script

「SampleForNote」直下に、「tsconfig.json」を作成し、以下の内容に変更します。
(https://github.com/google/clasp/blob/master/docs/typescript.mdより)

{
  "compilerOptions": {
    "lib": ["esnext"],
    "experimentalDecorators": true
  }
}

「SampleForNote」直下に「build.js」を作成し、以下の内容に変更します。
(https://github.com/mahaker/esbuild-gas-pluginより)

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

require('esbuild').build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  outfile: 'dist/bundle.js',
  plugins: [GasPlugin]
}).catch((e) => {
  console.error(e)
  process.exit(1)
})

「SampleForNote」の「src」直下にビルド用の「index.ts」「sub.ts」ファイルを作成し、以下の内容に変更します。
REALITYのTypeScriptのコード規約だとanyは推奨していないので、実際に書くときはインターフェースを定義しています。

// src/index.ts
import { subFunction } from "./sub"

declare const global: any;

global.onOpen = () => {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu("メニュー").addItem("実行", "myFunction").addToUi();
}

global.myFunction = () => {
  subFunction();
}
// src/sub.ts

export const subFunction = () => {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  ss.toast("Hello World!");
}

こちらのコマンドで、バンドル化することができます。

$ node build.js

今回、バンドル化ファイルを「dist」ディレクトリに出力するようにしたので、下記のように「.clasp.json」の「rootDir」を修正しておきます。

{
  "scriptId": "****************************************",
  "rootDir": "./dist",
  "parentId": ["****************************************"]
}

バンドルされたjsファイルが出力された時に、「dist」ディレクトリの中身は削除されてしまうので、package.jsonに以下のようなスクリプトを書きます。
また、本番・開発環境などを分けたい場合は、Claspプロジェクトを新しく作成し、「.clasp.json」をリネームして複数ファイル用意しておき、npmスクリプトで切り替えるなどをしています。



・・・
"scripts": {
    "push": "cp ./appsscript.json ./dist/appsscript.json && node ./build.js && clasp push",
    "prod": "cp ./.prod.clasp.json ./.clasp.json",
    "lab": "cp ./.lab.clasp.json ./.clasp.json"
  },
・・・

ここまでのファイル構造は以下のようになっています。

SampleForNote/
├── .clasp.json
├── .lab.clasp.json
├── .prod.clasp.json
├── appsscript.json
├── build.js
├── dist/
├── node_modules/
├── package-lock.json
├── package.json
├── src/
│   ├── index.ts
│   └── sub.ts
└── tsconfig.json

esbuild-gas-pluginについて

通常、esbuild単体でビルドすると、以下のように、グローバル汚染を回避するため、即時実行される無名関数内に入れられてしまいます。GASではトップレベルに配置された関数がトリガーなどで実行できる関数として認識されるため、このままでは実行することができません。

(() => {
  // src/sub.ts
  var subFunction = () => {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    ss.toast("Hello World!");
  };

  // src/index.ts
  global.onOpen = () => {
    const ui = SpreadsheetApp.getUi();
    ui.createMenu("\u30E1\u30CB\u30E5\u30FC").addItem("\u5B9F\u884C", "myFunction").addToUi();
  };
  global.myFunction = () => {
    subFunction();
  };
})();

「esbuild-gas-plugin」を使用することで、global変数のプロパティに実装した空の関数をトップレベルに配置し直すことができます。これらのトップレベルに配置された関数はGAS上で呼び出せる関数として認識され、実行時には無名関数内の処理で上書きされるようになります。

let global = this;
// src/index.ts
function onOpen() {
}
function myFunction() {
}
(() => {
  // src/sub.ts
  var subFunction = () => {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    ss.toast("Hello World!");
  };

  // src/index.ts
  global.onOpen = () => {
    const ui = SpreadsheetApp.getUi();
    ui.createMenu("\u30E1\u30CB\u30E5\u30FC").addItem("\u5B9F\u884C", "myFunction").addToUi();
  };
  global.myFunction = () => {
    subFunction();
  };
})();

これで、下記のようにGAS上で実行できる関数として認識されるようになりました。

ここまでの実装内容は以下の記事を参考にさせていただいております。
とても助かりました。

フロントエンドをReactで書けるようにする

スプレッドシートでは、GASを使ってダイアログなどのUIを表示することもでき、シート以外の入力フォームを表示したり、処理の結果を表示することもできます。
htmlファイルをGASに配置することで表示が可能になりますが、こちらはReactなどで実装することもでき、リッチなUIを表示できます。
Reactのバンドル時に、node-polyfillsなどのプラグインを利用することで、npmパッケージの機能を含めることも可能です。
Reactを利用する際は、こちらの記事を参考にして実装させていただきました。

gas-clientでフロントエンド側からサーバ側のスクリプトを呼び出しやすくする

GASではフロントエンド側からサーバ側のスクリプトを呼び出すために、「google.script.run」という関数を使用する必要がありますが、下記のプラグインを利用することで簡易に記述することができるのでおすすめです。

その他役立たせていただいているもの

最後にこちらのリンクをご紹介させていただきます。こちらのページには、GASでできることや、今回紹介したプラグインを含む、便利なプラグインなどがまとまっており、大変参考にさせていただいています。

まとめ

今回は、GASを利用するうえで、便利だったツールや構築をいくつか紹介させていただきました。
REALITYでは、サービスを運用する上でGoogleスプレッドシートを介したデータのやり取りをしており、人力による手入力が数多く存在しています。そのため、GASなどを併用してできる限り作業の自動化に取り組んでおります。
この取り組みを通して、REALITYでより多くの体験を皆様に届けられればと思います。