WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

Google I/O で発表された GAS の新機能で Web アプリを作ってみた

Google I/O 、盛り上がってますね! 2 日目の基調講演では新サービスの Google Compute Engine も発表されて、 Google のクラウドサービスはまさに死角なしです。

それはさておき、初日の発表で一番印象に残っているものはなんでしょうか。メガネも捨てがたいですが、私は Google Apps Script (GAS) の新機能が最も嬉しいものでした。 GAS はサーバーサイドの JavaScript を使って Google Apps と連携するアプリケーションが構築できるサービスです。これまでも GUI Builder などでプリセットされた UI ウィジェットを表示することはできましたが、今回のアップデートで HTML / CSS / JavaScript による UI のフルカスタマイズが可能になり、自由度が大幅にアップしました。

そこで、本日は GAS を使って簡単なノートアプリを作り、その使い方をご紹介しようと思います。サーバーサイドも含めてすべて Web ベースの IDE で開発できるので、必要なのは Web ブラウザと Google アカウントのみ。とても手軽にはじめられるので、ぜひ試してみてください。

GAS で Web ページを表示する

まずは GAS で静的な HTML を表示するところをまで実装し、実際に Web ブラウザで表示します。 GAS の始め方から解説しますので、ぜひやってみてください。

プロジェクトを作成する

GAS を用いた開発は、 Web ブラウザベースの IDE を使って行います。これがかなりの優れもので、色分け機能付きのテキストエディタ、 GUI ビルダー、デバッガ、その他の管理機能など、 GAS 開発に必要なすべての機能が集約されています。この記事では IDE の機能の説明は最低限になっていますので、興味のある方は公式サイトを参照してください。

IDE は以下の URL を Web ブラウザで開くことによって使うことができます。 ログインページに飛んだときは、 Google アカウントでログインしてください。

https://script.google.com/

上記の URL を開くと、まず下のような画面になるはずです。

いろいろなプロジェクトテンプレートが用意されていますが、ここでは最も単純な「Blank Project」を選択してください。リンクをクリックすると、テキストエディタが表示されて、すぐにコーディングを開始できます。

ただし、この状態ではまだプロジェクトがファイルとして保存されていないので、左上の「Untitled project」となっている部分をクリックし、プロジェクト名(なんでもいい)を入力してください。これでプロジェクトが Google Drive に保存されます。別タブで Google Drive を開けば、保存したファイルが表示されるはずです。

コーディング

まずは Web ページとして表示する HTML を作成します。 IDE のメニューから [File]>[New]>[Html File] を選択し、ファイル名として「index.html」を指定してください。すると、 index.html のタブが追加され、 HTML が入力できるようになります。このように、 GAS のプロジェクトにはスクリプトファイル(.gs)や HTML ファイルを必要な数だけ追加できます。

ここでは、 index.html に以下の内容を入力してください。スタイル指定が少し長いですが、 HTML 自体はノートのリストを表示するための table と編集用のフォーム、それにいくつかのラッパー DIV だけのシンプルなものです。入力したら、 [File]>[Save] で保存しておいてください。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>TinyNote</title>
  <style>
    #wrap { font-size:12px; }
    #notelist { height:100px; margin:10px; border: solid 1px black; overflow:auto; }
    #notelist table { table-layout:fixed; border-collapse:collapse; width:100%; }
    #notelist td { padding:2px 8px; }
    #notelist td.col2 { width:150px; }
    #notelist td.col3 { width:20px; }
    #edit { margin:10px; border: solid 1px black; height:400px; position:relative; }
    #edit input[type="text"] {
      position:absolute; top:10px; left:10px; right:10px; border:solid 1px #ccc;
    }
    #edit textarea {
      position:absolute; border:solid 1px #ccc;
      top:40px; left:10px; right:10px; bottom:40px; }
    #edit input[type="reset"]  { position:absolute; right:100px; bottom:10px; }
    #edit input[type="submit"] { position:absolute; right: 10px; bottom:10px; }
  </style>
</head>

<body>
  <div id="wrap">

    <!-- ノートのリストを表示するテーブル -->
    <div id="notelist">
      <table>
        <tbody></tbody>
      </table>
    </div>

    <!-- 編集フォーム -->
    <div id="edit">
      <form>
        <input type="hidden" name="id">
        <input type="text" name="title">
        <textarea name="body"></textarea>
        <input type="reset" value="新規ノート">
        <input type="submit" value="保存">
      </form>
    </div>
  </div>
</body>
</html>

次に、 Code.gs のタブに戻り、サーバーサイドの JavaScript を入力します。元からある内容を削除し、代わりに以下の内容を入力してください。

function doGet() {
  var tpl = HtmlService.createTemplateFromFile('index.html');
  return tpl.evaluate();
}

doGet() は Web アプリの URL に GET リクエストがきた時に実行される関数です。他に doPost() もあります。

HTML のレンダリングは HtmlService を使って行います。 createTemplateFromFile() は指定されたファイルを読み込んで HtmlTemplate オブジェクトを作成し、 evaluate() メソッドでレンダリング結果を格納した HtmlOutput オブジェクトが取得できます。この HtmlOutput オブジェクトを doGet() の返り値とすることで、レンダリングされた HTML が Web ブラウザに表示される、という仕組みになっています。

ここでは単純に静的な HTML を返しているだけですが、 PHP のように HTML 内に JavaScript コードを埋め込み、動的に内容を変化させることも可能です。そのやり方については後述します。

Web アプリケーションとして表示する

これで最低限のコーディングができたので、いったん Web アプリケーションとして Publish し、結果を確認します。「Publish」という言葉を使っていますが、アクセス可能なユーザーを自分だけに設定できるので、他者からアクセスされる心配はありません。単に Web ブラウザからアクセスする URL を与える操作と考えればよいでしょう。

GAS スクリプトを Publish する際は、あらかじめ「バージョン」を作成しておかなければなりません。バージョンとはその時点でのプロジェクトの全ファイルのスナップショットで、それを公開対象とすることで、後にスクリプトを変更しても公開アプリケーションには影響がないようになっています。バージョンは [File]>[Manage Versions] で作成できます。適当なコメントを入力し(空でもいいですが)、「Save New Version」をクリックすれば新しいバージョンが作成できます。

バージョンを作成したら、 [Publish]>[Deploy as web app] で Publish が可能です。

それぞれの選択項目の意味は以下のとおりです。

Project version
公開するバージョンを指定します。
Execute the app as
アプリケーションの実行権限をアプリケーション作成者と実際にアクセスしたユーザーのいずれかに指定できます。各種 Google アプリケーションのデータにアクセスする際の権限になるので、実行したい処理によってどちらかを選択してください。
Who has access to the app
アプリケーションにアクセスできるユーザーの範囲を、 Only myself (自分だけ)、 Anyone (全 Google アカウント)、 Anyone, even anonymous (未ログインでもアクセス可能)から選択します。 Google Apps の場合はドメイン内の全ユーザーにすることも可能です。

今回のアプリケーションでは、ひとまずデフォルトのままで大丈夫です。「Update」ボタンをクリックすると、 Web アプリの URL が表示されます。

この URL を開けば、アプリケーションにアクセスできます(「latest code」というリンクをクリックしても可)。

まだ外枠だけなので、ここから機能を実装していきます。

ノートの保存処理

ひとまず HTML の表示ができたので、次はノートを保存できるようにします。今回の更新で ScriptDb という JSON データベースが追加されたので、それを使います。

今度はサーバーサイドから実装しましょう。 Code.gs に以下の内容を追加してください。

function dateToStr(date) {
  return [date.getFullYear(), date.getMonth()+1, date.getDate()].join('/') +
         ' ' + date.getHours() + ':' + date.getMinutes();
}

function postNote(form) {
  var db   = ScriptDb.getMyDb();

  var note = form.id ? db.load(form.id) : null;
  if(!note || note.kind != 'note') {
    note = { kind: 'note', createdAt: +new Date };
  }
  note.title = form.title || '';
  note.body = form.body || '';

  var stored = db.save(note);
  return {
    id:stored.getId(), title:stored.title, body:stored.body,
    createdAt:dateToStr(new Date(stored.createdAt)) };
}

postNote() がクライアントから直接呼ばれる関数です。 form 引数に HTML 上のフォームの内容が key-value な感じで入っているので、それを前述の ScriptDb に保存しています。

ScriptDb を使うためには、まず ScriptDb.getMyDb() でデータベースインスタンスを取得します。そして、 load() でオブジェクトの読み込み(引数は保存時に確定する固有ID)、 save() で保存(引数は保存する JSON 形式のオブジェクト、もしくは load() などで読み込んだオブジェクト)が基本です。

上記のコードでは、まずフォームで指定された ID のオブジェクトを読み込み、取得できない場合は通常のオブジェクトとして保存内容を構築します。そして、 db.save() で保存した後、オブジェクトの固有 ID 、および他のフィールドをクライアントに返しています。

サーバーサイドが実装できたので、次はクライアントサイドです。以下のスクリプトを index.html の適当な位置に挿入してください。

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>
  $(function() {
    // 保存ボタンがクリックされた際に呼び出される処理
    $('#edit input[type="submit"]').click(function(e) {
      e.preventDefault();
      google.script.run.withSuccessHandler(onPostSuccess).postNote(this.parentNode);
    });
  });

  // postNote() が完了したら呼び出される
  function onPostSuccess(note) {
    var tr = $('<tr>').data('id', note.id);
    tr.append($('<td>').text(note.title));
    tr.append($('<td class="col2">').text(note.createdAt));
    tr.append($('<td class="col3">').text('×').click(onDeleteNote));
    tr.click(onClickNote);
    $('#notelist tbody tr[data-id="'+note.id+'"]').detach();
    $('#notelist tbody').append(tr);
  }

  // ノートリストの行がクリックされた際のイベントハンドラ
  function onClickNote() {
  }

  // 削除ボタンがクリックされた際のイベントハンドラ
  function onDeleteNote() {
  }
</script>

GAS のクライアントサイドでは jQuery (および jQueryUI)が使えるので、 Google Ajax Libraries API から読み込んでいます。

上記の処理のポイントは、 google.script.run〜で先ほど実装したサーバーサイドの postNote() を呼び出している部分です。サーバーサイドの関数をクライアントサイドから直接呼び出すことが可能です。上記のコードでは withSuccessHandler() を挟んでいるのでちょっとわかりづらいですが、単に postNote() を呼ぶだけならさらに単純になります。

google.script.run.postNote(this.parentNode);

つまり、 google.script.run に続けてサーバーサイドの関数を指定すれば、直接呼び出しが可能というわけです。引数としては文字列や数値、それらを要素としたオブジェクトや配列が任意の数指定できます。また、上記のようにフォームを直接渡せば、 JSON 形式に変換してくれます。さらに、上記コードのように withSuccessHandler() を挟んだ場合は、リクエストが成功した後に指定したコールバック関数 (onPostSuccess) を呼び出してくれます。コールバックの引数はサーバーサイドの関数の返り値です。

onPostSuccess() では、 postNote() からの返り値をもとにノートリストの table に行を追加し、タイトルや作成日時、削除ボタンを追加しています。ついでに行や削除ボタンがクリックされた際のイベントハンドラも追加してあります。

これでノート追加のコーディングは終わりですが、実行するためには ScriptDb のサービスを利用する権限をスクリプトに付加する必要があります。 Code.gs のタブを選択し、 [Run]>[doGet] を選択してください。以下のダイアログが表示されるので、「Authorize」ボタンで承認してください。

これでスクリプトに ScriptDb にアクセスする権限が付与されました。あとは先ほどと同じようにバージョンを追加し、公開設定をすれば実行できます。ページにアクセスしてノートを保存すると、上部のノートリストにエントリが追加されるのが確認できるはずです。 先ほどと同じ URL にアクセスしてノートを保存すると、上部のノートリストにエントリが追加されるのが確認できるはずです。

ちなみに、たぶん予想がつくと思いますが、動作確認のたびにバージョン追加→公開の作業をするのはなかなか面倒です。今回はチュートリアルということで大枠から作っていますが、関数単位であれば IDE から直接実行できるので、本来は下位処理から作って最後に組み立てるのが効率的でしょう。もしくは、最初は Google Spreadsheets 上のアプリケーションとして開発するほうがよいかもしれません。


2012/07/17 追記
この部分は私の勘違いでした。動作確認のたびにバージョンを追加する必要はなく、そのままで変更内容が適用されます。お詫びして訂正いたします。


ノートリストの表示

ノートの保存はできるようになりましたが、ページをリロードするとノートリストからエントリが消えてしまいます。サーバーサイドのデータベース (ScriptDb) には残っているのですが、 index.html のレンダリング時にそのデータを反映していないためです。

そこで、 HtmlTemplate の機能を使って ScriptDb からフェッチした内容を HTML に埋め込んでみましょう。まずは doGet() 内で ScriptDb から必要なデータをフェッチします。

function doGet() {
  var tpl = HtmlService.createTemplateFromFile('index.html');
  var db = ScriptDb.getMyDb();
  tpl.notes = db.query({kind:'note'}).sortBy('createdAt', db.NUMERIC);
  return tpl.evaluate();
}

中の 2 行が追加分です。 ScriptDb からデータをフェッチするには、 query() メソッドを使います。引数は検索条件で、ここで指定したフィールドに指定した値が設定されているオブジェクトがすべてフェッチされます。複数のフィールドを指定しての AND 検索や OR, NOT 等もあります。上記のコードでも使っているように、 sortBy() で結果をソートすることも可能です。詳細は公式サイトのドキュメントを参照してください。

クエリーの結果は ScriptDbResult オブジェクトで返されます。上記のコードではそれを HtmlTemplate オブジェクトの notes フィールドに設定していますが、こうすることでテンプレート内の JavaScript コードに値を引き渡すことが可能です。

テンプレート側 (index.html) では、 tbody タグの内部を以下のように変更します。

<tbody>
  <? while(notes.hasNext()) {
       var note = notes.next(); ?>
    <tr data-id="<?= note.getId() ?>" onclick="onClickNote.call(this);">
      <td><?= note.title ?></td>
      <td class="col2"><?= dateToStr(new Date(note.createdAt)) ?></td>
      <td class="col3" onclick="onDeleteNote.call(this, event);">×</td>
    </tr>
  <? } ?>
</tbody>

なんとなくわかるかと思いますが、「<? 〜 ?>」の内部が JavaScript コードとなっています。「<?= 〜 ?>」は、コードの実行結果を HTML に挿入します(エスケープ付き)。

JavaScript コード内でアクセスしている notes 変数は、 doGet() 内の notes 変数と同じものです。見ての通り、結果は配列ではなく、イテレータ形式になっています。 hasNext() で次の結果があるかどうかを確認し、 next() で次の結果を取得します。これを while ループで回し、クエリー結果の数だけ行を生成しています。

ノートの選択を実装

次は、保存済みのノートを編集する処理を実装します。データを保存する処理はすでに既存データの上書きに対応しているので、ノートリスト上の行がクリックされた際にデータをフォームに設定すれば OK です。具体的には、 index.html にある onClickNote() を以下のように実装します。

function onClickNote() {
  var noteId = $(this).data('id');
  google.script.run.withSuccessHandler(function(note) {
    $('#edit input[name="id"]').val(noteId);
    $('#edit input[name="title"]').val(note.title);
    $('#edit textarea').val(note.body);
  }).fetchNote(noteId);
}

クリックされた tr タグの data-id 属性からノートの固有 ID を取得し、それをサーバーサイドの fetchNote() に渡してノートの内容を取得しています。 fetchNote() の実装は以下のとおりです(Code.gs に追加してください)。既出の機能を使っているだけなので、説明の必要はないでしょう。

function fetchNote(noteId) {
  var db   = ScriptDb.getMyDb();
  var note = db.load(noteId);  
  return { title:note.title, body:note.body };
}

ノートの削除の実装

最後に、ノートの削除処理を実装します。基本的には編集の処理とほぼ同じで、「×」がクリックされた際にサーバーサイドの deleteNote() を呼び出し、 ScriptDb から対応するオブジェクトを削除するだけです。以下に追加コードを示します。

index.html の追加分

function onDeleteNote(e) {
  e.stopPropagation();
  var row = $(this.parentNode);
  google.script.run.deleteNote(row.data('id'));
  row.detach();
}

Code.gs の追加分

function deleteNote(noteId) {
  if(noteId) {
    ScriptDb.getMyDb().removeById(noteId);
  }
}

以上で、ひととおりの機能が実装できました。外見はノートの新規作成実装後から変わっていないので画像は掲載しませんが、ノートの作成・編集・削除が可能になっています。

感想

ということで、 GAS を使って簡単な Web アプリを作ってみました。触ってみた感想としては、良い点と足りない点が概ね半々かなぁ、というところです。

まず良い点ですが、クライアントサイドを自由に構築できるようになったこと + 手軽に使えるデータベースの追加で、従来の GAS と比べてできることが大幅に増えました。 GAS を ScriptDb や他の Google サービスにアクセスするためのスタブのように使い、多くのビジネスロジックをクライアントサイドで処理することで、本格的な Web アプリケーションの構築も可能だと思います。

しかし、残念ながらクライアントサイドの JavaScript のデバッグはかなり厳しいものがあります。コードが圧縮されている上に Caja で CSS も含めて書き換えられているのでブラウザのデバッガがほとんど役に立ちません。 alert や console.log を駆使する昔懐かしいデバッグ方法に頼らざるを得ません。今回のスクリプトでも、仕様がきちんと理解できなかったこともあり、だいぶ苦労しました。

また、とても便利な ScriptDb ですが、残念ながら容量に厳しい制限があります。例えば通常の Google アカウントの場合、ユーザーごとに 50MB で、これを(私の勘違いでなければ)すべてのスクリプトで共有することになります。また、ひとつのオブジェクトの容量もだいたい 4KB くらいでないとまずいようです。 ScriptDb はメタデータの保存などにとどめ、データ本体は従来通り Google Spreadsheets などに保存するのが無難でしょう。

ともあれ、今回のアップデートで GAS の可能性が大きく広がったことは間違いありません。最近の Google は GData API にほとんど力を入れてないので、その意味でも GAS は Google Apps の活用に必須のものとなっていくでしょう。

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles