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。
ということは、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 等ではコード補完されるようになります。
@types/google-apps-script も導入することにより VSCode 等ではコード補完されるようになります。
SpreadsheetApp
など、GAS 固有のクラス群も定義されています。素晴らしい!!
GAS プロジェクトの作成
次のコマンドで、GAS プロジェクトのファイルを Google Drive に作成します。その後、生成されたコードをローカルに pull します3。
$ clasp create clasp-ts-sample $ clasp pull
ここまででできたファイル構成は次の通りです。
clasp-ts-sample/ ├── .clasp.json ├── node_modules/ ├── package-lock.json ├── package.json ├── Code.js ├── appsscript.json └── tslint.json
rootDir
を設定し、ソースファイルを src に移動する
実装を開始する前に、環境準備にもうひと手間かけます。
というのも、このまま、
.claspignore を作って無視してやることもできますが、オススメは .clasp.json に
次の様に.clasp.jsonに
clasp push
を実行すると、node_modules以下のすべての JS を読み込もうとして失敗します。.claspignore を作って無視してやることもできますが、オススメは .clasp.json に
rootDir
を定義する方法です。次の様に.clasp.jsonに
rootDir
を追加します。{ "scriptId": "******-***************************************************", "rootDir": "src" }
そして、src ディレクトリを作って、
clasp push
の対象となるファイルを移動します。$ mkdir src $ mv appsscript.json src/ $ mv Code.js src/Code.ts
これで準備は終了。次のようなファイル構成になります。
clasp-ts-sample/ ├── .clasp.json ├── node_modules/ ├── package-lock.json ├── package.json ├── src/ │ ├── Code.ts │ └── appsscript.json └── tslint.json
Typescript のコードを PUSH してみる
// 型定義 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 に追加してプロジェクトディレクトリにインストールしたコマンドを実行できるようにしています。↩ -
GAS のスクリプトの実行自体は無料ですが、有料サービスの API 呼び出た場合、別途課金されます。↩