今から10分ではじめる Google Apps Script(GAS) で Web API公開

# 概要

- [**Google Apps Script (GAS)**](https://developers.google.com/apps-script/)で、Web API(無料、サーバーレス)を公開する方法についてのメモです
- **Google Apps Script (GAS)**をローカルで開発したい、Gitでコード管理したい、クラスを使いたい、TypeScriptを使いたい、などのやり方も後半簡単に紹介します

## Google Apps Script (GAS) の位置づけを理解してみる

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/f48beadb-e7e0-6b8d-d37f-349f22d4bd6c.png)


- **Google Apps Script**は**G Suite(GmailやスプレッドシートなどGoogleサービス)を JavaScriptで制御できる仕組み**。だから、Microsoft OfficeのVBAのGoogleサービス版みたいな感じ。

- **Google Apps Script**は**JavaScriptで書いた処理をWebAPIとして公開できる**ので、AWS LambdaとかGoogle Functionのようなサーバレスなコード実行環境のようにも使える<font color=blue>**←今回はこの用途で使いたい**</font>

- **Google Apps Script**は[JavaScript 1.6(2005年11月)がベース](https://developers.google.com/apps-script/overview)なので<font color=red>**モダンなJavaScriptで記述することはできない**</font>。かつブラウザ系機能であるDOM API等は無い。ただし、**ツールをこねくり回せば今風なコードで書くこともできる**。

- **Google Apps Script**は**無料で使える**が**従量課金のような考え方では無い**ので、<font color=red>**お金を払っても無限に使えない**</font>ということで[容量制限](https://developers.google.com/apps-script/guides/services/quotas)がある。

# Google Apps Script のプロジェクトを作る

## Google Apps Scriptのトップページにいく

Googleアカウントにログインした状態で以下に行く

https://script.google.com

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/698fb0fb-3bb6-a5c7-ff3a-c91752552495.png)


## 最初のプロジェクトを作る

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/029dd9c1-cd4f-3bd0-d8f0-1070f6120a27.png)


**[APPS SCRIPTを作成]**をクリックすると、プロジェクトが作成される。

**無題のプロジェクト**　というプロジェクトと**コード.gs**というファイルができるので、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/423480ba-422c-af39-a2b8-0a038d9050fd.png)

画面左上の**無題のプロジェクト**をクリックして、プロジェクト名を**webapi_example01**に変更した。


さらに、↓のように、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/e59029dc-117a-2357-c5e3-a53cd728378e.png)

左側ペインに**コード.gs**というファイルがあるのがわかる。これが最初のファイルとなる。ファイル名の右にある**[▼]**を押してメニューから**名前を変更**をクリックして**main.gs**というファイル名に変更した。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/a122bb44-71b8-5948-f908-3de04ea38385.png)


# コードを書いてWeb APIを公開する

さっそくコードを書いて、WebAPIを公開する。コードはJavaScriptで書く。
Google Apps Script はJavaScriptの最新をサポートしているわけでも、すべての機能をサポートしているわけでもないが、JavaScriptが書ける人ならなんとかなる。

## Web APIをつくる

デフォルトだと以下のようになっているが、Web APIとして公開したいので、GETやPOSTをハンドリングできるようにする。

```javascript:main.js
function myFunction(){
}
```

関数myFunctionは消して、以下のようにした

```javascript:main.js
//HTTP GETをハンドリングする
function doGet(e) {

    //リクエストパラメータ名"text"の値を取得する
    var text = e.parameter.text;
    var value;
    if (text) {
        value = "You say " + text;
    } else {
        value = "Please say something!";
    }
    var result = {
        message: value
    }

    var out = ContentService.createTextOutput();

    //Mime TypeをJSONに設定
    out.setMimeType(ContentService.MimeType.JSON);

    //JSONテキストをセットする
    out.setContent(JSON.stringify(result));

    return out;
}

```

シンプルなコードだが、いくつかみておく。
コード中のdoGetで渡される引数**e**は、リクエストパラメータ等のイベントパラメータが入る。

```javascript
function doGet(e) {
var text = e.parameter.text;
```

e.parameterはリクエストパラメータがkey/valueペアとして入るので、
Google Apps ScriptでWeb公開したURLで```https://xxxxxx/exec?text=Hello```とリクエストされたら e.parameter.textには"Hello"が入ることになる。

リクエストパラメータについて詳しくは[こちら](https://developers.google.com/apps-script/guides/web)に書いてある。

以下の [ContentService](https://developers.google.com/apps-script/reference/content/content-service) はテキストコンテンツを返すためのサービスで #createTextoutput　で [Textoutput](https://developers.google.com/apps-script/reference/content/text-output)オブジェクトを生成する。

```javascript
var out = ContentService.createTextOutput();
```

Textoutputオブジェクト **out**に対して ```#setMimeType```でJSONを指定し、```#setContent```で実際に返すテキスト（この場合は JSON形式のテキストにして）をセットする。

```javascript
out.setMimeType(ContentService.MimeType.JSON);
out.setContent(JSON.stringify(result));
return out;
```

これでTextoutputオブジェクト **out**を return すればOK。
セキュリティを考慮してスクリプトがダイレクトにテキストをブラウザに返すことはなく、実際には、ブラウザは googleusercontent.com にリダイレクトされ、そこから結果を受け取る、という構造になっている。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/c36e5afa-88f4-62e3-f719-d6f42423664c.png)


これで、GETメソッドでアクセスすると、JSONを返すWeb APIができた。
早速公開してみる。

## Web APIの公開

上部メニューから**公開**>**Webアプリケーションとして導入** を選択する。


![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/aa3a87c4-c580-f453-6eb2-18fb8c0d6866.png)

するとダイアログがでる、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/9af44b2a-5449-759c-ba9d-7ca6b43b4323.png)

アプリケーションにアクセス出来るユーザーを **全員(匿名ユーザーを含む)** にする

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/8fbc878b-6365-381d-81dc-7cb652819ebc.png)

**[導入]**をクリックすると、**現在のウェブアプリケーションのURL**が表示されるので、そのURLをコピーしてブラウザからアクセスしてみる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/0da3659f-905e-e232-55da-1548511dffad.png)

アクセス結果

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/74068471-f6a9-c6dd-79ca-d2c927839fde.png)

無事、レスポンスが返ってきた。
シンプルだけど、これでWeb API公開に成功した。

# ブラウザからこのAPIをたたきたい

ブラウザからAPIをたたくということは、まず**クロスドメイン大丈夫か？**ということが頭をよぎる。

つまりGoogle Apps Script で公開したWeb APIはCORS(Cross-Origin Resource Sharing)を OKにできるのか？
→~~Google Apps Script で公開したWeb APIはCORSできない~~(以前はでききなかった)
→ Google Apps Script は CORS に対応している。その証拠にヘッダに **access-control-allow-origin:＊**がセットされている。<font color=red>ただし、うまく行かないパターンがあるので注意</font>

<font color=red>**うまく行かないパターン**</font>
・JSONテキストをまるごと POST するような場合は エラーとなる
　→なぜなら、JSONテキストの content-typeが application/json となるため。
　→なぜ application/json だとエラーとなるかというと、 application/jsonの場合は **preflightリクエスト**が発生するため
　→なぜなら、Google Apps Script は <font color=red>OPTION メソッド</font>をサポートしていないため。
　

~~つまり異なるドメイン（オリジン）からAjaxはGETもPOSTもできない。~~
ブラウザ上で動くスクリプトから **preflightリクエスト**が発生するようなリクエストを使いたければ、同じドメインにHTMLを配備するか、別ドメインにHTMLを配備したうえで、**JSONP**を使うしか手がないようだ。

## Web APIをJSONP対応化する

ということで、別ドメインにHTMLを置く前提でさきほどのコードをJSONPに書き換える

```javascript:main.js
//HTTP GETをハンドリングする
function doGet(e) {

    //リクエストパラメータ名"text"の値を取得する
    var text = e.parameter.text;

    var value;

    if (text) {
        value = "You say " + text;
    } else {
        value = "Please say something!";
    }

    var result = {
        message: value
    }

    var responseText;

    var out = ContentService.createTextOutput();

    var callback = e.parameter.callback;

    if (callback) {
        responseText = callback + "(" + JSON.stringify(result) + ")";
        //Mime Typeをapplication/javascriptに設定
        out.setMimeType(ContentService.MimeType.JAVASCRIPT);

    } else {
        responseText = JSON.stringify(result);
        //Mime Typeをapplication/jsonに設定
        out.setMimeType(ContentService.MimeType.JSON);
    }


    //JSONPテキストをセットする
    out.setContent(responseText);

    return out;
}

```


```javascript
 var callback = e.parameter.callback;

    if (callback) {
        responseText = callback + "(" + JSON.stringify(result) + ")";
        //Mime Typeをapplication/javascriptに設定
        out.setMimeType(ContentService.MimeType.JAVASCRIPT);

    } else {
        responseText = JSON.stringify(result);
        //Mime Typeをapplication/jsonに設定
        out.setMimeType(ContentService.MimeType.JSON);
    }

```

リクエストパラメータに **callback**がついていたら、JSONPで返すようにした。

（[本家サイト](https://developers.google.com/apps-script/guides/content#serving_jsonp_in_web_pages)にも注意があるが、JSONPをつかうなら脆弱性に関して注意深く設計する必要がある。提供する情報はリードオンリーかつ誰にでも公開できるような情報のみとし、センシティブな情報はゼッタイ入れない）


## 公開しているWeb APIを更新する

**公開**>**Webアプリケーションとして導入** を選択していまつくったJSONP対応コードに更新する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/ce3bf5fc-d15f-ff91-948d-50ca4fd90183.png)

新しいバージョンを公開したいときは、プロジェクトバージョンを**新規作成**する。
**変更内容**は入力しなくてもよい。

さっきのが バージョン**1** だったので、次は自動的に **2** になる。

## HTMLをつくって、ブラウザからアクセスする

以下のような htmlファイルを作る。
今回は、JSONPへのアクセス用にjQueryを使った。

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>GAS Client Example</title>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
        integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
        crossorigin="anonymous"></script>
<script>
    const endpoint = "https://script.google.com/macros/s/xxxxxxxxxx/exec";
    $.ajax({
        type: 'GET',
        url: endpoint,
        dataType: 'jsonp',
        data: {
            text: 'Hi,There!'
        },
        success: out => {
            alert(out.message);
        }
    });
</script>
</body>
</html>
```

```javascript
const endpoint = "https://script.google.com/macros/s/xxxxxxxxxx/exec";
```

この部分には、さきほどの **公開**>**Webアプリケーションとして導入**で確認できる現在のWebアプリケーションのURLを入力する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/88e53496-df4b-4d4c-1c17-81712658720a.png)

さて、いまつくった index.html をブラウザで開いてみる。
ローカルファイルでも開ける。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/95f33feb-ff0c-a390-8ed7-476caa3303aa.png)

うまく呼び出すことができた。

これで、シンプルなWeb APIがお手軽に作れた。

# デバッグ、テストの方法

デバッグの方法はいろいろあるがいちばん準備が少なくて良いのは、デフォルトで準備されているスクリプトエディタとなる。

## スクリプトエディタ上で実行する

ブラウザ上から編集できるスクリプトエディタには、スクリプトの実行機能やデバッグ機能がついているので実行してみる

ここでは、**doGet**を呼び出すために以下のようなコードを main.gs に追加した。

```javascript:main.gsへの追加分

function testLogic() {
    var e =
        {
            "parameter": {
                "text": "hello",
                "callback": "myCallbackFunc"
            }
        };
    var out = doGet(e);
    var content = out.getContent();
    Logger.log("content=" + content);
}
```

**#doGet**のロジックがちゃんと動作しているのか、この**#testLogic**関数をつかって確かめてみる。

以下のように実行したい関数を選択できるので、 **testLogic**を選択して、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/06d01333-abf3-6da4-f11d-a73bf5309302.png)

実行ボタン![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/470cd3df-3e33-88b3-c0e7-f1760fee3686.png)をクリックすると実行される。

**表示**>**ログ**でログ画面を表示することができるので、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/08bb0c23-a0ab-07ab-fbf0-498d96977da1.png)

ログ画面を表示してみた。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/bb8463b6-a325-bc25-9782-7b43b432fc02.png)

ちゃんと動作している模様。

## デバッグ機能を使う

今のはふつうに実行だったが、今度はデバッグ機能![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/3e27c294-46d3-5bc0-d002-4572936a23e8.png)をつかう。

デバッグ実行するまえに、プログラムの実行を一時停止するブレークポイントを設定する。

コードの行番号のところを、マウスでポチっとすると、赤丸<font color=red>●</font>がついて、ここにブレークポイントがセットされる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/aa7004b9-4749-00fd-f36b-8a7118965f97.png)

この状態で[image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/3e27c294-46d3-5bc0-d002-4572936a23e8.png)をクリックする

すると、以下のような感じで、ブレークポイントまで実行され、ウォッチ画面が表示される。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/f7977b66-c06d-d071-3076-4b4d7d35aa65.png)

一時停止や再開、ステップ実行などひととおりの機能がブラウザベースでできるので、なかなか便利。

## Google Apps Scriptから自分自身を呼び出す

main.gsに以下を追加する。

```javascript:main.gs追加分

function testGet() {

    var url = ScriptApp.getService().getUrl() + "?text=Hello&callback=myFunc001";
    Logger.log("url=" + url);

    var options = {
        "method": "GET",
        "followRedirects": true,
    };

    var response = UrlFetchApp.fetch(url, options);
    Logger.log("response=" + response);
}
```

このコードは、自分自身を呼び出してみる関数になっている。

```javascript
var url=ScriptApp.getService().getUrl() ・・・
```

[**ScriptApp.getService().getUrl()**](https://developers.google.com/apps-script/reference/script/service)で自分自身のURLを取得する


```javascript
    var options = {
        "method": "GET",
        "followRedirects": true,
    };
    var response = UrlFetchApp.fetch(url, options);
```
こちらは、実際に GETメソッドで外部サービス（といっても自分自身）を呼び出しにいっている。外部サービスに接続にいくときは、 [**UrlFetchApp**](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app)を使う。

ここまでのコード(**main.gs**)は以下のようになっている。

```javascript:main.gs
//HTTP GETをハンドリングする
function doGet(e) {

    //リクエストパラメータ名"text"の値を取得する
    var text = e.parameter.text;

    var value;

    if (text) {
        value = "You say " + text;
    } else {
        value = "Please say something!";
    }

    var result = {
        message: value
    }

    var responseText;

    var out = ContentService.createTextOutput();

    var callback = e.parameter.callback;

    if (callback) {
        responseText = callback + "(" + JSON.stringify(result) + ")";
        //Mime Typeをapplication/javascriptに設定
        out.setMimeType(ContentService.MimeType.JAVASCRIPT);

    } else {
        responseText = JSON.stringify(result);
        //Mime Typeをapplication/jsonに設定
        out.setMimeType(ContentService.MimeType.JSON);
    }


    //JSONPテキストをセットする
    out.setContent(responseText);

    return out;
}

function testLogic() {
    var e = {
        "parameter": {
            "text": "hello",
            "callback": "myCallbackFunc"
        }
    };
    var out = doGet(e);
    var content = out.getContent();
    Logger.log("content=" + content);
}

function testGet() {

    var url = ScriptApp.getService().getUrl() + "?text=Hello&callback=myFunc001";
    Logger.log("url=" + url);

    var options = {
        "method": "GET",
        "followRedirects": true,
    };

    var response = UrlFetchApp.fetch(url, options);
    Logger.log("response=" + response);
}
```

さて、実行対象関数を **testGet**に指定して、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/0919245a-b447-d173-f674-e33a4e0b1a4b.png)


実行![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/934990e4-a0a5-5d0e-aa6b-74f7bcd8657a.png)してみると、こんなダイアログがでてきた。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/a3acc8ce-d9a8-7f89-2ca5-3b3b55608c71.png)

**許可を確認**をクリックすると、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/70743947-70f7-bdbf-a85e-4f29be775128.png)

認可画面がでてくるので、内容確認して **許可**をクリック。

実行が終わった後に、ログ画面を表示してみると、きちんと動作していた。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/331a1335-e5ac-bed0-562c-ec73e4f6d30d.png)

ここまででひとまず基本的な使い方は理解できた。

# ローカルで開発したい、Gitでコード管理したい、クラスを使いたい、TypeScriptを使いたい、などの要求（欲求）とやり方

Google Apps Scriptにも効率化やモダン？な開発を促進するための各種ソリューションを先人たちが親切にも準備してくれているのでキーワードだけカンタンに整理しておく。

## ローカルで開発したい！
要求：ブラウザ上のエディタじゃものたりません、ローカルで開発したいです

- そんなときは、ローカルでの開発を助けてくれる[**clasp**](https://github.com/google/clasp)。GITに似たコマンド体系で操作できる。

インストール

```shell
npm i @google/clasp -g
```

インストールしたらログインする。Googleのアカウントで入る。

```shell
clasp login
```

ローカルにプロジェクトを落としてくる

```shell
clasp clone [scriptId/scriptURL]
```

```shell:例
clasp clone "15ImUCpyi1Jsd8yF8Z6wey_7cw793CymWTLxOqwMka3P1CzE5hQun6qiC"
clasp clone "https://script.google.com/d/15ImUCpyi1Jsd8yF8Z6wey_7cw793CymWTLxOqwMka3P1CzE5hQun6qiC/edit"
```

pullする。バージョン指定も可能。

```shell
clasp pull
clasp pull -versionNumber 2
```

pushする。

```
clasp push
```

claspでローカルに落としてきたものをGitHubで管理したりできそう。

## GitHubでコード管理したい！
要求：Google Apps ScriptのコードはGoogle Driveに保存されるが、あっぱりGitHub使いたい！

- そんなときは、[Google Apps Script GitHub アシスタント](https://chrome.google.com/webstore/detail/lfjcgcmkmjjlieihflfhjopckgpelofo)

chrome拡張として提供されている。
前節でふれたclaspをつかってローカル→GitHubでもいいし、chrome拡張つかってもよさそう。

## クラスを使って書きたい！
要求：クラスを使いたい！

- そんなときは、まずES5のprototypeをつかったクラス風で満足できないか？

```javascript:ES5時代のクラス風構文

function Greeting(timing) {
    this.timing = timing;
}

Greeting.prototype.sayHello = function (name) {
    if (!name) {
        name = "Man";
    }
    if (this.timing == 'morning') {
        return 'Good morning,' + name;
    } else {
        return 'Hello,' + name;
    }
};


function doGet(e) {

    var g = new Greeting("morning");
    var hello = g.sayHello("John");

    var result = {
        message: hello
    };

    var responseText = JSON.stringify(result);
    var out = ContentService.createTextOutput();
    out.setMimeType(ContentService.MimeType.JAVASCRIPT);
    out.setContent(responseText);

    return out;
}
```

要求:ES5のクラス風とかではなく、TypeScriptを使ってきちんと書きたい

- そんなときは[claspのTypeScriptサポート](https://developers.google.com/apps-script/guides/typescript)
webpackやbabelの手動設定は最低限で済む

## ユニットテストがしたい
要求:Google Apps Scriptでもユニットテストがしたい

- そんなときは [Google Apps Script用Qunit](https://github.com/simula-innovation/qunit/tree/gas/gas)とか。

# まとめ
- Google Apps Script(GAS)でWeb APIを公開する方法をハンズオン的にみてきました
- 今回はほんの触りだけでしたが、機能豊富で味わい深い使い方ができそうです