Google Apps Scriptで今どきっぽい社内向けウェブアプリを作る

今どきっぽいというのは、各種JavaScriptライブラリを使うという意味です。ここでは、[Bootstrap](href="http://getbootstrap.com/)と、[Knockout.js](http://knockoutjs.com/index.html)を使ったサイトを作ってみます。

HTML Serviceの強化
==================

@dz_ さんの記事の[Check! Google Apps Script - UIの実装は HtmlService + Polymer の利用が主流に？] (http://qiita.com/dz_/items/4391ab8c94498cbc512c)に書かれているように、Google Apps ScriptのUI周りに少し手が入ります。

* 今まではUi Serviceというのを使っていた。JavaとかC++のGUIフレームワーク的な仕組みで、サーバサイドでUIのパネルとかボタンとかラベルを作り、コールバックも定義してあげる仕組み。ただし、生成されるHTMLはHTML 3.2的なテーブルレイアウトだったりするのはご愛嬌。ただし、6/30で終了。
* 今後はHTML Serviceというものがメインに。PHP的なテンプレートエンジン(サイトの説明のまま)を提供。そんでもって、2014/12/11にIFRAMEモードというものが追加され、標準的なHTML、JS、CSSが使えるようになる。

いままでは、生成されたHTMLはCajaというサニタイズのフィルタを通して結果がブラウザに返されてました。それと、サーバ側で定義されたコールバックへのつなぎ込みなど、大量のJSコードが生成されていました。

そのあたりのアーキテクチャは、より普通のウェブアプリっぽくなります。ただしIFRAMEの中なので、スクリプトでリロードとかはできません。

Bootstrapを使ったウェブアプリのひな形
=================================================

@dz_ さんはPolymerを使うのにいろいろ苦労されていますが、それは静的コンテンツがGoogle Apps Scriptのプロジェクトに追加できないのが要因です。外部CDNで提供されているものを使えば、そこは楽できます。Polymerはまだdev previewというステータスで、まだCDNでの提供はされてないので、ここでは枯れたBootstrapと、knockout.jsを使います。

プロジェクトを作って、main.gsに以下の内容を書き込みます。Google Apps Scriptの新規作成で"Web App"を選ぶと、古きよきUi Serviceを使ったコードを生成しますが、それはすべて消します。

```js:main.gs
/*
   ブラウザアクセスのエントリーポイント
 */
function doGet(e) {
  t = HtmlService.createTemplateFromFile('index.html');
  t.title = 'タイトル';
  t.data = JSON.stringify(getContent());
  return t.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

/** コンテンツ */
function getContent() {
  return [{"name": "Shibukawa Yoshiki", "age": 34}];
}
```

doGetが、ブラウザから呼ばれたときに呼ばれるメソッドです。HTMLを生成して返しています。

getContent()は、適当に配列でデータを返しています。SpreadsheetAppとか使ってスプレッドシートのデータを返すようなコードを書くことになると思いますが、とりあえずシンプルに配列で返しています。

```html+php:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title><?= title ?></title>
  <!-- Bootstrap -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">

  <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
  <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
  <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
  <![endif]-->
  <style>
  body {
    padding-top: 50px;
  }
  .mainpanel {
    padding: 40px 15px;
  }
  .logpanel {
    width: 100%;
  }
  </style>
</head>
<body>
  <nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
      <div class="navbar-header">
        <div class="navbar-brand"><?= title ?></div>
      </div>
    </div>
  </nav>

  <div class="mainpanel">
    <div class="container">
      <div class="row">
        <div class="col-md-8">
          <table class="table table-striped">
            <thead><th>名前</th><th>年齢</th></thead>
            <tbody data-bind="foreach: people">
              <tr>
                <td data-bind="text: name"></td>
                <td data-bind="text: age"></td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
  
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
  <script src="https://ajax.aspnetcdn.com/ajax/knockout/knockout-3.1.0.js"></script>
  <script type="text/javascript">
    var model = { people: JSON.parse(<?= data ?>) };
    
    $(document).ready(function () {
      ko.applyBindings(model);
    });
  </script>
</body>
</html>
```

Bootstrapのサンプルを適当に改変して、表のレンダリングにKnockout.jsを使っています。このプロジェクト固有のcssを定義して<link>タグで読み込むのは、静的コンテンツを追加できないという特性上できないので、そこはstyleタグで書き下しています。さっそくブラウザで動かしてみましょう。

1. File -> Manage Versions... を選択して、Save New Versionボタンを押す ![Screen Shot 2015-01-28 at 19.55.13.png](https://qiita-image-store.s3.amazonaws.com/0/6105/3e5dec42-de58-26f6-5098-f54c062957b7.png)

2. Publish -> Deploy as web appを選択して、Deployボタンを押します。![Screen Shot 2015-01-28 at 20.00.08.png](https://qiita-image-store.s3.amazonaws.com/0/6105/9344d6f5-40c2-87ce-173f-b1b3e4e93c6d.png)

これで公開の準備ができました。Deploy as web appダイアログで公開範囲などを決められます。あと、一度公開してしまったら、デプロイ後の画面や、再度開いたDeploy as web appのダイアログの「Test web app for your latest code.」と書かれているリンクをクリックすることで、ファイル保存だけで最新のコードで試すことができます。

![Screen Shot 2015-01-28 at 19.25.25.png](https://qiita-image-store.s3.amazonaws.com/0/6105/1b5b9fc7-01bd-15d4-598d-40a0fead3d54.png)

これでウェブサービスが動きました。

サーバとクライアント間のデータのやりとりの方法
================================================

最初のサンプルでは、JSON.stringify/parseを使って、テンプレート経由で文字列としてデータを渡しました。これも待ち時間なし(白い画面)にユーザに見せることなく表示できるので有効ですが、Ajax的にコンテンツを取得することもできます。

```js
function updateContent(data) {
  model.people = data;
}

// なんらかのボタンなどのイベントから呼ばれる関数
function onUpdate() {
  google.script.run.withSuccessHandler(updateContent).getContent();
}
```

サーバ側で定義した関数getContent()を非同期で呼び出すことができます。サーバからreturnで返されるデータが、withSuccessHandlerで指定された関数の引数として渡されます。エラー時のコールバックも登録することができ、runと最終的に呼びたい関数の間のどこかにwithFailureHandler(コールバック)をはさみます。この場合はJSON.stringifyなど呼ぶ必要はありません。

ウェブアプリとして見た時の特徴
============================================================

* お金がほとんどかからない。めちゃくちゃアクセスがあって、なおかつ有効にしないと使えない(Google Driveなど)APIを使っていればお金かかるかもですが、クオータも高めなのでほとんど問題ないでしょう。
* サーバのメンテが不要。PaaSバンザイ。
* 静的コンテンツのホストができない。JS/CSSを別ファイルに分けるには[IFRAME以前時代のファイル分割のこの手法](https://developers.google.com/apps-script/guides/html/best-practices#separate_html_css_and_javascript)を使う。
* パフォーマンスはそこまで高いわけではないが、10までのリクエストは並列で処理してくれるらしい。
* doGet/doPostは最初に呼ばれるエントリーポイントだが、それ以外に非同期で呼ばれるAPIはURLのルータなどを定義する必要はない。JSONをそのままクライアントとサーバでやりとりすることができる。
* Google Driveなど、GoogleのAPIが簡単に使える。
* 今回は説明しませんでしたが、ちょっとしたストレージなら、Spreadsheetなどを使わなくても、PropertiesService.getScriptProperties()を使えば十分。
* エントリーポイントがひとつ(正確には、doGetとdoPostの２つだが、URLはひとつ)しか持てない
* Google Driveのアクセス制御でアクセス権を設定できる。

CSSが得意でなくても、今回新たに使えるようになったIFRAMEモードを使えば、Bootstrapを使って短期間でまともに見えるサービスが作れます。社内向けには必要十分な性能だし、お手軽にそこそこのものを作るには良いと思います。Polymerが早くホスティングされて簡単に使えるようになったらいいですね。

参考
====

* [Google Apps Scriptの開発手法まとめ](http://qiita.com/soundTricker/items/4d04c97c499b22886dfd) (@soundTricker さん)
* [Check! Google Apps Script - UIの実装は HtmlService + Polymer の利用が主流に？] (http://qiita.com/dz_/items/4391ab8c94498cbc512c) (@dz_ さん)