Google Apps Scriptでウェブアプリケーション作成入門【GAS】
これまでブログにて、Google Apps Scriptを用いた様々なアプリケーションやテクニックを紹介してきましたが、その多くはある程度Google Apps Scriptが出来る前提でのものでした。その為、入門者向けの内容ではなく、コードの説明も主にポイントになる点と、コード内のコメントでどのようなものを書いてるのかを読み取るものになっています。その中でGoogle Apps Scriptでウェブアプリケーションを作る方法をまとめて欲しいという話があったので、現時点で最新の作り方をまとめてみました。
目次
- 1 今回使用するクラス等
- 2 概要
- 3 テクニックと注意点
- 3.1 現在、ファイルを開くことができません
- 3.2 ドライブ上のPDFをiframeで表示する
- 3.3 ドライブ上のHTMLを表示する
- 3.4 外部のJSやCSSを読み込み
- 3.5 HTML側⇔GAS側で通信する
- 3.6 特定の人にだけウェブアプリケーションを使わせる
- 3.7 スプレッドシートのダイアログで使う
- 3.8 ライブラリの関数は直接呼び出せない
- 3.9 サイトに埋め込み
- 3.10 複数のプロジェクトを用意する
- 3.11 画面遷移について
- 3.12 ページ内リンクについて
- 3.13 同時アクセス数について
- 3.14 デプロイを自動化する
- 3.15 デプロイバージョンの制限
- 3.16 HTMLダイアログの閉じるを検知できない
- 4 関連リンク
今回使用するクラス等
概要
Google Apps Scriptにはウェブアプリケーションを作り表示する為のサービスとして2種類のタイプのクラスが用意されています。どちらもウェブアプリケーションとして出力した結果は同じなのですが、その動作原理が異なるので、利用目的などに応じて使い分けると良いでしょう。また、ウェブアプリケーションとして公開する為の手順がとても重要なので、この手順はよく把握しておく必要性があります。コード変更時に嵌まるポイントの1つです。
また、HTML内ではjQueryといったライブラリやangularjsといったフレームワーク等、現在はかなり自由に使えるようになっています。速度も高速化されていますので、かつてのようにサニタイズされてライブラリが動かないだとか、遅いといった事がありません(それらの古いモードは現在廃止されました)。
用意の仕方
Google Apps Scriptのスクリプトのみではウェブアプリケーションは作れません。必ず表示する為のHTMLファイルが必要です。とはいえ、ドライブにHTMLファイルを用意するのではなく、スクリプトエディタの画面上で用意します。
- スクリプトエディタのメニューよりファイル⇒新規作成を開く
- HTMLを選択し、名前をつける
- HTMLファイルが作られるので、その中にHTMLやCSS、JavaScriptを記述してゆく
createHtmlOutputFromFile
非常にポピュラーなウェブアプリケーションを作る為のメソッドです。用意したHTMLをアウトプットするだけです。doGet()関数で呼び出すのがお決まりになっていますので、以下のコードで呼び出すだけです。一番よく使う形式です。以下の事例だとindex.htmlを呼び出しています。自分のコードでは、これに加えて、.setSandboxMode(HtmlService.SandboxMode.IFRAME)というものをつけていますが、これは今現在は省略可能です。
1 2 3 4 |
function doGet(){ var html = HtmlService.createHtmlOutputFromFile('index'); return html; } |
createTemplateFromFile
少し変わったウェブアプリケーションを作るためのメソッドです。同じく用意したHTMLをアウトプットします。doGet()関数で呼び出すのがお決まりになっていますが、特徴的なのがこれは動的に呼び出せるという点です。
- スクリプトレットというものを利用してGAS側のコードをHTML側から実行する事が可能です。
- GAS側で取得したデータをHTML側変数で受け取る事が可能である
- evaluate()という関数を必ず使う。
1 2 3 4 |
function doGet(){ var html = HtmlService.createTemplateFromFile('index'); return html.evaluate(); } |
スクリプトレットとは、<?= kinoko(); ?>といった形で記述をし、この場合、kinoko関数実行するといったものになります。また、output.append(“キノコ”)関数も使え、HTML内に文字列を出力させる事が可能です。また、<?!= ?>という記述もあり、この場合強制的に実行する強引なやり方です。HTML側でGAS側の実行結果を変数に受け取るようなシーンで使用します。
viewportの指定
HTML Serviceでは、HTML側にスマフォ対応で利用するviewportのメタタグを記述しても無視されます。このメタタグはGAS側で出力する前にオプション指定する必要があります。
1 2 3 4 5 |
function doGet(e) { var html = HtmlService.createHtmlOutputFromFile('index') .addMetaTag("viewport", "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=10.0"); return html; } |
.addMetaTagでviewportを指定することが可能です。この指定をしない場合、スマートフォンに最適な表示にはならないので、スマフォ用アプリを作成する場合には必須の項目です。
ウェブアプリケーションとしてデプロイ
これらのメソッドを使っても、最終的に「ウェブアプリケーションとしてデプロイ」をしなければ、ウェブページは表示されません。また、このウェブアプリケーションとしてデプロイはいくつかのハマりポイントがありますので注意が必要です。公開の仕方は以下の手順。
- スクリプトエディタを開く
- 右上のデプロイをクリック
- 新しいデプロイをクリック
- 種類の選択ではウェブアプリを選択し、次のユーザとしてアプリケーションを実行で誰の権限で動かすかを指定する。自分か?アクセスしてるユーザの二択。
- アプリケーションにアクセスできるユーザを指定する。自分のみ、組織内全員、Googleアカウントを持つ全員、全員の四択。但し、全員の場合はGoogleアカウントが必要で、匿名含むの場合は、Googleアカウントなしでアクセス可能です。
- 最後に導入すると、ウェブアプリケーションのURLが取得できます。このURLでアクセスをします。URLの最後がexecが本番用、devがテスト用で、テスト用はデプロイをテストをクリックすると表示されますが、変更したコードがそのまますぐに反映されてしまうので、テスト用のURLで運用しないように。
- 次回以降コードを編集して再デプロイ時はデプロイを管理から同じURLにて、新しいバージョンを指定して発行することが出来ます。
※5.の設定はGoogle Workspaceの設定次第では、Googleアカウントを持つ全員や全員は選択肢に出てこない事があります。
図:デプロイの仕方で挙動が変わります。
嵌まるポイント
さてハマりポイントなのですが、以下のような感じです。
- G SuiteことGoogle Appsの場合、外部との共有を管理者が許可していない場合、匿名でウェブアプリケーションは実行出来ませんし、外部に公開は出来ません。
- 公開済みの場合、再度公開を実行すると最新のコードをテストという項目があり、URLの最後に/devが付きます。公開していない状態でも最新コードで表示されます(ファイルオーナーのみ)
- 但し上記のそれはデベロッパーモードであり、コードの変更が直ちに反映されてしまうので、一般公開のURLとして使わない事。execの場合、新しい版で公開し直さない限り、コードは反映されません(但し、HTML部分のみで、GAS部分は反映されてしまいます)。
- きちんと正式公開した場合はURLの最後に/execが付き、これを貼り付けるガジェットのURLや一般公開のURLとして使うこと。
- コードを変更した場合、execのURLに反映させる為には、プロジェクトバージョンを変更し新規作成にしなければコードが反映しません。
- 新開発画面からは旧来と異なり、同じプロジェクトではない形でウェブアプリケーションを公開してしまうと、URLが変わってしまいますので注意。必ずデプロイを管理から行うこと。
- 何度もプロジェクトバージョンを新規作成してると、版が積み上がります。これらはファイル⇒版を管理の中で確認が出来、過去の版を廃止する事が可能です。過去の版でウェブアプリケーションを表示する場合は、プロジェクトバージョンを古いものにして公開すればOK.
- 但し、過去のプロジェクトバージョンのコードに戻す事は不可能。変更履歴から復元は可能ですが、全て変わってしまうので、細かく保存しましょう。
- ウェブアプリケーションを無効にするをクリックすると、ウェブアプリケーションが停止します。停止しないと版を廃止する事はできません。
- ウェブアプリケーションとして公開は、プロジェクトオーナーでなければ出来ません。これはスプレッドシートのオーナーとはイコールではありません。複数プロジェクトを設置した場合、片方が自分が作ったものではない場合、そのプロジェクトを自分が削除したりウェブアプリケーションとして公開する内容を変更したりは出来ません。
- 忘れがちですが、コードを書いて、一度doGet()などを実行し、承認をしておきましょう。これをしないと動きません。
- シートにアクセスするような場合、ウェブアプリケーションの場合は、SpreadsheetApp.getActiveSpreadsheet()でアクセスは出来ません。SpreadsheetApp.openById(“シートのID”)でなければ、シートのデータは取得できません。但し、スプレッドシート上のダイアログとして使う場合は、取得が可能です。
- メールを送信するMailAppを使っている場合、Fromの送信者アドレスはウェブアプリケーションとして公開した人のアドレスになります。このアドレスは任意のアドレスに変更は出来ません(但し、その人のメアドにエイリアスを設定してる場合には、MailAppのオプションにfrom: ‘eye4brain@xxx.gr.jp’といった形でFromを変更は出来ますが、あくまでもエイリアスです)。
- HTML Serviceで出力したHTML上では、ページ内リンクのアンカーは利用する事ができません。つまり、アンカーだけではページ内でジャンプが不可能です。
アクセス権限
概要
前項のハマりどころをもう少し深く掘り下げてみます。このアクセス権限が結構クセモノで、混乱するポイントなのですが、シートのアクセス権限(特に書き込み)とウェブアプリケーションのアクセス権限は別物という事です。以下の表のような感じになります。
スプレッドシート権限 | WebApp(ユーザ権限) | WebApp(オーナー) | 備考 |
---|---|---|---|
閲覧・編集権限 | ⭕ | ⭕ | 誰でもアクセスすることが可能です |
特定ユーザ | 🔺 | ⭕ | ユーザ権限の時はシート権限が必要 |
非公開 | ❌ | ⭕ | 非公開でもアクセス可能 |
※上記の表の⭕は、一般ユーザがアクセスした場合に、WebAppを実行できるかどうか?どうかを示しています(実行権限は制限していない場合です)
※非公開でもアクセス可能ですが、WebAppの実行権限が自分のみの場合、オーナーだけが実行できます。
※G Suiteだとさらに実行権限にはドメイン内ユーザのみという権限もあります。
よって、スプレッドシートの書き込み権限が自分のみであっても、実行権限が自分で、ウェブアクセスの権限が全員ならば、自分以外の人であっても、自分の権限を持って書き込みが可能です。一方で実行権限がアクセスしてる人の場合、ウェブにアクセスは出来ますが、シートの書き込み権限が無いことになるので、書き込めません。
この時、スプレッドシートの編集履歴は「自分」しか残りません。自分の権限で書き込みをさせている為です。逆を言えばシートは非公開で閲覧させたくないが、ウェブは使わせたいなんて時は実行権限を自分にしておくと良いでしょう。
ユーザ各々の実行権限で書き込みをする為には、以下のような設定にします。
この場合、スプレッドシートに個別にユーザに編集権限を加える、もしくは全員編集可にしておく必要があります。編集権限はグループアドレスでもOKです。この時のシートの編集履歴はそれぞれの人のメアドが記録される事になります。但し、各々のユーザはウェブアプリケーションアクセス時に1度だけ、実行承認をしなければなりません。また、後にコードを追記して新しいAPIを使った場合も、承認画面が改めて出ることになります。
※また要注意ポイントとして、WebAppの実行権限を自分にした場合、全ユーザが自分の権限でGASを実行するため、例えばUrlfetchAppなどのQuota制限のあるAPIの実行を伴う場合、リミットに達しやすくなります。各ユーザの場合には、各ユーザが1日辺り実行できるリミットとなります。
GASの実行権限
また、このアクセス権限が曲者で、以下のような特徴があります。
- オーナーの権限で動かす場合、ドライブのファイルはオーナーがアクセス出来る範囲になる
- ユーザの権限で動かす場合は、ドライブのファイルはユーザがアクセス出来る範囲となる(事前にそのユーザにアクセス権限の無いスプレッドシート等はアクセスできない)
- 2.は逆を言えばアクセス出来る以上そのファイルの中身はユーザは全部閲覧可能(なので、他のユーザの情報などを管理するようなシートとしては使えない。後述の特定ユーザに使わせるといった事がユーザ権限で動かす場合には使えません。)
- 1.はオーナーの権限で動かしているので、対象のファイルがオーナー以外非公開でも、ユーザがそのファイルにGAS経由でアクセス可能となる(ファイル自体は見えないので、ユーザ情報の管理などでシートを使う事が可能)
- また、Picker APIなども1.と2.で見えるフォルダの一覧が、オーナー側か?ユーザ側か?挙動が変わるので注意。
- 但し、Session.getActiveUser()で取得出来る実行ユーザ情報は、オーナーではなくアクセスしたユーザのアドレスなどになる
特定企業とファイルをシェアするようなアプリの場合、そこに無関係の別の企業のアクセス情報であったり、またファイルにつけた許可メアドなどが、ユーザ権限で動かした場合には見えてしまうので、非常によろしくないです。情報漏えいにつながるので、この権限周りは一度自分で色々試してどういう結果になるのかを見に付けておくべきでしょう。
また、この実行権限はスクリプトプロパティでも継承されるため、特にgetUserPropertiesを使ってる場合は要注意です。
作成者権限で動かしつつユーザ権限でも実行させる
前述のアクセス権限にあるよう、GAS作成者が管理者で様々なメソッドをウェブアプリを通して実行させたいので実行権限を自分にしてデプロイすると、今度はユーザがアクセスしてきてもそれらすべての実行の結果は自分自身が実行したことになります。つまり、カレンダーに登録作業をしても全て自分のカレンダーに登録されてしまいます。かといって、ユーザ権限で動かすことにすると、現在のアプリの元になってるスプレッドシートを全員に共有が必要になるだけじゃなく、見せたくないファイルも見せる必要性が出てきたり、ユーザ毎に認証も必要になります。
そこでこの問題を解決する「自分の権限でウェブアプリを動かしつつ、一部をユーザの権限で動かす手法」があります。以下のエントリーにまとめていますが、サービスアカウントを利用した手法になるので少々環境構築が厄介ですが、このテクニックを利用する事で、GASでウェブアプリの可能性が一気に広がる為、ぜひ体得して利用してみて下さい。
アクセスできるユーザで全員が指定出来ない
デプロイ時にアクセスできるユーザでどこまでアプリにアクセスを許可するかを指定出来ますが、無償のGoogleアカウントでも使えますがちょっと設定が必要になります。
Google Workspaceの場合にはそもそもテナントに於いて、Googleドライブの共有設定に於いて外部での共有がオンになっている必要があります。
- Admin Consoleにログインする
- 左サイドバーから「アプリ」⇒Google Workspace⇒ドライブとドキュメントを開く
- 共有設定を開く
- 共有オプションを開き、外部との共有をオンにする
ただしこの設定はドライブ内の全ユーザやファイルに対してもデフォルトではオンとなってしまい、推奨される設定ではありません。よって、組織部門で外部共有専用の部門を作成した上で、なおかつその部門にだけ所属する特別な共有ドライブを作成し、そこの中でだけ許可するといったような高度な管理が必要です。この内容については以下のエントリーを参考にしてみてください。
また、無償ユーザも含めてですが、「無条件に全員アクセス可能」な全員を指定するためには、
- ファイルそのものが外部に共有されている必要性は無いものの
- 次のユーザとして実行が「ウェブアプリケーションにアクセスしているユーザ」の場合は、「全員」を選択出来ません
- よって、実行権限は「自分」としてデプロイする必要があります。
更に注意点として、Google Workspaceのバグとして外部共有をオンにする前にデプロイしていたウェブアプリケーションに於いては、後からオンにしても「全員」のオプションが使えません。よってこの場合、再度新しいウェブアプリとしてデプロイする必要性があります。後から変更が出来ません。
図:使うにはちょっと設定が必要
WebアプリのURLを取得する
旧式の方法
現在、デプロイしてある最新のウェブアプリケーションのURLを取得したいケースがあります。例えばこのウェブアプリを利用する対象者にメールで通知であったり。しかし、いちいちデプロイ管理画面からURLをコピーして、Gmailでメールを作って貼り付けて送るではあまりにもスマートじゃありません。
以下のコードで一発で最新のデプロイしたウェブアプリのURLを取得出来ます(最後がexecのURLになります)。但しこの方法はV8エンジンがオンの状態の場合、誤ったURL(というかデプロイメントID)が返ってくる問題が報告されています。現在も解消されていません。
1 2 |
//webappsに最新のウェブアプリURLを取得する var webapps = ScriptApp.getService().getUrl(); |
V8での取得方法
前述の方法はV8がオンの場合、正しいデプロイされたウェブアプリのURLが取得出来ません。この問題に対してStackOverFlowではGCPのApps Script APIのDeployments.listを利用して取得する方法を提示しています。現在、Apps Script APIは旧Execution APIの機能である「直接REST APIのようにGASの関数を叩く」という機能の他にこういった機能も持ち合わせています。
こちらについては以下のエントリーで別に紹介しています。
テクニックと注意点
現在、ファイルを開くことができません
デプロイして、ウェブアプリケーションを開いたら「現在、ファイルを開くことができません」というエラーが出て、ウェブアプリが表示されないケースがあります。これは、開こうとした人が、2つ以上のGoogleアカウントにログインしている状態の時に発生します。以前は発生していなかったのですが、Google側がマルチアカウント対応を諦めたのか?ここ数年表示するようになっています。
解決法は、アクセスする人が一度アカウントを全部ログアウトして、1つだけGoogleアカウントにログインした状態にすること。
ドライブ上のPDFをiframeで表示する
Google Drive上にあるPDFファイルをHTML Service上で表示したい場合には、当然ながらiframeを使ってURLを指定する必要があります。しかし、この時に指定するURLがちょっと特殊で、ファイルの共有リンクで指定しても403エラーで見ることが出来ません。
しかし、以下のようなURLにすると埋め込み表示させることが可能になります。
1 2 |
//指定するURL "https://docs.google.com/file/d/ファイルのID/preview?usp=drivesdk" |
そして上記のURLをiframeでsrcに指定するとこうなります。
1 2 3 4 5 6 7 8 |
<iframe id="pdfview" frameborder="0" width="1024" height="768" src="https://docs.google.com/file/d/ファイルのID/preview?usp=drivesdk" > </iframe> |
すると、webapp上でPDFをプレビューできます。但し、CORSのエラーがいくつか出てる状態(直接的にプレビューする分には問題が無い)
図:Webアプリ上でドライブのPDFを表示してみた
ドライブ上のHTMLを表示する
どうしても、Google Apps Scriptの開発エディタ上でHTMLを書くのが辛いというケースでは以下のような形でHTMLファイルを呼び込んでウェブアプリケーションとして表示するテクニックがあります。
- 予めドライブ上に作り込んだHTMLファイルをアップロードしておく
- そのファイルのIDを取得しておく
以下のようなコードでこのHTMLファイルを元にウェブアプリケーションを構築し、エディタ上でHTMLを追加せずに作業が出来るので、他のアプリでも使うようなテンプレートとして流用できる利点があります。
1 2 3 4 5 6 7 8 |
function doGet(){ //HTMLファイルを取得する var fileid = "HTMLファイルのIDをここに記述"; var html = DriveApp.getFileById(fileid).getBlob().getDataAsString("UTF-8"); var output = HtmlService.createHtmlOutput(html) return output; } |
- 上記の状態でウェブアプリケーションとして公開すると、HTMLファイルを取得して表示してくれます。
- スクリプトレットを使いたい場合には、HtmlService.createTemplate(html)として読み込めばOK。HTMLファイル内にスクリプトレットで例えばGAS側の関数や外部のCSS、JSなどを読み込ませればOK
- Google Drive File Streamと合わせてローカルでHTMLファイルをVSCodeで編集するといったことが出来るので、色々と利点のあるテクニックです。
図:こんな感じで配置する
外部のJSやCSSを読み込み
嵌まるポイントの1つなのですが、HTML Serviceで外部JavaScriptやCSS類などを呼び出す場合、http://www.xxx.com/test.jsといった形で呼び出すことは出来ません。httpではなくhttps://でなければならないというルールがあります。https://が使えるのであれば、//www.xxx.com/test.jsという形で頭をつけずに指定して呼び出すことも可能です。
個人でレンタルサーバをファイルの置き場にしている場合、共用SSLなどがない場合には呼び出せませんのでご注意を。呼び出し方そのものは、外部のファイルの呼び出し方と全く一緒です。headタグ内に記述しましょう。
- ※外部に公開してるGoogle Driveのフォルダであれば、直リンクで呼び出すことも可能です。Direct Link Generatorなどを利用してURLを取得してみましょう。
- また、createTemplateFromFileを使ったケースの場合、GAS内でJavaScriptやCSSを独立して管理して呼び出すことも可能です。
HTML側⇔GAS側で通信する
Google Apps Script側と生成したウェブアプリケーションは、サーバーとクライアントの関係にあります。その為そのままでは、HTML側からGoogle Apps Script側に命令やデータを送ったり、またGoogle Apps Script側からHTML側へ返してあげる事が出来ません。しかし、Google Apps Scriptではこの相互通信をする手段として特別な関数を用意しています。以下に示すもの以外にも、google.script.url関数やgoogle.script.history関数なんてものも新規に追加されています。
- ※これらの関数は非同期で実行されてしまいますので、基本的に順番に実行はされません。
- ※これらの関数はHTML側で記述します。
また、これらの関数はプライベート関数にしておかないと、誰でも簡単に実行できてしまうので、そちらも意識しておく必要があります。
google.script.run関数
Google Apps Script側に用意してる関数を一方的に実行する関数です。引数で値や配列などを渡してあげる事が可能です。一方的に実行して完了してしまうので、返り値を受け取るといった事は出来ません。以下のような構文で実行します。
1 2 |
//berrygetという関数に"いちご"という値を渡して実行 google.script.run.berryget("いちご") |
google.script.run.withSuccessHandler関数
この関数は上記の関数とは異なり、返り値を受け取る事が可能です。withSuccessHandlerで成功時、withFailureHandlerで失敗時の処理をそれぞれ定義でき、同時に定義することも可能です。返り値を受け取って、HTML側で引き続き返り値の処理をする事が出来ます。例えば以下のような記述をします。
1 2 3 4 5 6 7 8 9 10 11 12 |
//berryget関数に引数として"いちご"を渡し、返り値を処理する google.script.run.withSuccessHandler(onSuccess).withFailureHandler(onFail).berryget("いちご"); //成功時に実行 function onSuccess(data){ var json = JSON.parse(data); } //失敗時に実行 function onFail(){ } |
直接値を変数で受け取る
直接実行した結果を変数で受け取る事も可能です。ただし、非同期で実行される点は同じですので、受け取った変数を元に続けてなにか作業をしたい場合には、うまく動作しないことがあります。
1 2 |
//resultの結果をdataで受け取る var data = google.script.run.withSuccessHandler(result).testman(); |
返り値に基づいて処理
また、返り値を受ける書き方ですが、別の関数にするのではなく、以下のような書き方も可能です。この特殊な関数は非同期で実行されてしまうので、順番に実行したい場合には、このような書き方が必要です。
1 2 3 4 5 6 7 8 |
google.script.run.withSuccessHandler(function(ret) { if(ret == "OK"){ flag = ret; }else{ document.getElementById("backman").innerHTML = ret; return; } }).exportlog(url,pref); |
上記の場合、onSuccessだった場所に無名関数をつくり、無名関数の引数retでGAS側の返り値を受け取って内部で処理をしています。exportlogはGAS側の関数の実行を意味しています。
また、この関数は以下のようなルールがあります。
- withFailureHandlerは別につけなくても良い
- GAS側でreturnすれば、HTML側へ値を返すことが出来る。
- returnで返す場合、配列などは必ずJSON.stringify(array)という形で加工して渡して上げる必要がある。
- 3.の場合、HTML側はJSON.parse(array)で受け取る事になる。
- withSuccessHandlerの引数は成功時に実行する関数名。onSuccessがそれでさらにそのonSuccessの引数dataがGAS側からreturnの返り値を受け取るものです。
- 但し受取側はJSON.parseをしなくても良い。obj.messageのようにJSONから値を取る形で処理を継続してもOK
- HTML側からGAS側へ渡す場合通常はJSON.stringifyせずとも渡せますが、オブジェクトの場合はJSON.stringifyする必要性があります。その場合、GAS側はJSON.parseで配列として受け取れます。
- 引数は複数渡すことが可能です。
- formなどの値を送る場合には、そのままノードまるごと渡せるのですが、複数の引数として渡す事は出来ません。必ずノード単独です。
- 主にこの関数はcreateHtmlOutputFromFileで使用します。
Promiseで同期的に処理
前項のgoogle.script.run.withSuccessHandlerによるGASからの返り値に基づいて処理するコードでは単一の処理ならばこれでも良いですが、いくつかの連続した処理をしなければならない時には、コードが複雑になってしまいます。そこで、Promiseを利用して順番に非同期なgoogle.script.runを実行して計算する手法があります。
GAS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var gasheet = "ここにスプレッドシートのURLを入れる"; function doGet() { //HTML生成と同時に配列情報を渡す var output = HtmlService.createTemplateFromFile('index').evaluate() .setSandboxMode(HtmlService.SandboxMode.IFRAME); return output; } function test1(){ var data = SpreadsheetApp.openById(gasheet).getSheetByName("シート1").getRange("A1").getValue(); return data; } function test2(){ var data = SpreadsheetApp.openById(gasheet).getSheetByName("シート1").getRange("B1").getValue(); return data; } function test3(){ var data = SpreadsheetApp.openById(gasheet).getSheetByName("シート1").getRange("C1").getValue(); return data; } |
test1〜3はそれぞれ、セルの値を取得して返すだけの関数です。これをHTML側から順番に取得して計算結果を表示します。
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<!DOCTYPE html> <html> <head> <base target="_top"> <script src="//cdnjs.cloudflare.com/ajax/libs/es6-promise/3.2.1/es6-promise.min.js"></script> <script> //セルの値を格納する変数 var test1 = ""; var test2 = ""; var test3 = ""; var total = ""; //処理を開始 startcalc(); //セルの値を順番どおり取得して研鑽する function startcalc(){ new Promise(function(resolve, reject) { //A1の値を取得する google.script.run.withSuccessHandler(function(ret) { //値を格納する test1 = ret; total = Number(ret); //resolveを返す resolve(total); }).test1(); }).then((result) => { //B1の値を取得してtotalに加算する google.script.run.withSuccessHandler(function(ret) { //値を格納する test2 = ret; total = Number(total) + Number(ret); //returnで次に引き継ぐ return; }).test2(); }).then((result) => { //C1の値を取得してtotalに加算する google.script.run.withSuccessHandler(function(ret) { //値を格納する test3 = ret; total = Number(total) + Number(ret); //計算結果をalertで表示 alert(total); }).test3(); }); } </script> </head> <body> </body> </html> |
- Promise〜thenによる順番に実行する処理です。
- 1つ目のgoogle.script.runの結果だけはresolveで返しています。コード内では使っていませんが、resolveで返す場合、次のthenではresultで値を引き継げます。
- グローバル変数に加算しつつ最後にalertで表示しています。
- Promiseを使わない場合、これら3つの処理は次の処理を待たずに実行されてしまうので、600の回答が得られることが保証できなくなります。
- es6-promise.min.jsの読み込みはIEなどの古いブラウザ用のライブラリです。
GASでHTML生成時に同時に渡す方法
関数で渡してあげる方法
1 2 3 4 5 6 7 8 9 |
function doGet(e){ //HTML生成と同時に配列情報を渡す var output = HtmlService.createTemplateFromFile('index').evaluate().getContent(); var html = HtmlService.createTemplate(output + "<script>\n" + "doIt( " + JSON.stringify(data) + ");\n</script>") .evaluate() .setSandboxMode(HtmlService.SandboxMode.IFRAME); return html } |
上記のケースでは、HTML側にdoIt()という関数を用意しておいて、予め用意したdataという変数の値をJSON.stringifyで加工、HTMLを生成時に自動的に変数まるごと渡して、doIt関数で処理をさせる事が可能です。ただ、あまりこれをやるメリットは薄いです。
オブジェクトに変数付で渡す
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var data = "とまとマン" function doGet(e){ //HTML生成と同時に配列情報を渡す var output = HtmlService.createTemplateFromFile('index') //変数をつけてevaluateで渡す output.data = data; //ページを生成する var html = output.evaluate() return html; } |
一旦createTemplateFromFileで生成したオブジェクト(output)に対して、output.dataという形で変数を渡せます。すると、evaluateした先のHTMLでこの値を受取る事ができるので、<?=data?>で表示させる事が可能です。前項のdoIt関数で処理するよりもお手軽ですね。送る側は特にJSON.stringifyする必要なくそのまま渡せばよいです。
HTML側は以下のような感じになります。
1 2 3 4 5 6 7 8 9 |
<!DOCTYPE html> <html> <head> <base target="_top"> </head> <body> <?=JSON.stringify(data); ?>参上❢ </body> </html> |
但しHTML側では数値の場合は直接<?! =data;?>で表示できるのですが、文字列の場合はエラーとなるのでJSON.stringifyで括ってから取得が必要です。そのまま変数に格納することが可能ですので、ワークフローみたくdoGetでパラメータにレコード番号を渡し、それを受け取ったウェブアプリ側が直接対象のレコードを表示するといったような用途で活用が可能です。
※dataが空の場合エラーとなるので、必ず空でもdataに値を入れておく必要がありますので、output.data = ""といったように空文字を入れておきましょう。
スクリプトレットでGAS側から直接受け取る方法
また、createTemplateFromFileの場合は、GAS側の変数や処理の中身をHTML側でスクリプトレットにて取得が可能です。
1 2 3 |
<script type="text/javascript"> var testman = <?!= JSON.stringify(SpreadsheetApp.openById('シートのID').getRangeByName('potato').getValues());?> </script> |
こうする事でHTML側でスクリプトレットの強制実行タグでtestmanにpotatoという名前付き範囲の値を取得させる事が可能です。もちろんここの部分は変数名でもOKです。他にも以下のようなコードをheadタグ内で記述する事で、createTemplateFromFileの場合には、テキスト文字列を取得が可能です。
1 2 |
//CSS.htmlを呼び出すスクリプトレット <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?> |
これはcssというGASで用意したHTML内にcssの記述をしたものをHeadタグ内で呼んでいるもので、CSSの記述をまるごと別のHTMLファイルから呼び出せます。CSSだけじゃなく、HTMLファイル内にJavaScriptを記述しておけば、関数等もロードできてしまいます。
このテクニックを使う事で、メインのHTMLに全てを記述するのではなく、CSSはcss.htmlへ、JSはjs.htmlへ分離管理が出来るようになります。具体的にはライブラリを別管理する手法サンプルを御覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
//CSSとJavaScriptを分離して呼び出す方法 <link rel="stylesheet" href="https://code.jquery.com/ui/1.11.4/themes/cupertino/jquery-ui.css" /> <script src="https://code.jquery.com/jquery-1.10.2.js"></script> <script src="https://code.jquery.com/ui/1.11.2/jquery-ui.js"></script> <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?> <?!= retcss(); ?> <?!= HtmlService.createHtmlOutputFromFile('javascript').getContent(); ?> <!-- 基本情報入力部分 --> <div id="karakasa"></div><div id="mainform"> <div><label><b>担当者名</b></label></div> <INPUT type='text' class='wasabi' id='wasabi1' placeholder='あなたの氏名を入力。' size='20' title='今このフォームを入力してる人の名前'><p><p> <div class="y_data_area"> <table class="y_data_title"> <col style="width: 50%;" /> <col style="width: 25%;" /> <col style="width: 25%;" /> <tr> <th>品名</th> <th>数量</th> <th>金額</th> </tr> </table> <div class="y_scroll_box"> <div class="y_hidden"> <table class='y_data' id="tableman"> <col style='width: 50%;' /> <col style='width: 25%;' /> <col style='width: 25%;' /> </table> </div> </div> </div> |
ドライブからCSSやJSファイルを直接受け取る方法 - その1
Google Drive内に別途作成したCSSやJSのファイルを使いたい場合もあるでしょう。そうすることで、他のGASプロジェクトでもそのまま流用できる為、非常に便利ですし、手間が省ける(プロジェクト毎に微妙に違うとか防げますし)。また、他のシステムから自動で上書きで吐き出されるJSONファイルを固定でロードするような仕組みにしておけば、インポートの手間も省けますね。
この場合、直接スクリプトレットでドライブのデータを取得するコードを書いても反映されないので、GAS側で一度取得して返すようにしてあげます。
GAS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//ドライブ側にあるCSSファイルを呼び出す function retdrivecss(){ var ret = DriveApp.getFileById("CSSファイルのID").getBlob().getDataAsString("UTF-8"); return ret } //ドライブ側にあるJavaScriptファイルを呼び出す function retdrivejs(){ var ret = DriveApp.getFileById("JSファイルのID").getBlob().getDataAsString("UTF-8"); return ret } |
- 各CSSやJSのファイルの文字コードに注意。Shift-JISならば、getDataAsStringではShift-JISを指定する必要があります。
- ドライブにある各ファイルから中身を取得し、HTML側へ返してあげています。
HTML側コード
1 2 3 4 5 6 7 8 9 10 11 12 |
<link rel="stylesheet" href="https://code.jquery.com/ui/1.11.4/themes/cupertino/jquery-ui.css" /> <script src="https://code.jquery.com/jquery-1.10.2.js"></script> <script src="https://code.jquery.com/ui/1.11.2/jquery-ui.js"></script> <?!= retdrivecss(); ?> <?!= retdrivejs(); ?> <!-- 基本情報入力部分 --> <button type="button" onClick="testman()"> <font size="2">ここを</font><font size="5" color="#333399">押してね</font> </button> |
- retdrivecssでドライブのCSSを、retdrivejsでJSファイルを取得しています。
- ドライブのJSファイルにあるtestman()という関数を呼び出しています。
ドライブからCSSやJSファイルを直接受け取る方法 - その2
もうひとつ、ドライブにあるCSSとJSファイルを呼び出す方法があります。こちらのほうがスマートかもしれません。HTML側へ直接対象のファイルの直リンクダウンロード出来る形でURLをそれぞれ呼び出してあげるだけ。
1 2 3 4 5 |
//CSSをドライブのファイルから直接呼び出す <link rel="stylesheet" href="https://drive.google.com/uc?export=download&id=ここにCSSファイルのID"> //JSをドライブのファイルから直接呼び出す <script src="https://drive.google.com/uc?export=download&id=ここにJSファイルのID"></script> |
- https://drive.google.com/uc?export=download&id=に続けてファイルのIDを入れるだけ
- 但しファイルのアクセス権限には注意。厳格にすると呼び出せなくなる
GAS側からJSON形式でHTML側へ渡す
GAS側からHTML側へreturnする場合、通常JSON.stringifyを行ってHTML側でJSON.parseするのが定石です。スプレッドシートで取得したデータは通常の配列であるため、受け取り手であるHTML側も配列で受け取って処理をすることになります。さてこの時、GAS側からスプレッドシートのデータをJSON配列で渡せたら、非常に便利です。理由は
- 通常の配列ではスプレッドシート側で列位置の変更が生じると、HTML側コードを変更する必要がある
- JSON配列の場合には、配列の中の特定値を指定できるので、列の変更が生じてもHTML側で変更は必要ない。
- 配列の何番目に「hoge」が入っているからといったことで頭を悩ませながらコードを書かなくて良い
GAS側からJSON化して渡すには、スプレッドシートのデータをJSONで取得するを参考にしましょう。主要なコードは以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//スプレッドシートのデータをJSONで返す function backjson(ssid,sheetname) { //スプレッドシートデータを取得する var sheet = SpreadsheetApp.openById(ssid).getSheetByName(sheetname); var ss = sheet.getDataRange().getValues(); //タイトル行を取得する var title = ss.splice(0, 1)[0]; //JSONデータを生成する return JSON.stringify(ss.map(function(row) { var json = {} row.map(function(item, index) { json[title[index]] = item; }); return json; })); } |
HTML側ではJSON.parseで受け取ると、例えばjson[0].pointといったように、0レコード目のpoint列の値といった具合に取得が可能になります。やり方しだいでは、複雑なJSONを構築して渡すこともできますね。
google.script.host関数
この関数はちょっと特殊な関数で、次の項目で紹介するスプレッドシートのダイアログで使う場合に併用するものです。おもに、GAS側で表示してるダイアログを閉じたり、そのダイアログのサイズを変更したりする為に使います。
但し、google.script.host.originだけは特別で、そのウェブアプリケーションやダイアログがどのURLドメイン(origin)で動いてるか?を返します。ダイアログならばdocs.google.com、通常のウェブアプリケーションならばscript.google.com、Google Sitesにガジェットとして貼り付けてる場合は、sites.google.comが返ってきます。Google Picker APIを使ったダイアログなどのsetOriginにセットする値を取得する為に利用する。
1 2 3 4 5 6 7 8 9 |
//GAS側で表示してるダイアログを閉じる google.script.host.close(); //GAS側で表示してるダイアログのサイズを変更する google.script.host.setHight(300); google.script.host.setWidth(450); //ウェブアプリケーションのoriginを表示する var origin = google.script.host.origin; |
- ※通常のウェブアプリケーションとして公開してるシーンではあまり使いません。
- ※Google Picker APIなどのsetOriginで利用するoriginデータの取得の場合は、ウェブアプリケーションでも利用します。
特定の人にだけウェブアプリケーションを使わせる
さて、ここで気がつくと思うのですが、ウェブアプリケーション自体のアクセスを細かく制御できないか?という話ですが、それはシステム上出来ません。ウェブアクセスは「自分のみ」「組織内ユーザ」か?「全員アクセス可」しかありません。となると特定の人にだけウェブアプリケーションを使わせたいのに、このままだと誰でもURLを知って入れば使えてしまう事になります。
そんな時は自分でロジックを組みます。この部分は、Google Workspace以降少々変更が加わっているので、以下のエントリーに独立させました。
スプレッドシートのダイアログで使う
すでにメッセージボックスあれこれにて紹介済みなのですが、スプレッドシート上で呼び出すダイアログにも、HTML Serviceで生成したウェブアプリケーションをダイアログとして呼び出す事が可能です。標準のダイアログは素っ気なく出来ることも限られているのですが、HTML Serviceで作ったダイアログは、自由度が高くあらゆるものをダイアログに詰めて、処理させる事が出来るので、重宝します。以下のようなコードを記述します。
1 2 3 4 5 6 |
function htmlbox() { var output = HtmlService.createTemplateFromFile('index'); var ss = SpreadsheetApp.getActiveSpreadsheet(); var html = output.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME); ss.show(html); //メッセージボックスとしてを表示する } |
ライブラリの関数は直接呼び出せない
HTML側からgoogle.script.runでGAS側の関数を実行したり、実行結果を受け取れるわけなのですが、関数類を他のシート等でライブラリ化している場合、GAS側でライブラリを導入していても、直接ライブラリの関数を呼び出す事は出来ません。あくまでもGAS側に用意されてる関数しか実行は出来ない仕組みになっています。ですので、ライブラリ化してる場合には、ライブラリの関数をラッピングしてGAS側で用意してあげる必要があります。
MDvtU0ospspq31f4ArmUs9G_JV1dIyBf4は自分が作ったライブラリのプロジェクトキーですが、この中にあるuidgene()という関数はランダムな数値を生成する関数ですが、例えば、以下のようなコードを実行しても呼べません。ore3という名前をつけてあります。
1 |
google.script.run.withSuccessHandler(onSuccess).ore3.uidgene(); |
この場合、GAS側に関数を用意するか?ラッピングした関数として以下のような形で用意すれば呼び出す事が可能です。HTML側はこのoreore()を呼び出して挙げればOKです。
1 2 3 |
function oreore(){ return ore3.uidgene(); } |
サイトに埋め込み
新しいGoogle Sitesに埋め込みする
旧Google Sitesに埋め込みする
作成したウェブアプリケーションをGoogle Sitesに埋め込む事が可能です。Google Sites上でもコードを記述して埋め込む方法とスプレッドシートなどの上に作成したコードを元に生成したものを埋め込む方法の2つがありますが、ここでは後者の方法を記述します。以下の手順で埋め込みましょう。
- ウェブアプリケーションとして公開した際に、最後にexecが付くURLをコピーしておく。
- Googleサイトの該当のページを開き編集モードにする
- メニューより、「挿入」⇒「Appスクリプト」を選択
- 下のほうにある「または、サービスとして既に公開されている Apps Script の URL を貼り付け:」のテキストボックスに1.のURLを入れて、選択ボタンを押す。
- Appスクリプトガジェットとして挿入されるので、サイズやフレームの表示などを調整する。
- 編集モードを終わらして保存する。
- ウェブアプリケーションが表示される。
自分もこの方法で各種申請用フォーム類などは表示するようにしています。直で1.のURLのページを表示させるようにしても良いのですが、ページURLが変わったりした場合に面倒なので、sitesの固定のページに貼り付けて閲覧してもらっています。縦長のフォームの場合はスクロールバーが出るのが嫌であれば、結構大きめの高さを入れるようにしましょう。
それ以外のサイトに埋め込みする
Google SitesはURLを指定すれば簡単に埋め込めるようになっていますが、例えば外部のサイト(社内で運用してるWordpressのイントラであったり、ローカルのindex.htmlなど)には通常埋め込みが出来ません。これはGoogle Apps Scriptで生成したウェブアプリケーションはデフォルトで「X-Frame-Options」という項目が入っており、外部サイトのiFrame内からは呼び出しが出来ない様になってるためです。ちなみにその場合Chrome Debuggerには以下のエラーが表示されます。
Refused to display ‘ここにURL’ in a frame because it set ‘X-Frame-Options’ to ‘SAMEORIGIN’.
そこでHTML Serviceに用意されてるオプションを指定して出力をすると、外部のサイトにてiFrameタグで埋め込みをすることが可能です。XFrameOptionsModeと呼ばれるもので、以下のようなコードで指定します。
1 2 3 4 5 6 7 |
//フォーム表示用 function doGet(){ var html = HtmlService.createHtmlOutputFromFile('workflow') .setSandboxMode(HtmlService.SandboxMode.IFRAME) .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); return html; } |
いつものコードに.setXFrameOptionsModeをつけ、引数にHtmlService.XFrameOptionsMode.ALLOWALLを入れるだけです。これで、iFrameタグでexecと最後につくウェブアプリケーションのURLを指定すれば、WordpressなどのサイトにAppスクリプトガジェットとして呼び出す事が可能です。ただし、作成者以外では表示画面の上に何やら英語の妙なメッセージが出ますが、これは仕様です。
※.setXFrameOptionsModeをつけることにより、iframe内にGoogle Apps Scriptのフォームを呼び出せるようにはなりますが、同時に他人が別の場所でiframe内に呼び出せる事にもなるので、クリックジャッキング対策が別途必要になる事があります。
実際に普通のGoogleアカウントで試してみましたが、匿名ユーザの閲覧も許可した所、以下のような形(リッチなGUIのアプリ参照)で表示ができました。匿名ではない指定をした場合には、Googleアカウントが必要です。また、自社ドメインのユーザのみの場合、当然外部のユーザは閲覧する事は出来なくする事が可能です。
また、XFrameOptionsModeの場合、Session.getActiveUserなどで情報を取得する事ができません。
上の方に出る青色の帯について
概要
社内で使う分には、「このアプリケーションはGoogleではなく、別のユーザによって作成されたものです」といった、灰色の帯は表示されません。しかし、外部向けのサイトでアカウントが無い人、社内アカウントじゃない場合、このメッセージが出て、非常にダサい感じになります。
これを解消する方法は、2種類のパターンがあります。
Google Sitesにページ全体で埋め込み
この手法がもっとも手軽でもっともスマートなのが、Google Sitesを用意して、そちらに「ページ全体で埋め込み」を利用して埋め込むだけで、この帯は表示されなくなります。もちろんアプリもサイトも外部に公開するか?対象者は閲覧可能な状態にしておく必要はあります。
こちらにサンプルを用意しました。
図:青色の帯が出ないで表示されるようになる。
外部ウェブサイト側にアプリを構築する
Google Sites側にはコードを直接記述して、GAS側はJSONなどでデータを配信するといった形に分けてあげると、綺麗な表示になります。(X-FrameOptions Modeの際には表示されないようで)。ただしこの手法はちょっとハードルが高い・・・
詳しくは、新しいGoogle SitesにネイティブなRSSリーダーを作ってみるを参照してみてください。
※また、個々のユーザにてChromeのカスタムJavaScriptにて対象のエレメントを非表示にする事で消すことも可能のようです。
1 |
document.getElementById('warning').style.display = 'none'; |
複数のプロジェクトを用意する
2つ以上のプロジェクトを用意する方法
Google Apps Scriptでは1つのスプレッドシートに1つしかdoGet()は置けないので、複数のウェブアプリケーションをそのままでは作ることができません。その為、例えば申請用ページをスプレッドシート上のGASで作り、承認用には別のシートか?単体のGASファイルを作って同じスプレッドシートを参照させるなんて形で作る事になります。しかし、Google Apps Scriptは1つのスプレッドシートに複数のプロジェクトを作る事が出来るようになっています。
それぞれのプロジェクトは
- 完全に独立したプロジェクトである
- それぞれのプロジェクトにdoGet()を置いた場合、それぞれ別のURLとしてウェブページを表示する事が可能。
- 但し、シートのデータは共有出来る。
- スクリプトプロパティも独立となるので、こちらは共有は出来ない。
といった仕組みになっています。つまり1シート2プロジェクトで2つのウェブアプリケーションを作る事が可能になっています。別々にファイルを用意するのではなく、プロジェクトを2つ作成して、別々にコーディングをすれば良いという訳です。新しいプロジェクトを作る場合には
- スクリプトエディタを開く
- メニューより「新規作成」⇒「プロジェクト」を選択
- 次回以降、スクリプトエディタを開くと、どのプロジェクトを編集するか?確認するようになります。
スクリプトプロパティを共通化する
1シート2プロジェクト体制の場合、スクリプトプロパティも個別になり具合が悪い事があります。その場合、3プロジェクトにしてあげて、3つ目のプロジェクトのスクリプトプロパティを1つ目、2つ目から読み書き出来るようにしてあげれば、共通化する事が可能です。以下の手順で共通化をしましょう(ライブラリ化する作業です。詳細は参照してみてください。)
- 3つ目のプロジェクトをつくり、スクリプトエディタにて読み書きする為の関数を用意する
- スクリプトエディタのメニューより「ファイル」⇒「版を管理」を開く
- 適当な名前をつけて、新しいバージョンを保存をクリック
- 続けてメニューより「ファイル」⇒「プロジェクトのプロパティ」を開く
- スクリプトIDを控えておく
- 1つ目のプロジェクトおよび2つ目のプロジェクトをそれぞれ開く
- スクリプトエディタのメニューより「リソース」⇒「ライブラリ」を開く
- ライブラリを追加に5.のスクリプトIDを入れて、追加ボタンをクリック
- バージョンを指定して、識別子を作ります。識別子は英語で他のライブラリと重複しない名前で適当につけます。自分はsetmanとつけてます。
- 既存のスクリプトプロパティを呼び出して書き込むコードをライブラリの関数を呼び出す形に書き換える。この時呼び出し方は、setman.kinoko("value")で、3つ目のプロジェクトのkinokoという関数にvalueを渡せるようになります。
図:こうしてライブラリ化しておくと参照が可能になる
ちなみに、自分が3つ目のプロジェクトでスクリプトプロパティを読み書きさせてるコードは以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//プロパティを取得する function getProp(value) { var Properties = PropertiesService.getScriptProperties(); var temp = Properties.getProperty(value); return temp; } //プロパティをセットする function setProp(key,value){ var Properties = PropertiesService.getScriptProperties(); Properties.setProperty(key,value); } |
Cloud Consoleプロジェクトも共通化する
1シート2プロジェクト体制の場合、Google Cloud Console側のプロジェクトも個別になります。これでは、Cloud Console側で設定した各種APIの設定や認証情報も個別になってしまうので、二度手間になります。そこで、以下の作業をする事で、1つ目のプロジェクト側に紐つける事が可能になります。(Cloud Consoleを弄ってみるを参照してください。)
- あらかじめ、Cloud Consoleにて手動でプロジェクトを作成しておき、プロジェクト番号の数字を控えておいてください。(プロジェクトIDじゃありませんよ)。今回はsystem400という名のプロジェクトを作っておきました。
- 1つ目のプロジェクトを開く。
- スクリプトエディタのメニューの「リソース」⇒「Cloud Platformプロジェクト」を開きます
- 今度は1.で控えておいた数字をプロジェクトを変更のテキストボックスに入力し、プロジェクトを設定をクリック。
- プロジェクト変更の確認ダイアログが出るので、確認をクリックする
- しばらく待つと切り替えが完了します。プロジェクトを変更しましたと出たら成功です。
- 2つ目のプロジェクトも同じく変更をしておきます。
- 変更をすると、もともとGoogle Apps Scriptについていたプロジェクトは削除されますのでご注意ください。
図:プロジェクト番号が移動に必要な情報です。
図:プロジェクト変更画面
図:プロジェクト確認ダイアログ
画面遷移について
Google Apps ScriptではHTMLファイルを複数作る事は可能なのですが、doGet()は1個しか設置出来ません。よって、ウェブアプリケーションとして表示出来るのは、1つのプロジェクトで1つだけです。その為、シングルページアプリケーションしか作れません。つまり、他のウェブサイトなどで見られるような画面遷移というものが出来ませんので、jQueryなどを使ってダイアログを表示したり、サイドバー的なものを用意して1ページ内で収めるようなコードを書かなければなりません。
しかし、createTemplateFromFileを使ったケースの場合、過去にも紹介しましたが複数のHTMLファイルを呼び出すような感じで、擬似的に画面遷移を作る事が可能です。あくまでもシングルページなのですがHTMLをごっそりチェンジ可能なので、複数の画面遷移的なものを作ってみたい場合は、挑戦してみましょう。
※Framework7を使った画面遷移を装備する事が可能です。また、HTML5のsectionタグを使った画面遷移というテクニックもおすすめです。
ページ内リンクについて
Google Apps ScriptのHTML上では、<a href="#hoge">のようなページ内リンクが利用出来ません。クリックをしても何も起きず、また、URLをコピーして開いても、真っ白です。こんな場合、jQueryを使う事でこのアンカーについてページ内リンクのように移動できるようにする事が可能です。
1 2 3 4 5 6 7 8 9 |
//ページ内リンクをクリックした場合の挙動 $('a[href*=#]').click(function() { var target = $(this.hash); if (target) { var jumpman = target.offset().top; $('html,body').animate({scrollTop: jumpman},600,"easeInOutQuart"); return false; } }); |
ただし、上記の場合、hrefが#で始まる何かをクリックした場合の挙動をコントロールしているので、別にアンカーに対してイベントリスナーでコマンドを割り当ててるとオカシナ挙動になるので、注意は必要です。
同時アクセス数について
HTML Serviceだけに限らず、Google Apps Scriptの同一スクリプトに対するアクセス集中に関する資料は、Google公式によると「最大30接続」。これについては、こちらでも立証しています。しかし、過去の経験から「一時的にアクセスが集中した場合、処理が止まる可能性」は十分にあります。スプレッドシート上の呼び出しであっても「同時呼び出しの数が多すぎます」といったエラーに遭遇する事があります。
当然ながら、Quotaに掛かるようなコードである場合、複数名同時に利用することによってLimitに掛かってしまい、エラーとなって正しく処理がなされない可能性があります。設計上考慮すべきは以下の項目になります。
- 瞬間的に30名以上の接続や利用が見込まれるケースの場合、スプレッドシートは1つでも、受ける窓口(HTML Service)は、部署毎やチームごとなどに分けてあげる必要性がある。
- UrlfetchAppのリミットは1日にコールできる数以外にも、10秒間の間に呼び出せるのがおよそ3回まで(これについては公式に掲載なし。しかし、事実Limit execeedでストップする)なので、ユーザの呼び出しに応じて使わないようにする。
- HTML Serviceの場合、オーナー権限での呼び出しだと1ユーザが全てを実行する事になるので、Limitに達しやすい。よって、実行権限は「各ユーザ毎の権限」で行うようにする(こうする事で殆どのQuotaは回避可能)
- ただし、3.の場合そのユーザに対して、対象のスプレッドシートへのアクセス権限も必要となるので、運用上シートを見せずに運用は、トリッキーな回避策が必要になる。
- 1.の場合、必ず排他制御が必要になる。また、スプレッドシートへの書き込みは必ずまとめて1発で書き込むようにしないと、排他制御の時間リミットに達するので、1行ずつ書き込みはしないように。
- 他のアプリからのアクセスを行う場合には、doGetやdoPostではなく、Google Apps Script Execusion APIを利用するようにしましょう。
- 大規模ユーザをさばく必要がある場合には、Cloud Functions + Cloud SQLの活用を検討し、スプレッドシート側は、管理者が必要な時に、Cloud SQLからデータを取れるように仕様変更をするのが望ましい。
- Google Formで対応出来るものは、そちらで対応させる事で、同時にGoogle Apps Script呼び出しを減らせます。但し、自動応答などのスクリプトを使ってる場合には効果はない。
図:StackOverFlow掲載の同時アクセスの計測結果
デプロイを自動化する
たまたまGithubを探索していたところ、Google Apps Script APIを利用する事で、現在公開されているウェブアプリケーションのデプロイを自動化出来ることがわかりました。但し、今回のコードは「同じURLでウェブアプリケーションをデプロイ」するパターンになります。
こちらのサイトにて、そのコードが公開されています。使い方ですが、以下の手順になります。予め、GCP側でプロジェクトを用意しておく必要があります。
- ウェブアプリケーションをデプロイしたいGASアプリのプロジェクト設定を開いてスクリプトIDを調べておく(以後、アプリAと呼びます)
- スクリプトを実行するGASにて、プロジェクト設定を開く(以後、アプリBと呼びます)
- アプリBのappscript.jsonを表示しておく
- アプリBのGCPプロジェクトを予め作っておいたGCPプロジェクト側の番号を登録して、紐つける
- GCP側では、サイドバーからAPIとサービス -> ライブラリを開く
- Apps Script APIを有効化しておく
- Githubのコードをコピーして、アプリBのエディタに貼り付ける
- 7.の中にあるprojectIdの変数に、1.のスクリプトIDを入れておく
- 7.の中にあるdescriptionの変数に、デプロイする版の名前を適当に入れる
- アプリBのsaveAndDeployNewVersion関数を実行し、承認をする。
- アプリAのウェブアプリケーションが同じURLにて、新しい版でリリースされる。
複数管理するGASのウェブアプリについて、スクリプトIDをアプリBで管理しておけば、リリース日に合わせて、コードを実行する事で一括でデプロイが出来る利点がありますね(トリガーも使えるので、リリース日に自動リリースをすることも可能)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
function saveAndDeployNewVersion() { var projectId = "1AUxZTkOqLpP33ywkgo8T2NJlkfZ3CznkkKvWiTAiHY6KIhq7hFqbjpsM"; // your script project's Drive ID. var description = "my new version"; // the description of the new version to create var webAppConfig = { access: "ANYONE", executeAs: "USER_ACCESSING" }; updateManifest_(projectId, webAppConfig); var newVersionNumber = saveNewProjectVersion_(projectId, description); var webAppUrl = deployNewProjectVersion_(projectId, newVersionNumber); Logger.log(webAppUrl); } /** * Update manifest (appscript.json) with the correct web app configuration * * @param {string} projectId - The script project's Drive ID. * @param {object} webAppConfig - The web app configuration (access / executeAs) */ function updateManifest_(projectId, webAppConfig) { // get current manifest var output = makeRequest_(projectId, 'content'); var files = output.files; for (var i in files) { if (files[i].type == "JSON") { var manifest = JSON.parse(files[i].source); manifest.webapp = webAppConfig; files[i].source = JSON.stringify(manifest); } } makeRequest_(projectId, 'content', 'put', JSON.stringify({files:files})); } /** * Save a new version of the script project. * * @param {string} projectId - The script project's Drive ID. * @param {string} description - The description for this version. * * @return {number} The version number for the newly created version. */ function saveNewProjectVersion_(projectId, description) { var payload = JSON.stringify({description: description}); return makeRequest_(projectId, 'versions', 'post', payload).versionNumber; } /** * Deploy for the first time the script as a web app or update the deployment with the new script version. * if already deployed, we should find a deployment named "web app meta-version" * * @param {string} projectId - The script project's Drive ID. * @param {number} newVersionNumber - The new version number of the project. * * @return {number} The url of the web app. */ function deployNewProjectVersion_(projectId, newVersionNumber) { var deploymentId = getWebAppDeploymentId_(projectId) || createDeploymentAsWebApp_(projectId, newVersionNumber); var payload = JSON.stringify({deploymentConfig:{versionNumber:newVersionNumber, description: "web app meta-version"}}); var output = makeRequest_(projectId, 'deployments/' + deploymentId, 'put', payload); var entryPoints = output.entryPoints; for (var i in entryPoints) { if (entryPoints[i].webApp) return entryPoints[i].webApp.url; } } /** * Create first deployment as an Apps Script web app with the new version of the project. * * @param {string} projectId - The script project's Drive ID. * @param {number} newVersionNumber - The new version number of the project. * * @return {string} The deployment ID for the deployment as an Apps Script web app. */ function createDeploymentAsWebApp_(projectId, newVersionNumber) { var payload = JSON.stringify({versionNumber: newVersionNumber, description: "web app meta-version"}); return makeRequest_(projectId, 'deployments', 'post', payload).deploymentId; } /** * Get the deployment ID for the deployment as an Apps Script web app. * If it exists, description should be "web app meta-version". * * @param {string} projectId - The script project's Drive ID. * * @return {string} The deployment ID for the deployment as an Apps Script web app. */ function getWebAppDeploymentId_(projectId) { var output = makeRequest_(projectId, "deployments"); if (output.nextPageToken) throw "Project contains more than 50 saved deployments, update code to retrieve all results"; var deployments = output.deployments; for (var i in deployments) { if (deployments[i].deploymentConfig.description == "web app meta-version") return deployments[i].deploymentId; } } /** * Make calls to the Apps Script API * Required scopes: * - https://www.googleapis.com/auth/script.external_request * - https://www.googleapis.com/auth/script.deployments * - https://www.googleapis.com/auth/script.projects * * @param {string} projectId - The script project's Drive ID. * @param {string} resourcePath - The resource path. * @param {string} [method] - the HTTP method for the request. * @param {string} [payload] - the payload (e.g. POST body) for the request. * * @return {object} The response from the Apps Script API. */ function makeRequest_(projectId, resourcePath, method, payload) { var baseUrl = "https://script.googleapis.com/v1/projects/"; var url = baseUrl + projectId + "/" + resourcePath; var options = { headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() } }; if (method == 'post' || method == 'put') { options.method = method; options.payload = payload; options.headers['Content-Type'] = 'application/json'; } return JSON.parse(UrlFetchApp.fetch(url, options)); } |
図:GASからデプロイができました
デプロイバージョンの制限
2024年2月29日、Google Apps Scriptのデプロイに関して小さな制限事項が設けられました。それがバージョンを最大200までとする制限。1プロジェクトについて200回デプロイした場合、201回目をデプロイしたい場合には、過去のバージョンをアーカイブするか?削除しなければデプロイできなくなります。
デプロイした過去のバージョンを削除したい場合には
- スクリプトエディタを開く
- プロジェクト履歴を開く
- 対象のバージョンの右にある「︙」をクリックして、このバージョンを削除するをクリック
ライブラリやウェブアプリケーションとしてデプロイしたものが対象になります。200回もデプロイすることはないとは思いますが、長い年月で溜まってしまった場合削除するのも大変な作業なので最新版は10個程度に留めてアーカイブなり削除なりマメにしておきましょう。
図:バージョンを削除しましょう
HTMLダイアログの閉じるを検知できない
Spreadsheet上でHTMLにてダイアログを出した時に、google.script.host.close()や、直接閉じた際に、以下のようなJSのコードでunloadを検知はできても、その後にGAS側のコードを呼び出すことができません。
故にユーザには処理の続行を明示的に示すボタンを押して処理を促すか?閉じたことをきっかけに発動するような仕掛けを使わないで実現するか?の2択になりますので要注意。
1 2 3 4 5 6 |
window.addEventListener("unload", function() { //閉じたらGAS側のコードを叩く google.script.run.testman(); } ); |
関連リンク
- Google Apps Scriptにおける「プロジェクト」の関係
- Google Apps Scriptで今どきっぽい社内向けウェブアプリを作る
- Google Apps Script でガジェットを作成してみる
- Passing data to html service
- Enum XFrameOptionsMode
- Embedding Google Apps Script in an iFrame
- 【GAS】ウェブアプリケーションとして導入→取得したURLの表示画面を共有したいがうまくいかない・・・
- HTML Serviceをiframeで他サイトに埋め込む方法
- tanaikech/taking-advantage-of-Web-Apps-with-google-apps-script
- Cannot embed Google Drive Files in an iframe
- Google Apps Script - Deploy as Webapp - new version every time?