俺流BackboneラーメンとPhalanxのはなし
Posted: Updated:
前置き
この記事は Frontrend Advent Calendar 2013 の7日目です。
意見表明を避けてたジャンルだけど、俺流Backbone.jsとの付き合い方と、それを反映したライブラリについて書いてみる。大半が夏前に書かれていたけど、イマイチで放置してた系を掘り起こした!
職場近くに俺流塩らーめんというお店があって、そこの熟成塩ラーメン(¥680)が、スガキヤのラーメン(¥280)に近い味してる気がする。¥400余分に払っても価値がある。
巷ではdata bindingsだとかMV*の在り方に関心が集まっている昨今、マイペースにAOP風(記述言語がないので実装はmixin...)とか、Viewの領域管理の表現に腐心していた。
今の時点ではこれがベストとは思っておらず、つまるところ Marionette.js あたりを上手に使うことに注力すれば良さそうというのが結論だ。そこに至るまでの経過が反映されたライブラリの話を通して、Backboneに対する取捨選択について私見を述べてみたい。
ahomu/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っぽくありながら、どこかで見たことがあるようなノスタルジーを感じるコードを生産できることを目的としている。サービスの高速な改修に対する、変更・修正の作業をただただ単純な肉体労働に落とし込みたいというモチベーションが強い。
- HTMLは普通にマークアップする
Layout
をスキャフォルなりコピペなりする- APIと対応した
Model
をセットする - 対象のHTMLに
region
用の識別子を与える - 共通部品の
View
に Modelなどからデータをセットしてassign
する
サーバーサイドViewで生成されたHTMLに機能を乗せていくケースでは、厚く共通化されたViewをassignするだけの薄いLayoutを作ればOKというフローが確立している。
俺流Backboneラーメン
こまかいオブジェクトメッセージングの考え方とか、想定しているアーキテクチャの話を、とりとめもなくつらつらと書き連ねる。
やっていること
- SuperView 的な役割として
Layout
をつくった Layout
は ViewController として機能するLayout
がregion
として複数の SubView を俯瞰的に管理するLayout
がModel
の操作や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の主従ニュアンスが異なる。
HTMLとJSの主従については後日、別の記事でまとめたいと思う。話がPhalanxというよりも、某CMSとか某Angularに飛び火する方面になるので。
やらないこと
Collection
を厳密にがんばらないItemView
を多数生成したり、View
とModel
の1:1をがんばらないextend
による継承モデルをがんばらない- 画面内の整合性管理・協調をがんばらない(がんばるならdata bindingを足す)
Collection
と Model
の関係をがんばりすぎると、ListView
と ItemView
の関係を気にしたり、イベントをペタペタ張り込んだりと、無闇に面倒くさい感じで分厚くなってくる。
「こまけぇこた気にしないでバーンといけばいいんだよ!バーンと!」という決して褒められることのない大雑把さによってロジックの分厚さを回避しつつ、細かい粒度に関心を向けなくてもページを管理できるようになった。
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はよりミニマルで怠惰なので万人にお勧めするものではない。
Layout
と region
がかぶったのは意図してないのだけど...。View.ui
は途中で命名パクった!!
Thorax とか Chaplin についてはあんまり見てないので言及しないです :)
おしまい
明日のFrontrend Advent Calendar 2013は、Masataka Yakura氏です!