Closure Tools, JSRender, JSView, Backbone.js, JQuery UI,

いろいろなライブラリ、フレームワーク、ツールが必要だった。
"ちゃんと"JavaScriptでWeb Applicationで開発するには、いろいろと準備する必要がある。
最近、Amazon S3のWeb Consoleがバグっていたので、Amazon S3のブラウザをChrome Extension「S3 Tree」を作っていた。参考にしていたのは、下記の本。


JavaScript Web Applications: jQuery Developers' Guide to Moving State to the Client

JavaScript Web Applications: jQuery Developers' Guide to Moving State to the Client

Web Application開発に必要なもの

Web Applicationを、"ちゃんと"作ろうとすると、次の5つが足りない。
- モジュールロード
- テンプレート エンジン
- ViewとModelのバインディグ
- MVCフレームワーク
- UI Component

Javaもそうだけど、いくつものフレームワークやライブラリがある。フレームワークやライブラリに何を使うかを決めることから始めなければならない。しかも、Javaよりもたちが悪く、デファクト・スタンダードと呼べるものがなさそう。「JavaScript Web Applications」の中でも、"Great Alternative"が何回も登場する。どれを使えばよいか分からない。いろいろ調べながら開発を進めていた。最後は、Closure Tools, JSRender, JSView, JQuery UI, Backbone.jsを組み合わせたカタチになっていた。

モジュールロード「Closure Tools」

JavaScriptはモジュールロードする仕組みがなく、自分で用意しなければならない。「JavaScript Web Applications」では、サーバサイドJavaScriptの標準仕様を作ろうとしているCommonJSの仕様ベースのYabbleRequiteJSが紹介されていた。使ってみたが、非同期でモジュールを読み込むので、モジュールのロードと実行のタイミングが入れ替わるとエラーが発生する。多段にモジュール読み込みがあると使い物にならない。

それで、Closure Toolsを使うことにした。Closure ToolsはライブラリからテストツールまでそろったAll In Oneのツールと思っていたが、モジュールロードの仕組みを使うだけであれば、goog/base.jsとcalcdeps.pyだけで、使うことができる。Closure Toolsは一手間かかるが、現実的なアプローチでモジュールのロードをする。

  • モジュールの依存関係の定義

goog.provideとgoog.requireでモジュールの依存関係を定義していく。goog.provideが公開するための名前、goog.requireが依存しているモジュールの名前となる。

 goog.provide("s3.app");
 goog.require("s3.load");
  • 自動生成されるファイル

上記のようにモジュールの依存関係を定義しておき、calcdeps.pyを動かすと、モジュール依存関係を表すdeps.jsを生成することができる。

goog.addDependency("../../s3/app.js", ['s3.app'], ['s3.load', 'downtown.EventBus', 's3.view.login', 's3.view.bucketselector', 's3.view.treeview', 's3.view.listview', 's3.view.S3Router']);
goog.addDependency("../../s3/deps.js", [], []);
goog.addDependency("../../s3/load.js", ['s3.load'], ['downtown.Loader']);

この自動生成されるdeps.jsがあることで、起動時にすべての依存モジュールを読み込むことができる。非同期読み込みの問題は発生しない。さすがGoogleのツールだ。

モジュールのロードが使えるようになることでファイルの分割が簡単になり、1クラス1ファイルで管理することができる。

こんなファイルになる。

goog.provide("downtown.EventBus");
/**
*@constructor
*/
downtown.EventBus = function() {
  this.dispacher = jQuery("<div></div>");
};

(function($) {

  downtown.EventBus.prototype.bind = function(type, data, callback) {
    this.dispacher.bind(type, data, callback);
  };
  downtown.EventBus.prototype.trigger = function(type, data) {
    this.dispacher.trigger(type, data);
  };

}(jQuery));

テンプレート エンジン「JSRender」 ViewとModelのバインディグ「JSView」

ここはミーハーに、JSRenderJSViewを使ってみて、機能としては、不便はなかったのでそのまま利用した。

JSRenderは1つだけ使い方を工夫して、HTMLファイルを独立して管理できるようにした。

  • 別のファイルからHTMLのテンプテートを読み込む
  var loadedHtml = {};
  downtown.Loader.loadHtml = function(src, callback) {
    var path = settings["basePath"] + src + ".html";
   // すでに読み込んでいたら読み込まない。
    if (loadedHtml[src]) {
      // コールバックは非同期で
      setTimeout(function() {
        callback(src);
      }, 0);
      return;
    }
    // HTMLをロードする
    $.ajax({
      async : true,
      dataType : "text",
      url : path,
      success : function(data) {
        loadedHtml[src] = true;
        // JSRenderに登録する
        $.template(src, data);
        callback(src);
      }
    });
  };

このメソッドがあると、下記のようなコードでHTMLをファイルから読み込むことができる。

  • HTMLファイルのロード
downtown.Loader.loadHtml("s3/view/bucketselector/list", function(src) {
      var html = $($.render(self.model, src));
      html.appendTo(self.el);
      self.bindEvent();
    });

MVCフレームワーク「Backbone.js」

ここまでで、JavaScriptファイルとHTMファイルを分割して開発を進めることができる仕組みが準備でいた。次はプログラムに構造を持たせることができるフレームワークを導入した。
JavaScript Web Application」では、Backbone.jsの他にもknockout.jsJavaScriptMVCが紹介されていた。
knockout.jsは使いやすそうでしたがMVVMモデルのため、HTMLにロジックを持たせることになりそうだったので辞めました。MVVMは画面の開発ツールが"しっかり"していないと、使いこなせない気がします。
JavaScriptMVCはクラスの宣言が独特でした。

  • JavaScriptMVCのクラスの宣言
$.Model("Todo",{ findAll : "/recipes" }, {});
Todo.findAll(function(todos){ ... });

クラスの宣言が独特だと、Closure ToolsやEclipseと相性が悪くなるので辞めました。

  • Closure ToolsとBackbone.jsを組み合わせる

Closure Toolsのクラスの宣言方法に従って、Backbone.jsのクラスを使えば、組み合わせることができます。

/**
 * @constructor
 */
s3.view.bucketselector.BucketSelectView = function(attr) {
  var self = this;
  // Backbone.jsのベースクラスのコンストラクタを呼び出す
  Backbone.View.call(this, attr);
  this.eventBus = new downtown.EventBus();
};
// Backbone.jsのベースクラスを継承する。
goog.inherits(s3.view.bucketselector.BucketSelectView, Backbone.View);

Closure Toolsのクラスの宣言方法は、Eclipseとも相性が良くEclipseでコード補完も使えるようになり便利です。

Backbone.jsのモジュール構造

S3 Treeでは有効に使えなかったですが、Backbone.jsはJavaScriptでのWeb ApplicationのMVCの考え方を教えてくれます。Backbone.jsを使って開発を進めると、ModelとViewとRouterを作ることになります。

  • Model

Modelはサーバと接続部分を担当し、通信前のバリデーションのチェックやXMLデータのパース、aJaxリクエストの送信を担当します。

  • View

HTMLのDomとのやりとりはViewに実装します。Viewはthis.elでDomのエレメントを、this.modelでModelを持ち、ModelとテンプレートからのHTMLの生成や、ModelとDomのデータのバインディング、DOMイベントのバインディング等、ユーザインタフェースに関係する処理をすべてViewが担当します。

  • Router

aJaxのアプリケーションは戻るボタンとの連携ができない問題があります。Routerを使うと戻るボタンとの連携ができます。
aJaxアプリケーションで戻るボタンを利用するには、window.location.hashを利用します。hashを利用すると画面遷移なしにURLを変更することができ、戻るボタンとも連携することができるため、aJaxアプリケーションで戻るボタンやブックマークが使えるようになります。
Routerはhashの変更のイベントを受け取り、hashの種類ごとに実行するロジックを振り分けを行います。
# ただし、hashに"/"が入っているとRouterの振り分け機能が動作しなかったので、
# 利用するのは辞めました。
# 同じような処理は、jQueryで$(window).hashchange(function(event){})を
# 実装すればできます。

  • Backbone.jsを使うと、

Viewに複雑になりがちな画面イベントのハンドリングやテンプテートを寄せることができ、Modelに通信部分を寄せることができるので、役割をキレイに分離します。Modelはサーバを見ながら設計し、Viewは画面とModelの関係から設計するので、通信と画面が分離されて開発しやすくなります。

  • 実際のViewのコード
goog.provide("s3.view.login.LoginView");

goog.require("downtown.Loader");
goog.require("downtown.EventBus");
goog.require("s3.model.LoginUser");

/**
 * @constructor
 */
s3.view.login.LoginView = function(attr) {
  this.model = new s3.model.LoginUser();
  Backbone.View.call(this, attr);
  this.eventBus = new downtown.EventBus();
  if (attr.eventBus) {
    this.eventBus = attr.eventBus;
  }
};
goog.inherits(s3.view.login.LoginView, Backbone.View);

(function($, undefined) {
  /**
   * 画面の表示
   */
  s3.view.login.LoginView.prototype.render = function() {
    var self = this;
    downtown.Loader.loadHtml("s3/view/login/login", function(src) {
      var html = $($.render({}, src));
      // テンプテートからHTMLを生成し、画面に描画する。
      $(self.el).html(html);
      var userJson = localStorage["s3.login.user"];
      if (userJson) {
        self.model = new s3.model.LoginUser(JSON.parse(userJson));
      }
      // ModelとViewをバインドする
      html.link(self.model);
      self.bindEvent();
    });
  };

  /**
   * 画面のイベントをバインドする
   */
  s3.view.login.LoginView.prototype.bindEvent = function() {
    var self = this;
    // loginボタンのクリック
    $(this.el).find(".login").click(function(e) {
      if (self.clicked) {
        return;
      }
      self.clicked = true;
      self.model.accessKey = self.model.accessKey.trim();
      self.model.secretKey = self.model.secretKey.trim();
      self.model.loadBuckets({
        success : function(buckets) {
          self.eventBus.trigger("buckets_select", buckets);
          $(self.el).children().remove();
        },
        error : function(req, status) {
          alert("Login Error status:" + status);
          self.clicked = false;
        }
      });
    });
    // saveボタンのクリック
    $(this.el).find(".save").click(function(e) {
      localStorage["s3.login.user"] = JSON.stringify(self.model);
    });
  };

}(jQuery));

UI Component 「jQuery UI」

jQueryを利用するとViewをカプセル化することがことができ、Viewを部品化するのに便利でした。
jQuery UIのUI Compoentを作るのは簡単です。

goog.provide("s3.view.login");

goog.require("s3.view.login.LoginView");

(function($) {
  // $(xxx).s3login()で呼び出せるように登録する
  $.widget("ui.s3login", {
    // 外部からのパラメータ
    options : {
       eventBus:null
    },
    // 初期化ロジック
    _create : function() {
      // Backbone.jsのViewにブリッジする
      this.view = new s3.view.login.LoginView({
        el : this.element,
        eventBus:this.options.eventBus
      });
      this.view.render();
    }
  });

}(jQuery));

Viewを実装していれば、jQuery UIからBackbone.jsへディスパッチするコードを書くだけです。jQuery UIで登録しておけば、次のコードで呼び出せます。

  • UI Compoentの呼び出し
  var eventBus = new downtown.EventBus();
  $("#login").s3login({
      eventBus : eventBus
   });

Backbone.jsの作法を知らなくても、Viewを利用することができるようになります。
Viewの公開インタフェースとして、UI Compoentを利用するのは便利ではないかと思います。

まとめ

紆余曲折した結果、モジュールロード「Closure Tools」、 テンプレート エンジン「JSRender」、ViewとModelのバインディグ「JSView」、MVCフレームワーク「Backbone.js」、 UI Component「jQuery UI」、と様々なツールを使うことになりました。ツールの組み合わせで罠があるので注意が必要ですね。