複数のhtmlをGASで公開する

Google Apps Script (GAS)は、javascriptに非常に近い文法で記述されたスクリプトでGoogleのアプリケーションを動かすことができるサービスであり、htmlで作成したGUIつきのウェブアプリケーションを作ることもできます。

今回は、htmlで作成したUIが複数あり、それをひとつのプロジェクトでうまく公開してやりたい、という場面についてお話ししようかと思います。

### 開発環境

私はGoogle Claspを用いてTypescript環境でGASを開発していますが、本記事のコードは、函数の引数や戻り値に対する型付けを外せば、おおむね.gsファイルとしても使えるようになっていると思います。無理そうなら[Typescript Playground](https://www.typescriptlang.org/play/)などでJavascriptに変換すれば、ローカルにTypescript環境のない方でもご活用いただけるかと思います。

* Google Clasp
* @types/google-apps-script

# GASがどのようにhtmlを表示しているか

GASでhtmlファイルを公開するためには、いくつかのステップを踏む必要があります。ここでは本筋ではありませんので、軽くリストアップするにとどめましょう。

1. htmlファイルと、"doGet"という名前の函数が含まれる.gsファイルを作る（函数の内容は後述します） 
1. スクリプトエディタの「公開」メニューから「ウェブアプリケーションとして公開」を行う

こうすると、ウェブアプリケーションのリンク（ウェブアプリケーションとして公開した際に得られます）を踏んだ際に、Googleのサーバー側にリクエストが送られて、所定のhtmlファイルを読み込み、HTMLOutputが返ってきます。こうしてGASのhtmlを表示することができるのです。

たとえば、ウェブアプリケーションのUIとして、index.htmlの内容を表示したい場合には、次のようにすればよかろうと思います。

```ts:doGet.ts
const doGet = (e:GoogleAppsScript.Events.AppsScriptHttpRequestEvent):GoogleAppsScript.HTML.HtmlOutput => HtmlService.createHtmlOutputFromFile("index");
```

``doGet``函数の引数``e``は、[公式リファレンスのこのページ](https://developers.google.com/apps-script/guides/web#request_parameters)にあるRequest Parametersというもので、Query Parametersなどが格納されたオブジェクトです。

# 複数のHTMLファイルを状況に応じて表示する
GASによるアプリケーションの開発においては、「似たようなロジックだけども、複数のUIを提供したい」という場面もあると思います。たとえば、スプレッドシートをデータベースとして使っているプロジェクトにおいて、

* データを検索し、整形して表示するhtml
* 新しいデータを追加するためのフォームを表示するhtml

のふたつをアプリケーションとして公開したい、という場面などでしょうか。こうした場合に、それぞれについてGASのプロジェクトを立てたうえで、似たような函数定義をそれぞれでしなければならないのは面倒ですし、メンテナンスの観点からも好ましくありません。

こういった際には、Query Parametersを活用した効率的なUIの公開を行いたいところです。以下、その方法を解説していきます。

## Query Parametersにファイル名を渡す
公開したウェブアプリケーションのURLは、``https://script.google.com/.../exec``というような形になっていると思います。この末尾に``?page=index``のような形でファイル名を渡しましょう。

Query Parameterを附したURLにアクセスがあるたびに、GAS側の``doGet``函数が呼び出されます。この``doGet``函数の引数``e``には、次のような形でQuery Parametersの値が格納されるのでした。

```ts
e.parameter = {
	"parameter1": value1,
	"parameter2": value2, // 以下続く
}
```

したがって、たとえば先のように、表示したいhtmlファイルの名前を``page``というQuery Parameterにして渡すとすれば、この値は``doGet``函数の中で受け取ることができます。Javascriptで開発されている方は型定義は必要ありませんが、Typescriptの場合はそのまま書くとエラーを吐きますので、まずどのようなparameterを渡すのかを型定義してやりましょう。

```ts:doGet.ts
interface WebAppOnOpen extends GoogleAppsScript.Events.AppsScriptHttpRequestEvent {
      parameter: {
            page: string | undefined
      }
}
```

そのうえで、``doGet``函数を以下のように定義します。

```ts:doGet.ts
const doget =  (e: WebAppOnOpen): GoogleAppsScript.HTML.HtmlOutput => {
      if (e.parameter.page) {
            return HtmlService.createHtmlOutputFromFile(e.parameter.page)
      } else {
            Logger.log("error: Required Query Parameter not Provided!")
            return HtmlService.createHtmlOutput("URLの末尾に「?page=***」の形式で種別を指定する文字列を追加してください。")
      }
}
```

もちろん、存在しないhtmlファイルが読み込まれた場合に、「そのページは存在しない」ということを表示する処理を書いてもよいと思います。無効なHTMLOutputが返されるとGASデフォルトのエラーページが表示されますので、ここではそれは省略してしまいました。

これで、複数のhtmlファイルをひとつのプロジェクトに入れて、状況に応じて表示し分けられるようになりました。

## 補足
* 「ウェブアプリケーションとして公開」で得られるURLは、なんど公開しなおしても（バージョンを上げても）変化しません。したがって、query parameterつきのURLをほかの人に渡してしまっても、アップデートを続けることが可能です。
* Google Claspで開発している場合、``clasp push``を行うとウェブアプリケーションの公開設定がリセットされます。この場合、再度メニューからウェブアプリケーションとして公開しなおす必要があります。私の調べた限り、これは手動で行うしかなさそうです。

# 蛇足：html側にQuery Parameterの値を渡す
今回は、Query Parametersをページの表示を制御する目的だけに使用していますが、場合によってはフロントのhtml側で利用したい場面もあるでしょう。その場合には、次のようにするとよかろうと思われます。

```ts:doGet.ts
interface WebAppOnOpen extends GoogleAppsScript.Events.AppsScriptHttpRequestEvent {
      parameter: {
            [key: string]: string | undefined;
      }
}

const doGet_ = (e: WebAppOnOpen): GoogleAppsScript.HTML.HtmlOutput => {
      if (e.parameter.page) {
            return Object.keys(e.parameter).reduce((htmlTemplate, parameterKey) => {
                  if (e.parameter[parameterKey]) {
                        htmlTemplate[parameterKey] = e.parameter[parameterKey]
                  }
                  return htmlTemplate
            }, HtmlService.createTemplateFromFile(e.parameter.page)).evaluate()
      } else {
            Logger.log("error: Required Query Parameter not Provided!")
            return HtmlService.createHtmlOutput("URLの末尾に「?page=***」の形式で種別を指定する文字列を追加してください。")
      }
}
```

たとえば、先ほどと同じくスプレッドシートをデータベースにしている場面において、「特定の行のデータだけを取得し、そのデータに基づいてUIを表示したい」などが例として挙げられるでしょうか。

このような方法でhtml側に渡したパラメータ値は、html側では次のようにして取得することができます。

```html:page.html
<script>
const parameter = <?=parameterName?>
</script>
```

したがって、先ほど挙げたような例は、次のような.gs(ts)と.htmlを用意すれば実現できます。

```ts:getData.ts
const getData = (row: number):string => {
      const sheet = SpreadsheetApp.openById(sheetId).getActiveSheet()
      const lastColumn = sheet.getLastColumn()
      return JSON.strinfigy(sheet.getRange(row, 1, 1, lastColumn).getValues())
}
```

```html:page.html
<script>
google.script.run.withSuccessHandler(responce => {
      console.log(JSON.parse(responce)) // 取得したデータをconsoleに表示
}).getData(Number(<?=row?>))
</script>
```

この場合、``getData``函数の返り値として配列を返してしまうと、html側では``null``がconsoleに表示されます。``JSON.stringify()``をかませ、文字列を返す必要があることに注意してください。