Backbone.jsで今つくっている構成について (Backbone Advent Calendar 2012 1st day)

Advent Calendar 1日目だよ

ってことで、Backbone.js Advent Calendar 1日目を書かせていただきます。本当は先日公開したFAQの日本語訳を一発目にしたかったのですが、公開したい欲に負けた次第。

何とかして現状を振り返るアウトプットの機会にしたかったので、今やっているプロジェクトで書いたコードの大まかな構成と局所的なパターンの紹介をします。

いま手元で作成しているものは、スマートフォン向けのシングルページなWebアプリケーションです。専任で大量のJavaScriptをスクラッチするの初めて。Backbone自体も割と初挑戦。

ここでいうシングルページは、HTML1枚のみで、ページ遷移はbodyの中をJavaScriptで書き換えていく構成のアプリケーションを指しています。

今のプロジェクトから抽出されたクラス構成

下記のような構成に落ち着き、抜本的なリファクタが難しいところまできたので、このままフィニッシュする予定。

  • BaseModel
    • PostModel
  • BaseView
    • PageView
    • ActionView
      • ModalView
      • PopupView
    • PartialView
      • ListView
      • (ItemView - 用意したけど出番なし)
  • BaseCollection
    • ContinuasCollection
  • BaseRouter

分かりづらいところを述べると、ListViewPartialViewのサブクラスなのは、List部分自体がPartialになりがちで、どうせ企画が進む中で外側にオマケ要素が付いたり付かなかったりするのでハナからPartial扱いで作ってしまえ、というような考えです。

ItemViewについては、ListViewに対応するのを後述するCollectionにはしといたけれど各行のModel操作はなんもなかった…というオチからお蔵入り。

大まかなクラス別の大まかな役割

Model
GETとPOSTをつつがなく行えればよく、刻一刻と状態が変化するようなModelではないのでほとんど仕事させていない。まともな仕事はPOSTリクエストのvalidateぐらい。
View
実質のControllerとして一番活躍している。イベントのディスパッチやらModelのハンドリングやらで過労気味。反面、APIからのデータ+テンプレートでHTMLを生成する部分は、別途テンプレートエンジンのロジックを多く使うことで、Viewはデータをテンプレートに投げるだけの軽い責任にしている。

伝統的MVCに照らし合わせれば、Controller = Backbone.Viewであり、View = テンプレートと考えると良い感じ。

Collection
これもModel同様、ほとんど仕事をしていない。異なるのは、リスト表示と組み合わせて使う前提で、データのページングについて仕事してるぐらい。後述するが、付随するModelはマトモに作っていない。
Router
URLルーティングと一部の初期化のみ。

スポットの構成と判断

体系的に大言を吐いてみたかったのですが、さすがに初回でそんな見極めることはままならずということで、局所的なパターンと考えについてシェアします。Backboneのコンセプト的に、まあ好きにすればという所がありますし、あくまでケーススタディです。

てきとーにやってんなぁ、という所からBackboneの柔軟さを感じ取っていただけたら幸い。

RouterでPageViewの表示を制御した

Routerで、1ルーティングにつき、1つのPageViewを基本構成としています。サーバーサイドもPageAPIという分類のAPIを備えているので、ほとんどソレに対応した構成で書けています。(大人の事情で一部パス名が異なりますが)

そんな複雑な構成ではなくRouter自体も1つしか扱っていないので、実質的にApplicationクラスとしています。その名目で、簡単な初期化やPageViewの切り替え前後の仕事もRouterの中で行っています。

リスト表示はCollectionにしてるけどModelをつくらなかった

いつかCollection的な処理が必要になる日を夢見て、リスト表示はView:ModelではなくView:Collectionになるようクラスを作成しています。ただ、Collectionに対応したModelは何も定義していないので、順当にBackbone.Modelが無駄に生成されている状態。renderするときは大雑把にcollection.toJSON()です。

// ふいんきを感じるサンプルコード
Backbone.View.extend({

  initialize: function() {
    this.collection = new ListCollectoin();
    this.collection.fetch({
      success: this.render
    });
  },
  render: function() {
    this.$el.html(this.tmpl(this.collection.toJSON()));
  }
});

生成したHTML内に各行のパラメータをdata-*として持たせておいて、単純な操作は個々にItemViewを作成しなくても親元のListViewevents: {'.js-actionClassName' : 'actionMethod'}のような一括監視して済ませています。

// ふいんきを感じるサンプルコード
actionMethod: function(evt) {
  var params = Util.parseParmas($(evt.target).attr('data-params'));
  var model  = new PostModel(); // 一瞬のPOSTリクエストのためだけに使い捨て

  // パラメータを元にリクエスト(いいね!みたいなのとか)
  model.save(params);
}

むしろ前職でよくあった要件だと、このへん活用してリッチな業務アプリケーション作れば良かったんではないかと思う今日この頃。1行1行のデータの状態が変化して、かつ保存のリクエストを小まめに投げるような仕様であれば、ちゃんとModelを使うべき

ヘッダーパーツの制御はViewのプロパティに持たせた

RouterPageViewを切り替えるときに、イベントでHeaderViewに現在のViewが変わったことを伝えて、都度更新するパターンにしました。ちゃんとイベント使うとこういうのラクで良いですね。

// ふいんきを感じるサンプルコード
var HogeView = Backbone.View.extend({
  header: {
    title: 'ホゲビュー'
  }
});
new (Backbone.Router.extend({
  routes: {
    'hoge': 'gotoHoge'
  },
  initialize: function() {
    this.headerView = new HeaderView();
    this.on('change:currentView', this.headerView.render);
  },
  gotoHoge: function() {
    this.currentView = new HogeView();
    this.trigger('change:currentView', this.currentView.header);
  }
});

エラー用のハンドリングは、エラー表示を伴うため、すべてViewに持たせた

よくよく考えたら全部Viewにエラーハンドリングさせりゃ良いじゃん、ってことでthisをView自身にするべくViewのメソッドに、各Modelのエラーをlistenさせています。PostModelには、別途ソレ用のエラーハンドリングを持たせたり。

// ふいんきを感じるサンプルコード
Backbone.View.extend({

  initialize: function() {
    this.model.on('error', this.onModelError);
  },
  doLikeAction: function() {
    var like = new Like();
    like.on('error', this.onPostModelError);
    like.save();
  }
});

ここでいうonModelErroronPostModelErrorは、各Viewで独自の実装はせずに基底クラスにあたるBaseViewのレイヤーで共通化されています。

BaseModelとPostModelを分けた

結構悩みましたが、結局BaseModelのサブクラスとして、PostModelを別途作成して、Create, Update, Deleteの操作はPostModelを継承したクラスのみで行っています。通常のModelはReadのみ行います。

というのもAPI構成(アプリ特性的にも)、GETしかないリソースあるいは、POST(& PUT, DELETE)しかないリソースが多く、CRUDすべて備えたリソースに至ってはひとつもありません。

そんな事情もあって、いさぎよく基底クラスからして分類して疎遠にしておいた次第。ここでのModelはリソースを表現するというより、APIへのリクエスト方法や、求められるバリデーションを知っているものという位置づけです。

どうでもいいけどJSDocしづらい...

Backbone.jsがdoccoで、アノテーションコードを生成しているので、JSDocにあたるものがありません。@extends Backbone.Viewとか書きたいのに…。

/**
 * @class BaseModel
 * @extends Backbone.Model
 */
// Inheritance
var BaseModel = function(attributes, options) {
  options || (options = {});
  Backbone.Model.apply(this, [attributes, options]);
};

// mixin base methods
_.extend(BaseModel.prototype, Backbone.Model.prototype, {
  /**
   * @property {String} ownName
   */
  ownName: '__UNKNOWN__',
...略

doccoのアノテーションコードも好きなんですが、extendsとかmixinさせるつもりのライブラリなら順当にそのへんの整備もオフィシャルに提供していて欲しい気がしました。

Underscore.js互換ライブラリlodashのこと

A drop-in replacement* for Underscore.js, from the devs behind jsPerf.com, delivering performance, bug fixes, and additional features. bestiejs/lodash

bestiejs (BestieJS) が提供している lodashというライブラリがあります。これは、Underscore.jsの代わりになるべく互換性の確保以外に、Underscore.jsにある問題の解決パフォーマンス向上機能追加が行われてるライブラリです。jsPerfの関係者とかが絡んでいるので、パフォーマンスをウリにしている面は信頼できるんじゃないでしょうか。

AMD対応や豊富なビルドオプションが用意されていたり、個人的には_.cloneにdeep cloneオプションが追加されているところなども好き。コミットも尋常じゃないペースで続いています。

プラグイン・参考など

使ったプラグイン。唯一Validation周りは再実装の意味なさそうだったので、さくっと採用。

使わなかったプラグイン。Viewの制御の枠組みを入れておいたほうが良いかと思ったが、スマートフォン向けだと、あまり複雑にViewをレイアウトしたりリージョン組んだりする必要もなかったので全部スルー。

参考にしたラッパー構成。言うほど参考に出来ていないが、よくよく読み込んで抽出したパターンからオレオレBackboneベーススターターを年度内には用意するつもり。

おわり!

いきなりオチが付かない系で申し訳ないのですが、 以後のAdvent Calendar みなさんよろしくお願いします。

小綺麗なアウトプットは、GitHubで共有されるようなプロジェクトが示してくれるので、現場的というか局所的なケーススタディが示されていくと個人的には面白いな〜とか。

あ、まあ気にせずなんかもう何でも良いのでどしどしネタなげてください。✧◝(⁰▿⁰)◜✧

↑今の参加状況だと1人何日分書けば良いのか...という皮算用に震えてる:;(∩´﹏`∩);: そのときは、ここで紹介しきらなかった局所パターンを小出しにする予定・・。