この記事はGoogle Apps Script Advent Calendar 2023 1日目の記事です。
Google Apps Scriptを使うと無料でウェブアプリを作成できます。無料のため速度やデータ量に制約がありますが1、500名規模の社内システムには十分対応できます。
無料で作る鍵は、Google Sheets(Googleスプレッドシート)をデータベースとして利用することです。データの更新や削除は実装が難しいのですが、新規追加と読み込みはスプレッドシートでも安定して実行できます。
ヘッダー画像はApps Scriptくんとスプレッドシートくんが手をつないでいる様子です。二人をうまく連携させて、ウェブアプリを作ってみましょう。
アクセスログを記録・表示するウェブアプリ
この記事で作るのは、ウェブアプリにアクセスした人のメールアドレスをスプレッドシートに記録して、Webブラウザーに表示する、という簡単なウェブアプリです。3つのステップに分けてApps Scriptで実装していきます。Step1でHello WorldとWebブラウザーに表示し、Step2でスプレッドシートに日付とアクセスした人のメールアドレスを記録し、Step3でアクセスログをWebブラウザーに表示します。最終的にはWebブラウザーには次のようにアクセスログを表示します。
- アクセスログ
- 2023/12/01 09:00:01: user1@example.com
- 2023/12/01 09:10:02: user2@example.com
- ...
対象読者
この記事は、JavaScriptやApps Scriptの経験者を対象としています。開発にはGoogleアカウントとWebブラウザーが必要です。
関係する公式ドキュメントはこちらです2。
では早速開発していきましょう!
Step1: 最も簡単なウェブアプリの作成 - Hello World
最初はHello Worldとだけ表示する簡単なウェブアプリを公開してみます。
スプレッドシートの準備: 初めに、スプレッドシートを作成しましょう。このステップでは「Hello World」の表示には使用しませんが、後のステップで必要です。Google Driveにアクセスし、新しいスプレッドシートを作成して、「アクセス履歴」と名付けます。
次に、スプレッドシートの上部メニューから「拡張機能 > Apps Script」を選んで、スクリプトエディターを表示します。スプレッドシートを開いたばかりだと、メニューからApps Scriptを選んでもエディターが別タブとして表示されないことがあります。その時は、もう一度、メニューから選び直してください。スクリプトエディターのプロジェクト名は「AccessLog」としておきます。
ここから、バックエンドのApps Scriptと、フロントエンドのHTML + JavaScriptを記述していきます。まずは、フロントエンドから始めましょう。スクリプトエディターの左ペイン「ファイル」でプラスアイコン(+)を押して、HTMLを選択します。さらに、ファイル名に「Index」を入力します。ここで間違いやすいのですが「Index.html」ではなく、「Index」と入力します。もし「Index.html」と入力した場合は、ファイル名が「Index.html.html」となるので、リネームして「Index」とだけ入力します。
すると、スクリプトエディターにHTMLの雛形が表示されます。まずは単純に「Hello World」とだけ表示させてみましょう。下のコードをIndex.htmlに上書きして保存します。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
Hello World
</body>
</html>
次にバックエンドの準備をします。WebブラウザーからHTTP GETでアクセスがあると、Index.htmlを返信するコードです。スクリプトエディターで「コード.gs」を選ぶと
function myFunction() {
}
と記述されていますが、function myFunction()
は削除して、下のコードで上書きして保存します。
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index');
}
これでソースコードの準備は終了です。ここで書いたコードが正しいかどうか試してみましょう。デプロイ作業をするとウェブアプリにURLが与えられてWebブラウザーからアクセスできるようになります。ただ、デプロイ作業に関して注意事項があるので先に説明させてください。
デプロイの注意事項
Apps Scriptのデプロイは多機能であり、注意が必要です。ここでは主要なポイントを紹介します。
「新しいデプロイ」は一度だけ。次からは「デプロイを管理」から
これからやる作業は「新しいデプロイ」から作業します。この作業でURLが入手できます。ただ、新しいデプロイをするたびに、URLは新しくなります。URLが変わってしまうと混乱の元になるため、新しいデプロイは一度だけにしましょう。次のデプロイは(新しいデプロイではなく)「デプロイを管理」を選択し、右上のエンピツアイコンを押して、バージョンを「新しいバージョン」に変更します。こうすることで、最初に入手したのと同じURLでデプロイをし直すことができます。
デプロイし直さなくてもテスト用URLで動作を確認できる
新しいデプロイを一度実行すると、URLを入手できます。このURLの末尾は/exec
になっています。デプロイを実行した後にソースコードを変更しても、変更分は反映されません。
このURLは一般利用者用のURLです。開発者は別の便利なURLを使えます。「デプロイをテスト」を選択すると表示される、末尾が/dev
のURLです。こちらのテスト用URLにアクセスすれば、最新のソースコードでの動作を(デプロイし直すことなく)試せます。
なお、テスト用URLにアクセスできるのは、Apps Scriptの編集権限を持っている開発者だけです。一般利用者向けには、デプロイし直す必要があります。デプロイを管理から新しいバージョンでデプロイをして、利用者が同じURLで最新の機能を使えるようにしてください。
ウェブアプリの実行権限とアクセス権限
ウェブアプリをデプロイする時に「次のユーザーとして実行」と「アクセスできるユーザー」の2つを指定します。まずは次のユーザーとして実行から見ていきます。
- 次のユーザーとして実行
- 自分: 誰がウェブアプリにアクセスしたか関係なく、そのウェブアプリを開発者本人として実行します。
- ウェブアプリケーションにアクセスしているユーザー: そのウェブアプリはウェブアプリにアクセスした人として実行します。
「次のユーザーとして実行」はウェブアプリが別のリソース、例えば、スプレッドシートにアクセスする時に影響が出ます。そのスプレッドシートに開発者だけがアクセスできる場合で、「ウェブアプリケーションにアクセスしているユーザー」に設定すると、開発者以外がウェブアプリにアクセスした時にリソースが使えずエラーになります。
- アクセスできるユーザー
- 自分のみ: 開発者だけがそのウェブアプリにアクセスできます。
- Googleアカウントを持つ全員: ウェブアプリにアクセスするとGoogleアカウントによるログインが求められます。
- 全員: 誰でもそのウェブアプリにアクセスできます。
誰がそのウェブアプリにアクセスした人のメールアドレスは、Apps ScriptでSession.getActiveUser().getEmail()
を呼び出すことで分かります(Session.getActiveUser())。しかし、メールアドレスを取得するには次の条件を満たさなければいけません。
- ウェブアプリでメールアドレスを取得できる条件
- 次のユーザーとして実行: ウェブアプリケーションにアクセスしているユーザー
- アクセスできるユーザー: Googleアカウントを持つ全員
- ウェブアプリから利用するリソース: 全員が編集権限を持つ(ウェブアプリの利用者がそのリソースの編集権限を持つ)
なお、開発者本人がアクセスする場合に限っては、開発者本人のメールアドレスが取得できます。自分で動かすときにはうまく動くのですが、他の人に使ってもらうと動作しないことがよくあるので注意してください。
初めてのデプロイ
注意事項が長くなってしまってすみません。それではデプロイをしてみましょう。最初のデプロイなのでスクリプトエディターの右上「デプロイ」ボタンから「新しいデプロイ」を選びます。次のユーザーとして実行は「ウェブアプリケーションにアクセスしているユーザー」、アクセスできるユーザーは「Googleアカウントを持つ全員」にします。
デプロイが完了すると、ウェブアプリのURLが表示されます。URLをクリックすると、Hello Worldと表示されるはずです。URLの末尾は/exec
になっています。
テスト用URLも確認しましょう。スクリプトエディターの右上「デプロイ」ボタンから「デプロイをテスト」を選びます。すると、テスト用URLが表示されます。テスト用URLの末尾は/dev
になっています。こちらをクリックしても、先程と同様にHello Worldと表示されるはずです。
では、スクリプトエディターでソースコードを書き換えて、/exec
と/dev
のそれぞれのURLにアクセスしてみましょう。Hello World を、Hello World!!!に書き換えてみます。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
Hello World!!!
</body>
</html>
「デプロイを管理」で表示される末尾が/exec
のURLにアクセスしても、Hello Worldのままですが、「デプロイをテスト」で表示される末尾が/dev
のテスト用URLにアクセスすると、Hello World!!!が表示されます。テスト用URLなら、ソースコードを書き換えて保存するだけで(デプロイ作業をしなくても)ウェブアプリの動作が変わっていることが確認できます。
さて、ウェブアプリの動作スピードはいかがでしたか?URLにアクセスをしてから、少し時間がかかって、Hello Worldが表示されたと思います。HTMLを表示するだけの一番単純なウェブアプリでもこの動作スピードです。もっとキビキビ動くウェブアプリを作るのであれば、Apps Script以外の手段を使いましょう。
Step2: スプレッドシートとの連携 - アクセスログ追記
誰がこのウェブアプリにアクセスしたかをスプレッドシートに追記していきます。
Sheet.appendRow()で追記
ウェブアプリからスプレッドシートの末尾に情報を追記するには、Sheet.appendRow()を使います。今回は、アクセスのあった時刻と、アクセスした人のメールアドレスを追記します。
まず、開発者以外のアクセスログも記録できるよう、他の人がスプレッドシートを編集できるよう権限を付与しておきます。スプレッドシート右上の「共有」ボタンから操作します。
バックエンドのApps Scriptを次のように変更します。
function doGet() {
// スプレッドシートにアクセス時刻とアクセスした人のメールアドレスを追記
const email = Session.getActiveUser().getEmail();
const row = [ new Date(), email ];
SpreadsheetApp.getActive().getSheets()[0].appendRow(row);
return HtmlService.createHtmlOutputFromFile('Index');
}
スクリプトエディターで保存をしたら、テスト用URLにアクセスしてみます。すると、Authorization is required to perform that action.
と表示されるかもしれません。これは、新しく保存したコードを実行するのに、スプレッドシートを操作する権限が新しく必要になったためです。
ウェブアプリに与える権限を確認する
スクリプトエディター上で「実行」ボタンを押し、doGet()を実行します。すると、ウェブアプリが開発者の情報にアクセスして良いかを確認する長いプロセスが始まります。下図のような警告が出るので内容を良く確認し、先に進んでください。
スクリプトエディターの実行ログには何も表示されず成功したはずです。スプレッドシートを見てみると、doGet()を実行した時刻と、自分のメールアドレスが表示されています。このログはウェブアプリを閲覧したときのログではなく、スクリプトエディターからdoGet()を実行したときのログです。
先程はエラーが出ていたテスト用URLにもう一度アクセスしてみましょう。テスト用URLにアクセスするたびに、スプレッドシートにアクセスした時刻と自分のメールアドレスが表示されるようになります。
友人に頼んで、友人のアクセスログがスプレッドシートに記録されるかを確認するのも良いでしょう。友人が初めてウェブアプリにアクセスするときは次の画面が表示され、ウェブアプリが友人の情報にアクセスしてもよいかを確認する長いプロセスが始まります。
トラブルシューティング
スプレッドシートに日付だけ追記され、友人のメールアドレスが追記されない場合は、デプロイの設定が間違っているかもしれません。次のユーザーとして実行が「ウェブアプリケーションにアクセスしているユーザー」に、アクセスできるユーザーは「Googleアカウントを持つ全員」になっているかを確認してください。
友人がウェブアプリにアクセスした時に次のエラーが表示される場合は、スプレッドシートに編集権限を付与しているかを確認してください。Exception: You do not have permission to access the requested document.
Step3: フロントエンドからの非同期なデータ読み込み - アクセスログ表示
最後に、アクセスログをウェブブラウザーに表示してみます。体感速度を向上させるために、ウェブブラウザーに「アクスログを取得中...」と表示してから、google.script.runを使ってバックエンドのfetchAccessLog()を呼び出してアクセスログを取得し、フロントエンドに表示します。この方法は公式ドキュメントのベストプラクティスでも紹介されています。
Sheet.getDataRange().getValues()で取得
まずバックエンドにfetchAccessLog()を実装していきます。Apps Scriptでスプレッドシートの情報をまとめて取得するには、Sheet.getDataRange()と、Range.getValues()を組み合わせて使います。さらに、JSON形式に変換して、日付dateと、メールアドレスemailを渡すことにします。
先程のコードをリファクタリングして、getSheet_()を抽出した結果、バックエンドのコード.gsは次のようになります。Apps Scriptでは、メソッド名_()
のようにメソッド名の後にアンダーバーを記述するとプライベートメソッドとして定義され、スクリプトエディター上で実行することも、google.script.runから呼び出すこともできなくなります。
function getSheet_() {
return SpreadsheetApp.getActive().getSheets()[0];
}
function doGet() {
// スプレッドシートにアクセス時刻とアクセスした人のメールアドレスを追記
const email = Session.getActiveUser().getEmail();
const row = [ new Date(), email ];
getSheet_().appendRow(row);
return HtmlService.createHtmlOutputFromFile('Index');
}
function fetchAccessLog() {
return getSheet_().getDataRange().getValues().map(row => {
const [date, email] = row;
// このままではgoogle.script.runで動作しない
return { date, email };
});
}
function testFethAccessLog() {
console.log(fetchAccessLog());
}
スクリプトエディター上でtestFetchAccessLog()を実行すると、実行ログにJSON形式でアクセスログが正しく出力されます。なので大丈夫かと思いきや、このままではフロントエンドからgoogle.script.runを使ってfetchAccessLog()を呼び出しても正しく動作しません。その理由と対策は後ほど解説します。
次に、フロントエンドの実装に移りましょう。Index.htmlはフロントエンドのDOMツリーを構築すると、google.script.runを使ってバックエンドのfetchAccessLog()を呼び出します。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
アクセスログ
<ul id="access-log">
<li>アクセスログ取得中...
</ul>
<script>
document.addEventListener('DOMContentLoaded', () => {
google.script.run.withSuccessHandler((data) => {
console.log(data);
const li = [];
for (let log of data) {
li.push(`<li>${log.date}: ${log.email}`);
}
document.querySelector('#access-log').innerHTML = li.join('\n');
}).withFailureHandler(error => alert(error)).fetchAccessLog();
});
</script>
</body>
</html>
ここまで実装できたら、テスト用URLにアクセスしてウェブアプリの動作を確認してみましょう。しかし、Webブラウザーの画面は「アクセスログ取得中...」ままです。Webブラウザーのデベロッパーツールでコンソールを確認すると、Uncaught TypeError: data is not iterable
というエラーが表示されています。コンソールログにfetchAccessLog()の返り値も表示していますがnull
になっています。
google.script.run経由で渡せないデータ型
実は、google.script.runには制限があり、Date型をフロントエンドに返すことができません。
form
のほかに、Date
、Function
、DOM要素、またはその他の禁止されたタイプ(オブジェクトまたは配列内の禁止タイプを含む)を渡そうとすると、リクエストは失敗します。循環参照を作成するオブジェクトも失敗し、配列内の未定義フィールドは null になります。
そこで、バックエンドのfetchAccessLog()を次のように修正します。Utilities.formatDate()を使って、Date型を文字列に変換しています。なお時間に相当するHH
は必ず大文字にしてください。間違えて小文字にすると(24時間ではなく)12時間表記となり変換ミスになります。
function fetchAccessLog() {
return getSheet_().getDataRange().getValues().map(row => {
const [date, email] = row;
// このままではgoogle.script.runで動作しない
// return { date, email };
return {
date: Utilities.formatDate(date, 'JST', 'yyyy/MM/dd HH:mm:ss'),
email
};
});
}
では再びテスト用URLにアクセスしてウェブアプリの動作を確認してみましょう。今度は「アクセスログを取得中...」の文字列が、実際のアクセスログに書き換わったことが確認できるはずです。
まとめ
この記事では次のことをご紹介しました。
- Apps Scriptを使用して、基本的なウェブアプリを作成する。
- バックエンドのApps Scriptで
doGet()
を実装して、フロントエンドのHTMLを返却する。 - フロントエンドのHTMLで
google.script.run
を使い、バックエンドのApps Scriptを呼び出してデータを連携する。
- バックエンドのApps Scriptで
- Apps Scriptで作成したウェブアプリをデプロイし、実際にアクセス可能な状態にする。
- 新しいデプロイは1回だけ。その後はデプロイを管理から新しいバージョンでデプロイすることで、URLを維持する。
- 末尾が
/dev
のテスト用URLを使えば、デプロイし直さなくてもウェブアプリの動作を確認できる。 - ウェブアプリ利用者のメールアドレスを取得する条件は「ウェブアプリケーションにアクセスしているユーザー」「Googleアカウントを持つ全員」「リソースは全員が編集権限を持つ」
- ウェブアプリからGoogleスプレッドシートにデータを追加し、それを読み取る。
- 追記には
Sheet.appendRow()
を使う。 - 読み取りには
Sheet.getDataRange().getValues()
を使う。
- 追記には
- フロントエンドからバックエンドを呼び出し、データを取得して表示する。
- google.script.runでDate型は取得できない。
Utilities.formatDate()
で文字列に変換する。
- google.script.runでDate型は取得できない。
スプレッドシートをデータベース代わりにしてデータの追記と読み取りができるウェブアプリを作ってみました。ここで紹介した仕組みをベースにして、より複雑なウェブアプリを作ることができます。ぜひご活用ください。
-
Apps ScriptをはじめとするGoogleサービスでの制約はこちらにまとまっています。 https://developers.google.com/apps-script/guides/services/quotas?hl=ja ↩
-
同じ階層(ユーザーインターフェース)にある、次のドキュメントも有益です。ウェブアプリがうまく動かなくて困ったときには目を通してみてください。(1) シングルページアプリケーション(SPA)のウェブアプリを作る時に必要になる
google.script.run
の説明。フロントエンドのJavaScriptから、バックエンドのApps Scriptで記述された関数を呼び出すのに使います。(2) マルチページアプリケーション(MPA)のウェブアプリを作る時に必要になるHtmlService
によるテンプレートエンジンの説明。バックエンドのApps Scriptから使います。(3) ソースコードの整理術などベストプラクティスの紹介。 ↩