俺流BackboneラーメンとPhalanxのはなし

前置き

この記事は Frontrend Advent Calendar 2013 の7日目です。

意見表明を避けてたジャンルだけど、俺流Backbone.jsとの付き合い方と、それを反映したライブラリについて書いてみる。大半が夏前に書かれていたけど、イマイチで放置してた系を掘り起こした!


熟成塩ラーメン


職場近くに俺流塩らーめんというお店があって、そこの熟成塩ラーメン(¥680)が、スガキヤのラーメン(¥280)に近い味してる気がする。¥400余分に払っても価値がある。

巷ではdata bindingsだとかMV*の在り方に関心が集まっている昨今、マイペースにAOP風(記述言語がないので実装はmixin...)とか、Viewの領域管理の表現に腐心していた。

今の時点ではこれがベストとは思っておらず、つまるところ Marionette.js あたりを上手に使うことに注力すれば良さそうというのが結論だ。そこに至るまでの経過が反映されたライブラリの話を通して、Backboneに対する取捨選択について私見を述べてみたい。

ahomu/Phalanx


Phalanx


Backbone.jsを素のまま使うと、自由度が高すぎてアレという最大のメリットであり、デメリットでもある雰囲気が、例外なく襲いかかる。その点、量産性が求められて、開発グループが複数存在する場面では、ある程度の指向性を与えないと、共通化基盤として機能してくれない。

そこで、ahomu/Phalanx というBackboneの薄いラッパーライブラリを書いてみたことで、それなりに満足しているのが現状だ。

本当は ahomu/Elastic として、jQueryやUnderscoreに依存しないよう書いてたのだが、大人の事情でBackboneラッパーとして生まれ変わったのがPhalanxである。

サンプルコードとか詳しくは Overview · ahomu/Phalanx Wiki を参考にしてほしい。

Motivation

  • View の使い回しとは別に、複数Viewをまたがる小さいUIを使い回したい
  • コピペで単調に View に相当するものをぺったんこぺったんこ作りたい
  • 1ページ作るたびに大量のViewインスタンスが生まれるの辛い
  • イベント → DOM → 世界のすべて(DOMにデータを置くことの割り切り)
  • 認知コスト節約のため、道具は小さい方が良い

ユーザーアクションがUIに与える影響はあんまりないという業務上の傾向に基づき、ユーザーアクションとAPIリクエストとUI更新の結びつきのシームレスさは、フォローすべき機能から外している。(やりたかったら 別でData Binding機構を入れれば良い)

Solution

  • Layout という単位で ViewController の役割を分離して、ページ固有の親Viewとページ横断の子Viewを分けた
  • Component という単位で、ItemView が担保すべきだった Model との関係を簡素化し、ユーザーアクションをトリガーとして遅延生成することでインスタンスの大量生成コストを下げた
  • プロパティとして宣言的に、Component の注入・DOMの参照保持・イベント定義などをさせることで、メソッド内のコード量を減らしてコピペ効率を上げた
  • destroy() によるイベントリスニングや Model、各種参照の処分を集約し、インスタンス管理のリスクをまとめた(オマケ)

Layout がページ広域の管理責任をもつのに対して、View は複数のLayout間で共有されうるモジュールとしての責任を持つ。そこから更にミクロなUIに役割や機能を持たせて、複数のViewで使い回すとき Component がViewに添えられる。

var AcmeLayout = Phalanx.Layout.extend({
  regions: {
    header : '#js-reg-header',
    content: '#js-reg-content',
    footer : '#js-reg-footer'
  }
});
var LikeBtnComponent = Phalanx.Component.extend({
  events: {
    'click .js-like': 'doLike'
  },
  ui: {
    count: null
  },
  doLike: function() {
    $.post('/api/like', {id: this.id});
    this.$ui.count.text(parseInt(this.$ui.count, 10) + 1);
  }
});
var ContentView = Phalanx.View.extend({
  components: {
    'likeBtn': LikeBtnComponent
  }
});
var layout  = new AcmeLayout({el: 'body'});
layout.assign('header',  new HeaderView());
layout.assign('content', new ContentView());
layout.assign('footer',  new FooterView());

Layout はViewの機能をすべて備えた上で、ネストすることもできるので画面構成部品のヒエラルキーを表現することもできるようになっている。

Background

  • フィードないしタイムライン的なカードコンテンツが流れるサービス多め
  • クライアントサイドのビジネスロジックはそうそう複雑にならない
  • ユーザーアクションがUIに与える影響が小さい(業務アプリ・ゲーム系と比べて)
  • 「MVCとは」という哲学と向き合わせず、ViewやModelを再利用したい
  • 画面内の整合性はさほど積極的に行わないでもテヘペロできる

業務背景と怠惰設計を下に、相応の割り切りを含みつつ、複雑さを廃して量産ハードルを下げることを熱心に行っている。

ここでいう量産は ノービスが書いても何となく似たようなBackboneっぽくありながら、どこかで見たことがあるようなノスタルジーを感じるコードを生産できることを目的としている。サービスの高速な改修に対する、変更・修正の作業をただただ単純な肉体労働に落とし込みたいというモチベーションが強い。

  1. HTMLは普通にマークアップする
  2. Layout をスキャフォルなりコピペなりする
  3. APIと対応した Model をセットする
  4. 対象のHTMLに region 用の識別子を与える
  5. 共通部品の View に Modelなどからデータをセットして assign する

サーバーサイドViewで生成されたHTMLに機能を乗せていくケースでは、厚く共通化されたViewをassignするだけの薄いLayoutを作ればOKというフローが確立している。

俺流Backboneラーメン


矢印大杉(写真はイメージです)


こまかいオブジェクトメッセージングの考え方とか、想定しているアーキテクチャの話を、とりとめもなくつらつらと書き連ねる。

やっていること

  • SuperView 的な役割として Layout をつくった
  • Layout は ViewController として機能する
  • Layoutregion として複数の SubView を俯瞰的に管理する
  • LayoutModel の操作や View にデータを注入する
  • Component は一意な Model と結びつく 偽ViewModelとして機能する
  • Component はDOMイベントをトリガーとして遅延生成される(省リソース)
  • Component がHTML内のどこで作用するかは、HTML側で指定させる
  • 横断的な関心事は mixin として分離する
  • components, ui, listeners などは View のプロパティで宣言する

DOMの参照とかイベントの張り込みは、プロパティとしてまとめて宣言させることで、属人性の軽減と同時にメソッド内のコード量が減るようにした。イベントハンドリングのあるべき姿を分かっていなくてもここにこうかけば動くという蜜を吸わせたかった。

var ListView = Phalanx.View.extend({
  components: {
    'moreBtn': ReadMoreBtnComponent
  },
  listeners: {
    'success moreBtn': 'renderMore'
  },
  ui {
    list: null
  }
  renderMore: function(html) {
    this.$ui.list.append(html);
  }
});

HTMLとJSが構造的な依存を抱える参照箇所については、JSがHTMLのclassをセレクタ探索するのではなく、HTMLがJS側で用意された識別子を下記のように呼び出すスタイルを積極的に採用している。処理的には同じなのだが、HTMLとJSの主従ニュアンスが異なる。

<div data-component="LikeBtn">      
   <button data-ui="btn">Like<button>
   <span data-ui="count">3</span>
</div>

HTMLとJSの主従については後日、別の記事でまとめたいと思う。話がPhalanxというよりも、某CMSとか某Angularに飛び火する方面になるので。

やらないこと

  • Collection を厳密にがんばらない
  • ItemView を多数生成したり、ViewModel の1:1をがんばらない
  • extend による継承モデルをがんばらない
  • 画面内の整合性管理・協調をがんばらない(がんばるならdata bindingを足す)

CollectionModel の関係をがんばりすぎると、ListViewItemView の関係を気にしたり、イベントをペタペタ張り込んだりと、無闇に面倒くさい感じで分厚くなってくる。

「こまけぇこた気にしないでバーンといけばいいんだよ!バーンと!」という決して褒められることのない大雑把さによってロジックの分厚さを回避しつつ、細かい粒度に関心を向けなくてもページを管理できるようになった。

Componentについて補足

一人称をもつ Model と結びついたItemView が管理するようなロジックは、遅延生成される Component で受け止めている。

ItemViewをサボった際、DOMからidを拾ってビジネスロジックに渡して…を、親View (ListView)で行うとUIの更新と絡めて責任範囲が広大でワヤになるし、Backboneの継承ベースな管理だと機能を共通化しづらいという話もあって、Componentという単位の分離を用意した。

Component が遅延生成を前提とすることからも分かるとおり、Model の更新に伴う画面内の即時協調などはオミットされている。

何らかの変更によって同じ Model を参照する複数のUIをちゃんと更新し、整合性を保とうとするならば、View に data bindings を与えて Model を共有するほうが良い。

Component という一般語すぎる命名は反省している。

Single Page Application

Backboneベースなので、もちろんSPAな構成での利用も想定している。その用途では、Phalanxをベースにしつつ、Handlebars統合を加えた ahomu/Sarissa をスターターキットとしてまとめつつあるが体裁がまだ整ってない('A`)

Backboneの薄いラッパーとしてのPhalanxに対して、Sarissaはより一定の使い方を強いる抽象化が含まれている。Backboneが極薄のライブラリである以上、最終的にはアプリケーションレイヤーにおける抽象化が必要な点は変わらないだろう。

Sarissa的な振る舞いで動いてるプロダクトはあるんだけど

まとめ

自分なりにBackboneに対して求めるモノ・作るべきモノを取捨選択した結果はこんな感じだったのだけど・・・なんかまとまらなかった。(´・ω・`)

Marionette.jsをご存じの方はお気づきかもしれないが、かのライブラリが解決したものと近いものが散見される。(見方によっては劣化版)

Viewをより良く使えるように体系化するというのがニーズであるならば Marionette.js を使ってみるのが、プロダクトの完成度からしても妥当だろう。Phalanxはよりミニマルで怠惰なので万人にお勧めするものではない。

Layoutregion がかぶったのは意図してないのだけど...。View.ui は途中で命名パクった!!
Thorax とか Chaplin についてはあんまり見てないので言及しないです :)

おしまい

明日のFrontrend Advent Calendar 2013は、Masataka Yakura氏です!