概要
- Google Apps Script (GAS)で、Web API(無料、サーバーレス)を公開する方法についてのメモです
- Google Apps Script (GAS)をローカルで開発したい、Gitでコード管理したい、クラスを使いたい、TypeScriptを使いたい、などのやり方も後半簡単に紹介します
Google Apps Script (GAS) の位置づけを理解してみる
Google Apps ScriptはG Suite(GmailやスプレッドシートなどGoogleサービス)を JavaScriptで制御できる仕組み。だから、Microsoft OfficeのVBAのGoogleサービス版みたいな感じ。
Google Apps ScriptはJavaScriptで書いた処理をWebAPIとして公開できるので、AWS LambdaとかGoogle Functionのようなサーバレスなコード実行環境のようにも使える←今回はこの用途で使いたい
Google Apps ScriptはJavaScript 1.6(2005年11月)がベースなのでモダンなJavaScriptで記述することはできない。かつブラウザ系機能であるDOM API等は無い。ただし、ツールをこねくり回せば今風なコードで書くこともできる。
Google Apps Scriptは無料で使えるが従量課金のような考え方では無いので、お金を払っても無限に使えないということで容量制限がある。
Google Apps Script のプロジェクトを作る
Google Apps Scriptのトップページにいく
Googleアカウントにログインした状態で以下に行く
最初のプロジェクトを作る
[APPS SCRIPTを作成]をクリックすると、プロジェクトが作成される。
無題のプロジェクト というプロジェクトとコード.gsというファイルができるので、
画面左上の無題のプロジェクトをクリックして、プロジェクト名をwebapi_example01に変更した。
左側ペインにコード.gsというファイルがあるのがわかる。これが最初のファイルとなる。ファイル名の右にある[▼]を押してメニューから名前を変更をクリックしてmain.gsというファイル名に変更した。
コードを書いてWeb APIを公開する
さっそくコードを書いて、WebAPIを公開する。コードはJavaScriptで書く。
Google Apps Script はJavaScriptの最新をサポートしているわけでも、すべての機能をサポートしているわけでもないが、JavaScriptが書ける人ならなんとかなる。
Web APIをつくる
デフォルトだと以下のようになっているが、Web APIとして公開したいので、GETやPOSTをハンドリングできるようにする。
function myFunction(){
}
関数myFunctionは消して、以下のようにした
//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は、リクエストパラメータ等のイベントパラメータが入る。
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"が入ることになる。
リクエストパラメータについて詳しくはこちらに書いてある。
以下の ContentService はテキストコンテンツを返すためのサービスで #createTextoutput で Textoutputオブジェクトを生成する。
var out = ContentService.createTextOutput();
Textoutputオブジェクト outに対して #setMimeType
でJSONを指定し、#setContent
で実際に返すテキスト(この場合は JSON形式のテキストにして)をセットする。
out.setMimeType(ContentService.MimeType.JSON);
out.setContent(JSON.stringify(result));
return out;
これでTextoutputオブジェクト outを return すればOK。
セキュリティを考慮してスクリプトがダイレクトにテキストをブラウザに返すことはなく、実際には、ブラウザは googleusercontent.com にリダイレクトされ、そこから結果を受け取る、という構造になっている。
これで、GETメソッドでアクセスすると、JSONを返すWeb APIができた。
早速公開してみる。
Web APIの公開
上部メニューから公開>Webアプリケーションとして導入 を選択する。
アプリケーションにアクセス出来るユーザーを 全員(匿名ユーザーを含む) にする
[導入]をクリックすると、現在のウェブアプリケーションのURLが表示されるので、そのURLをコピーしてブラウザからアクセスしてみる。
アクセス結果
無事、レスポンスが返ってきた。
シンプルだけど、これで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:*がセットされている。ただし、うまく行かないパターンがあるので注意
うまく行かないパターン
・JSONテキストをまるごと POST するような場合は エラーとなる
→なぜなら、JSONテキストの content-typeが application/json となるため。
→なぜ application/json だとエラーとなるかというと、 application/jsonの場合は preflightリクエストが発生するため
→なぜなら、Google Apps Script は OPTION メソッドをサポートしていないため。
つまり異なるドメイン(オリジン)からAjaxはGETもPOSTもできない。
ブラウザ上で動くスクリプトから preflightリクエストが発生するようなリクエストを使いたければ、同じドメインにHTMLを配備するか、別ドメインにHTMLを配備したうえで、JSONPを使うしか手がないようだ。
Web APIをJSONP対応化する
ということで、別ドメインにHTMLを置く前提でさきほどのコードをJSONPに書き換える
//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;
}
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で返すようにした。
(本家サイトにも注意があるが、JSONPをつかうなら脆弱性に関して注意深く設計する必要がある。提供する情報はリードオンリーかつ誰にでも公開できるような情報のみとし、センシティブな情報はゼッタイ入れない)
公開しているWeb APIを更新する
公開>Webアプリケーションとして導入 を選択していまつくったJSONP対応コードに更新する。
新しいバージョンを公開したいときは、プロジェクトバージョンを新規作成する。
変更内容は入力しなくてもよい。
さっきのが バージョン1 だったので、次は自動的に 2 になる。
HTMLをつくって、ブラウザからアクセスする
以下のような htmlファイルを作る。
今回は、JSONPへのアクセス用にjQueryを使った。
<!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>
const endpoint = "https://script.google.com/macros/s/xxxxxxxxxx/exec";
この部分には、さきほどの 公開>Webアプリケーションとして導入で確認できる現在のWebアプリケーションのURLを入力する。
さて、いまつくった index.html をブラウザで開いてみる。
ローカルファイルでも開ける。
うまく呼び出すことができた。
これで、シンプルなWeb APIがお手軽に作れた。
デバッグ、テストの方法
デバッグの方法はいろいろあるがいちばん準備が少なくて良いのは、デフォルトで準備されているスクリプトエディタとなる。
スクリプトエディタ上で実行する
ブラウザ上から編集できるスクリプトエディタには、スクリプトの実行機能やデバッグ機能がついているので実行してみる
ここでは、doGetを呼び出すために以下のようなコードを 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をクリックする
すると、以下のような感じで、ブレークポイントまで実行され、ウォッチ画面が表示される。
一時停止や再開、ステップ実行などひととおりの機能がブラウザベースでできるので、なかなか便利。
Google Apps Scriptから自分自身を呼び出す
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);
}
このコードは、自分自身を呼び出してみる関数になっている。
var url=ScriptApp.getService().getUrl() ・・・
ScriptApp.getService().getUrl()で自分自身のURLを取得する
var options = {
"method": "GET",
"followRedirects": true,
};
var response = UrlFetchApp.fetch(url, options);
こちらは、実際に GETメソッドで外部サービス(といっても自分自身)を呼び出しにいっている。外部サービスに接続にいくときは、 UrlFetchAppを使う。
ここまでのコード(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);
}
許可を確認をクリックすると、
認可画面がでてくるので、内容確認して 許可をクリック。
実行が終わった後に、ログ画面を表示してみると、きちんと動作していた。
ここまででひとまず基本的な使い方は理解できた。
ローカルで開発したい、Gitでコード管理したい、クラスを使いたい、TypeScriptを使いたい、などの要求(欲求)とやり方
Google Apps Scriptにも効率化やモダン?な開発を促進するための各種ソリューションを先人たちが親切にも準備してくれているのでキーワードだけカンタンに整理しておく。
ローカルで開発したい!
要求:ブラウザ上のエディタじゃものたりません、ローカルで開発したいです
- そんなときは、ローカルでの開発を助けてくれるclasp。GITに似たコマンド体系で操作できる。
インストール
npm i @google/clasp -g
インストールしたらログインする。Googleのアカウントで入る。
clasp login
ローカルにプロジェクトを落としてくる
clasp clone [scriptId/scriptURL]
clasp clone "15ImUCpyi1Jsd8yF8Z6wey_7cw793CymWTLxOqwMka3P1CzE5hQun6qiC"
clasp clone "https://script.google.com/d/15ImUCpyi1Jsd8yF8Z6wey_7cw793CymWTLxOqwMka3P1CzE5hQun6qiC/edit"
pullする。バージョン指定も可能。
clasp pull
clasp pull -versionNumber 2
pushする。
clasp push
claspでローカルに落としてきたものをGitHubで管理したりできそう。
GitHubでコード管理したい!
要求:Google Apps ScriptのコードはGoogle Driveに保存されるが、あっぱりGitHub使いたい!
chrome拡張として提供されている。
前節でふれたclaspをつかってローカル→GitHubでもいいし、chrome拡張つかってもよさそう。
クラスを使って書きたい!
要求:クラスを使いたい!
- そんなときは、まずES5のprototypeをつかったクラス風で満足できないか?
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サポート webpackやbabelの手動設定は最低限で済む
ユニットテストがしたい
要求:Google Apps Scriptでもユニットテストがしたい
- そんなときは Google Apps Script用Qunitとか。
まとめ
- Google Apps Script(GAS)でWeb APIを公開する方法をハンズオン的にみてきました
- 今回はほんの触りだけでしたが、機能豊富で味わい深い使い方ができそうです