morishitaです。
Cloud Functions と並ぶ(?)Google のサーバレスな JavaScript 実行環境といえば Google Apps Scripts(GAS)です。
GAS ってあの Excel で言う VB スクリプト環境のようなものでしょう? と思ったあなた!
このエントリでその認識が変わると思います。
以前は使いやすいとは言い難かったGASですが、最近は使いやすくなってきました。 といっても、GAS 自体がアップデートされたのではなく周辺ツールが整備が進み開発・運用しやすい状況が整ってきたからです。
そして、なんと最近Typescript でとても実装しやすくなったので、それをご紹介したいと思います。
google/clasp
以前の GAS は Web エディタ上でしか実装できず、コードを VCS で管理することもままならない状況でしたが、Google からgoogle/claspがリリースされ、状況が改善されました。
これは GAS を管理するための CLI ツールで、Google Drive 上の GAS のコードをローカルに pull したり、逆に Google Drive 上の GAS のプロジェクトに push したりできます。
ということは、Git で管理しながらローカルの使い慣れたエディタでコードを書いて、GAS に push して実行するという開発ができるのです1。
google/clasp が、この平成最後の夏にリリースされた v1.5.0 でなんと Typescript をサポートしたのです。
これまでも Webpack や Babel を使ってトランスパイルして ES6 や Typescript で GAS の開発はできました。
しかし、どんどんバージョンアップする Webpack や Babel に追従しようとしてアップデートするとビルドできなくなるようなトラブルも起こりがちでした。
でも、その苦労から解放されたのです。
少々、前置きが長くなりましたが、実際に使ってみましょう。
@google/clasp のインストールとローカル環境の初期化
google/clasp は Node.js のモジュールです。Node.js 4.7.4 以上が必要なので、用意してください。
Node.jsの準備ができたら、次のコマンドで、ローカル環境を作ります2。
$ mkdir clasp-ts-sample $ cd clasp-ts-sample $ npm init -y $ npm install @google/clasp tslint -D $ npm install @types/google-apps-script -S $ tslint --init # tslint は必須ではありませんが、大人のたしなみとして導入しましょう。
Typescript は明示的にインストールしなくても@google/claspが依存しているのでインストールされます。2018/09/10時点では Typescript 2.9.2がインストールされます。
@types/google-apps-script も導入することにより VSCode 等ではコード補完されるようになります。
SpreadsheetApp
など、GAS 固有のクラス群も定義されています。
素晴らしい!!
GAS プロジェクトの作成
(2018/10/03 clasp 1.6.0を反映した記述に変更しました)
次のコマンドで、GAS プロジェクトのファイルを Google Drive に作成します。その後、生成されたコードをローカルに pull します3。
clasp 1.6.0 で、create
コマンドに --rootDir
オプションが追加されました。
このオプションを使うと、ソースを置くディレクトリを指定できます。
--rootDir
オプションを使ってソースディレクトリを指定しながらプロジェクトを作成します。
$ clasp create clasp-ts-sample --rootDir ./src $ clasp pull
ここまででできたファイル構成は次の通りです。
clasp-ts-sample/ ├── .clasp.json ├── node_modules/ ├── package-lock.json ├── package.json ├── src/ │ ├── Code.ts │ └── appsscript.json └── tslint.json
--rootDir
オプションを使わなければ、srcディレクトリ以下に作成されたファイルはプロジェクトのルートディレクトリに出力されます。
その場合、 clasp push
を実行すると、node_modules以下のすべての JS を読み込もうとして失敗します。
--rootDir
オプションでソースディレクトリを指定してやると、そのディレクトリ以下のファイルだけを見るようになり、うまくclasp push
が動きます。なので、指定することをおすすめします。
--rootDir
オプションを使わずにプロジェクトを作ってしまったら
(clasp 1.5.x までは以下の手順を自分でやる必要がありました)
次の様に.clasp.jsonにrootDir
を追加します。
{ "scriptId": "******-***************************************************", "rootDir": "./src" }
そして、src ディレクトリを作って、clasp push
の対象となるファイルを移動します。
$ mkdir src $ mv appsscript.json src/ $ mv Code.js src/Code.ts
これで--rootDir
オプションを使った場合と同じ状態になります。
Typescript のコードを PUSH してみる
clasp のリポジトリにあるサンプルをコピーして試してみます。
それが次のCode.ts です。alert
を使っていた部分は、GAS では動かないので修正しています。
// 型定義 const isDone: boolean = false; const height: number = 6; const bob: string = "bob"; const list1: number[] = [1, 2, 3]; const list2: number[] = [1, 2, 3]; enum Color { Red, Green, Blue } const c: Color = Color.Green; let notSure: any = 4; notSure = "maybe a string instead"; notSure = false; // okay, definitely a boolean function showMessage(data: string): void { // Void Logger.log(data); } showMessage("hello"); // クラス class Hamburger { constructor() { // コンストラクタ } public listToppings() { // メソッド } } // テンプレート文字列 const name = "Sam"; const age = 42; console.log(`hello my name is ${name}, and I am ${age} years old`); // Rest arguments const add = (a: number, b: number) => a + b; const args = [3, 5]; add(...args); // same as `add(args[0], args[1])`, or `add.apply(null, args)` // スプレッド構文 (array) const cde = ["c", "d", "e"]; const scale = ["a", "b", ...cde, "f", "g"]; // ['a', 'b', 'c', 'd', 'e', 'f', 'g'] // スプレッド構文 (map) const mapABC = { a: 5, b: 6, c: 3 }; const mapABCD = { ...mapABC, d: 7 }; // { a: 5, b: 6, c: 3, d: 7 } // 分割代入 const jane = { firstName: "Jane", lastName: "Doe" }; const john = { firstName: "John", lastName: "Doe", middleName: "Smith" }; function sayName({ firstName, lastName, middleName = "N/A" }) { console.log(`Hello ${firstName} ${middleName} ${lastName}`); } sayName(jane); // -> Hello Jane N/A Doe sayName(john); // -> Helo John Smith Doe // Export (The export keyword is ignored) export const pi = 3.141592; // Google Apps Script の独自サービスの利用 const doc = DocumentApp.create("Hello, world!"); doc .getBody() .appendParagraph("This document was created by Google Apps Script."); // デコレータ(高階関数) function Override(label: string) { return (target: any, key: string) => { Object.defineProperty(target, key, { configurable: false, get: () => label }); }; } class Test { @Override("test") // invokes Override, which returns the decorator public name: string = "pat"; } const t = new Test(); console.log(t.name); // 'test'
どうでしょう、次のような Typescript ならではのものを含むモダンな実装を含んでいます。
- 型アノテーション
- クラス
- テンプレート文字列
- スプレッドオペレータ
- 部分代入
そして、Google Docs を扱う DocumentApp
を利用するコードも含んでいます。
では、Google Drive 上の GAS プロジェクトに push してみましょう。
次のコマンドだけで、自動的にトランスパイルして、GAS に push してくれます。
$ clasp push
tsc
などを使って事前にトランスパイルする必要はありません。
tsconfig.jsonすら用意不要です4。
Javascript のコードを push するように Typescript のコードも push できます。
続いて GAS プロジェクトに push されたコードを見てみましょう。
clasp open
コマンドを実行すると Google Drive 上の GAS プロジェクトがブラウザで開きます。
次の様にファイル Code.gs としてトランスパイルされています。
var exports = exports || {}; var module = module || { exports: exports }; var __assign = (this && this.__assign) || Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; var __decorate = (this && this.__decorate) || function(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; // 型定義 var isDone = false; var height = 6; var bob = "bob"; var list1 = [1, 2, 3]; var list2 = [1, 2, 3]; var Color; (function(Color) { Color[(Color["Red"] = 0)] = "Red"; Color[(Color["Green"] = 1)] = "Green"; Color[(Color["Blue"] = 2)] = "Blue"; })(Color || (Color = {})); var c = Color.Green; var notSure = 4; notSure = "maybe a string instead"; notSure = false; // okay, definitely a boolean function showMessage(data) { Logger.log(data); } showMessage("hello"); // Classes var Hamburger = /** @class */ (function() { function Hamburger() { // コンストラクタ } Hamburger.prototype.listToppings = function() { // メソッド }; return Hamburger; })(); // テンプレート文字列 var name = "Sam"; var age = 42; console.log("hello my name is " + name + ", and I am " + age + " years old"); // Rest arguments var add = function(a, b) { return a + b; }; var args = [3, 5]; add.apply(void 0, args); // same as `add(args[0], args[1])`, or `add.apply(null, args)` // スプレッド構文 (array) var cde = ["c", "d", "e"]; var scale = ["a", "b"].concat(cde, ["f", "g"]); // ['a', 'b', 'c', 'd', 'e', 'f', 'g'] // スプレッド構文 (map) var mapABC = { a: 5, b: 6, c: 3 }; var mapABCD = __assign({}, mapABC, { d: 7 }); // { a: 5, b: 6, c: 3, d: 7 } // 部分代入 var jane = { firstName: "Jane", lastName: "Doe" }; var john = { firstName: "John", lastName: "Doe", middleName: "Smith" }; function sayName(_a) { var firstName = _a.firstName, lastName = _a.lastName, _b = _a.middleName, middleName = _b === void 0 ? "N/A" : _b; console.log("Hello " + firstName + " " + middleName + " " + lastName); } sayName(jane); // -> Hello Jane N/A Doe sayName(john); // -> Helo John Smith Doe // Export (The export keyword is ignored) exports.pi = 3.141592; // Google Apps Script の独自サービスの利用 var doc = DocumentApp.create("Hello, world!"); doc .getBody() .appendParagraph("This document was created by Google Apps Script."); // デコレータ(高階関数) function Override(label) { return function(target, key) { Object.defineProperty(target, key, { configurable: false, get: function() { return label; } }); }; } var Test = /** @class */ (function() { function Test() { this.name = "pat"; } __decorate( [ Override("test") // invokes Override, which returns the decorator ], Test.prototype, "name" ); return Test; })(); var t = new Test(); console.log(t.name); // 'test'
動作確認
GAS の Web エディターでは 3 つの関数が実行対象として選択できると思います。 その中から試しにOverrideを実行してみます。 Override以外の関数は実行されませんが、関数外の部分は実行されます。 もちろんちゃんと動きます。
console.log
の出力はStackdriver Loggingに次のように出力されます。
また、DocumentApp.create
して、中に文字列を書き込んでいる部分がありますが、
その出力として次のようなGoogle Docのファイルが Google Driveの中に作成されます。
とても簡単です。
また、clasp push
にはwatchモードまであります。
次のコマンドを実行しておけば、コードの変更を検知すると自動的に再 push してくれます。
$ clasp push --watch
これで実装->実行->また実装 のサイクルが少し楽になりますね。
まとめ
どうでしょう、これまで GAS を使ってきた方には、今までのやり方がバカバカしくなるほど簡単に Typescript で実装できることがおわかりいただけたと思います。
もう Typescript で GAS を実装しない理由が見当らないでしょう?
GAS は Cloud Functions に比べると制約が多く、Google Drive 上のアプリケーションの拡張用と思われがちです。
しかし次のような特徴を備えており、ユースケース次第では大変便利に使えるサービスです。
- Sheets や Docs、Slides といった Google Drive 上のアプリケーションにアクセスしやすい
- Gmail、BigQuery や Analytics などの一部の Google のサービスを利用でき、しかも SDK よりも手軽に使えるものもある
- Web アプリケーションも作れる
- 定期実行可能
- そして、無料5
特に、BigQuery や Analytics のデータを集計して、レポートを作成する作業を自動化するには最も便利な環境だと思います。 SheetsやSlidesのファイルとしてGoogle Drive上に出力するのが簡単ですし、Gmail経由でメールも出せますし、定期実行できますし。
また、去年次の 2 つが使えるようになり、ますます運用しやすくなりました。
- Apps Script dashboard
- GAS 専用の管理ダッシュボード。
- Google Drive に散らかりがちな GAS プロジェクトを一元管理できます。
- Sheets ファイルなどに含まれる container-bound な GAS プロジェクトも管理できます。
- Stackdriver Logging
- 汎用のロギングサービス。
console.log
等の出力がログとして記録されます。- デバッグや実行状況の確認が格段にやりやすくなりました。
うまく使えば業務の効率化に大いに役立ってくれる GAS を Typescript でモダンに開発しましょう。
参考
- Command Line Interface using clasp | Apps Script | Google Developers
- @google/clasp/typescript
- The Apps Script Dashboard | Apps Script | Google Developers
- Logging | Apps Script | Google Developers
最後に
アクトインディでは エンジニアを募集しています。
-
このような開発スタイルを最初に実現し、エポックメイキングなツールだったnode-google-apps-scriptはすでにディスコンとなっています。↩
-
clasp
はnpm install -g @google/clasp
でグローバルにインストールしてもいいのですが、私はndenv
で複数バージョンの Node.js をインストールしており、プロジェクトごとに Node.js のバージョンが異なったりします。それで、グローバルなインストールは避けています。代わりに、./node_module/.bin
を PATH に追加してプロジェクトディレクトリにインストールしたコマンドを実行できるようにしています。↩ -
これまで、
clasp
を使ったことがなければ、ログインと、API の有効化が必要になります。参考:GAS の Google 謹製 CLI ツール clasp↩ -
clasp
はts2gasを利用してトランスパイルしています。コンパイルオプションはこちら =>compilerOptions。↩ -
GAS のスクリプトの実行自体は無料ですが、有料サービスの API 呼び出た場合、別途課金されます。↩