Marionette.jsのCompositeViewって、どこで使えばいいの?問題
ビール飲みながらダラダラ書いてるので、おかしな所は申し訳ない。
CompositeView
marionette.jsには、CompositeViewというクラスがある。
CollectionViewを継承していて、
- 再起的な構造を持ったviewを作れる
- 親側もtemplateを持つ事が出来る
というのが、特徴とかなんとかで、イマイチ使い道のimageが湧かなかった。
普段書くときも、ItemViewとCollectionViewで事足りていた。
が、最近、使い道がやっと分かってきた。
View親子に関係のある親戚のおじさんView
あるViewに親子階層がある時であれば、ItemViewとCollectionViewを紐づければ良い。(ulタグとliタグのイメージ)
これは、このブログでも何度も書いてきた、よく書くパターン。
ところが、この親子に働きかけたい別のView要素が必要になる時がある。
※以降、このViewの事を、その親子の「親戚のおじさんView」と呼ぶ事とする。とりあえず。
シチュエーション
親戚のおじさんViewとは、具体的に言うと、例えば、
- 検索ボックス
- タブ
等々。
これらは、親子の階層を持った検索結果のようなViewに対して、そのデータを渡したり、表示を切り替えたりする必要がある。
普通に書くと、event treiggerするか、subview構成にするかで、親子と連携していかないといけない。
結構、手間だし、複雑になって可読性が落ちる恐れが有る。
こういう時に、CompositeViewを使うと良い。
こうすると、今迄、親子と別の家に居た親戚のおじさんが、その親子の家にやってきて、2世代家族になるので、
わざわざ電話かけなくても、そのまま話が出来る!
みかんとかも、そのまま渡せる!
※よくわかんなくなってきた
CompositeViewの効果
親子との連携eventとか、無駄に複雑なsubView構成みたいなのを書かなくて済む。
書いてたら、fatになるんじゃないか?と思われるかもだが、
そんな事も無かった。
そもそも、元々、親子 + 親戚構成だと、親がめっちゃsimpleで親戚にlogicが寄るので、
本当に、連携部分だけ楽出来てる感じ。
この連携部分だけといっても、馬鹿にならない。
自力でこの連携書くと、そこを起因として、また数々のオレオレ実装が出来るので、
やっぱり、これも、汎用的な物を使った方が良い。
まとめ
- 親子があったら
- CollectionView
- 親子の近くに親戚のおじさんがいたら
- CompositeView
ie8で画面が真っ白になった
ie8で真っ白画面
とある機能を追加改修してie8で確認したら、
ブラウザの画面が真っ白になった。
特に、エラー等も出力されなかったので、
何が起こったかさっぱり分からなかった
条件
幾つか、付けたり外したりしていくうちに、以下の条件で起きているっぽい。
※他に、条件があるかもしれないが。
もし、画面真っ白になったら、cssのoverflow周りから試すと良いかも。
※この前、ie8がブラウザクラッシュしたのは、overflow周り+jsに起因してたし、この辺りがbuggyかも
次に誰か地雷をふむかもしれない人の為に残しておきます。
grunt bower yeoman入門記事
※これは、社内の勉強会の資料の下書きです。
ここ数年で、js環境が整理されてきた。
js開発するなら、これらの知識は必須。無いとめっちゃ不便。
という事で、今日は、gruntとbowerとyeomanの話。
各ツール概説
- Yeoman
- applicationのひな形を作る
- アプリ構築に必要な様々な作業を任せられる
- Grunt
- build / preview / testに使う
- Bower
- 依存性の管理を行う
- 手動でDLやscript管理する必要無
rails開発者のために
ぶっちゃけ、どれも、railsの環境にそっくり。rails知ってる人は、要するに、以下の事。
- grunt => rails server実行時に勝手にやってくれる作業を切り離してカスタマイズできるようにしたもの(coffeeのコンパイルとか)
- bower => ruby gem管理をjsのclient側で実現した物。
- yeoman => rails generator/scaffold 等と発想は一緒。
逆に言うと、rails使ってる人は、あまり使う機会は無い。必要ない。(railsが全部やってくれるし)
(但し、bowerは、rails開発界隈でもjs管理ツールとして使われ始めてるので、要check!)
gruntとは
jsのtask runner
なんのために使うのか?
- jsのミニファイ化
- コンパイル
- UT
- jsLint
install
npm install -g grunt-cli
- クライアントがこれで入る(imageは、rvmと同じ。このクライアントで、複数のversionのgruntを入れる事ができる)
設定
- package.json
- Gruntfile
に、設定を書く。
package.json
package.json自体は、gruntでなく、node.jsのライブラリ管理npmの話。version管理/依存性管理を行う設定file。(json形式)
ライブラリを追加したい時は、
npm install ****** --save
すると、パッケージをinstallすると同時にpackage.jsonも追記する。これを一通り作った後、
npm install
すると、指定通りのライブラリをinstallできる ※rails3からのbundle installと同じ発想。
※installしたライブラリは、そこの配下のnode_modulesに入る
Gruntfile
gruntの設定ファイル。以下を書く。
- ラッパー関数
- ①project / task設定
- ②Grunt pluginやtaskのロード
- ③カスタムtask
例
// ラッパー関数 module.exports = function(grunt) { // ①Project configuration. grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), uglify: { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' }, build: { src: 'src/<%= pkg.name %>.js', dest: 'build/<%= pkg.name %>.min.js' } } }); // ②Load the plugin that provides the "uglify" task. grunt.loadNpmTasks('grunt-contrib-uglify'); // ③Default task(s). grunt.registerTask('default', ['uglify']); };
ラッパー関数
Gruntのコードは以下の関数内に書く
module.exports = function(grunt) { // Do grunt-related things in here };
①projectとtask設定
grunt.initConfig
メソッドを通過したオブジェクトの中で設定を定義<% %>
でプロパティ参照書ける。
②loading Grunt plugin
- package.jsonに定義
- npm install
- Gruntfile内で
grunt.loadNpmTasks('grunt-contrib-uglify');
で、そのコンポーネントが使える
③カスタムタスク
- 設定したタスクをdefaultタスクにするには、
grunt.registerTask('default', ['uglify']);
これで、 grunt
で動かせる
grunt uglify
でもgrunt default
でも動く。
pluginで提供していないタスク
以下のように関数内で定義する
grunt.registerTask('default', 'Log some stuff.', function() { grunt.log.write('Logging some stuff...').ok(); });
plugin
coffeeのコンパイル等、めっちゃいっぱいプラグインがある。
自分でgrunt作る
grunt-init使う。
npm install grunt-init
templateが幾つか準備されているので、選んで、installして、grunt-init
する
- 例:commonjs
- https://github.com/gruntjs/grunt-init-commonjs
- こんなのが幾つか準備されている。
流れ
自分で作る場合。
grunt-init
で初期設定(ちなみに、npm単体の時は、npm init
)npm install ***** --save-dev
でライブラリを追加- gruntfile内で、loadNpmTasksでロード。initConfigで設定。registerTaskでタスク登録。
こんな感じ。
参考資料
和訳公式Page
jscafeでの須郷さんの資料
bower
Twitter社製の、フロントエンド用のパッケージ管理ライブラリ。
Git経由で行われているらしい。
install
- 前提条件
- node & npm
- git
npm install -g bower
使い方
- install
bower install <package>#<version>
- 例:
bower install jquery#1.9.1
- 基本的に、bower_componentsに指定したライブラリが格納される。
install済みのpackage一覧
bower list
bowerに登録されたpackageを探す
bower search <name>
- ホームページを見てもよい。
- ちなみに、欲しい物が無かったら、
bower install url
でinstallできる
設定
.bowerrc
- JSONを使って設定できる
入力元ファイル(bower.json)や出力先directory(bower_components)を変更する時に書く
例
{ "directory": "public/components", "json": "bower.json" }
bower.json
上述のcomponent.jsonのクライアント版。依存してるライブラリを保存しておける仕組み。
bower init
で対話形式で作れる。
- 後でライブラリをjsonに追加
bower install <package> --save
dependenciesとdevDependenciesの違い
- dependencies ; prosuction用
- devDependencied : 開発用
さっきのオプションの--save
か--save-dev
かで違う
流れ
bower init
でテンプレート作成bower install <package> --save
でライブラリ追加
こんな感じ。
参考
yeomanとは
web開発を楽にするためのベストプラクティス/ワークフローを詰めたツール環境の事。
install
npm install -g yo
これでyeoman本体は入った。
個別のパッケージの例
例えば、generator-webappを入れて使うまで
install
npm install -g generator-backbone
これで、generatorが手に入った。
後は、任意のディレクトリ上で、
yo backbone
と打つだけ。
後は、聞かれた事を答えて行けば終わり。
※このyeomanの行ってくれる作業の中で、今迄書いてきたgruntやbowerの設定fileも作ってinstall等もしてくれる(勿論、どのgeneratorを使うかで内容は全く違うが)
使い方
generatorによって違うのだが、例えば、Backboneの場合、
幾つかソースを作るgeneratorが準備されている。
yo backbone:model blog yo backbone:collection blog yo backbone:router blog yo backbone:view blog
これらを使って実装していき、最後にサーバーを起動する
grunt server
大体、他もこんな感じ。
※他に、grunt test
でtestするとか、grunt
でビルド等。
参考
yeoman製のbackboneのgenerator
コレ使う。
メリットは?
install
勿論、yeomanのinstallが前提。
npm install -g generator-backbone
project作成
- dir作成
$ npm install -g backbone $ mkdir my-project $ cd my-project
- yeoman実行
yo backbone
この時、以下の通り、template指定も出来る(デフォルトが確か何も入ってなかったから、以下、オススメ)
$ yo backbone --template-framework=handlebars
使い方
- generator
$ yo backbone:model blog $ yo backbone:view blog $ yo backbone:router blog
こんな感じで、railsみたいに、ひな形となるコンポーネントを追加して、編集して作って行く。
サンプルソース
さっそくなので、前回のBackbone.Viewの内容を書いてみる。
mainにrouterを追加
- app/scripts/main.js
require([ 'backbone', 'routes/blog' ], function (Backbone, BlogRouter) { new BlogRouter(); Backbone.history.start(); });
routerを追加
- app/scripts/routes/blog.js
/*global define*/ define([ 'jquery', 'backbone', 'models/blog', 'views/blog' ], function ($, Backbone, Blog, BlogView) { 'use strict'; var BlogRouter = Backbone.Router.extend({ routes: { "": "index" }, index: function() { var blog = new Blog({name: "yamada"}); var view = new BlogView({model: blog}); $('body').html(view.render().$el); } }); return BlogRouter; });
blogViewを変更
- app/scripts/views/blog.js
define([ 'jquery', 'underscore', 'backbone', 'templates' ], function ($, _, Backbone, JST) { 'use strict'; var BlogView = Backbone.View.extend({ template: JST['app/scripts/templates/blog.hbs'], render: function(){ this.$el.html(this.template(this.model.toJSON())); return this; } }); return BlogView; });
- app/scripts/templates/blog.hbs
name is {{name}} !
実行
grunt server
localhost:9000かな?に出てるはず。なんかが。
注意
vagrantから:
$ vi Gruntfile.js :s/localhost/0.0.0.0/gc =>全部 y :wq =>保存して終了 $ grunt server --force
おしまい
今日やった事をおさらい
- grunt = task runner
- testやcoffee コンパイル等の作業の自動化
- bower = client側のライブラリ管理の仕組み
- bower install していけば、一々、自分でDLする必要はなくなった
- yeoman = generator
- 上記の設定ファイルや、install作業や、コンポーネントのgenerator等として使える。
yeomanのgeneratorが今、いっぱい作られているので、やりたい事があったら、まずは、yeomanのプロジェクトに無いかチェックする所から始めると色々楽ができる。
Backbone.View入門
(この資料は、社内のBackbone入門の勉強会の為に作った物の下書きです。)
これから、Backbone.Viewについての入門記事を書きます。
内容としては、Backboneを初めて学ぶ人の為の噛み砕いた入門記事にするつもりです。
Backbone.Viewの使い方
基本的な使い方は、ModelやCollectionと一緒です。流れとしては、Backboneのコンポーネントをextendした物を宣言して、必要な時にnewしていくという形になります。
var PostView = Backbone.View.extend({ // your source }); var postView = new PostView
Backbone.Viewの役割
Backbone.Viewの役割は、まとめると以下の通りです。
- 役割
- htmlを描写する
- DOMと関数を紐づける
- Model/Collectionと紐づける
これらについて、それぞれ1つずつ見ていきたいと思います。
役割1:htmlを描写する
htmlを描写するには、render関数を使います。
render
ここに、どんなソースを書いても良いのですが、一般的には、templateを準備してこれを描写するという事がよく行われます。例を見てみましょう。
- 元になるhtml
<div id="main"></div>
- Viewクラス
define([ 'jquery', 'underscore', 'backbone', 'templates' ], function ($, _, Backbone, JST) { 'use strict'; var BlogView = Backbone.View.extend({ template: JST['app/scripts/templates/blog.hbs'], el: "#main", render: function(){ this.$el.html(this.template(this.model.toJSON())); return this; } }); return BlogView; });
- templateファイル
name is {{name}} !
※これは、yeomanのBackbone generatorで作ったソースの一部です。全部試したい方は、gistに書いたのでご参考に。
他の言語やフレームワークを触った事がある人は、このソースを見たら大体想像が付くと思いますが、一応、簡単に説明をしたいと思います。(なんとなく分かる方は読み飛ばして下さい)
- 別fileとして存在する静的なtemplateを準備しておきます。これは、呼ばれる迄画面に出る事はありません。
- この定義があった後で、Backbone.Viewのrender()の中からこのtemplateの呼び出しが行われます。
- この時、値を受け渡す事が出来て、これをキーに紐づけると、templateの中の同名のキーの中にその値が出力されます。
- ※よくBackboneではModelのtoJSONやattributesを渡して、そのキーをtemplateで書いておき、その値を表示させる事をします。
- 最後に、実際のhtmlとして出力します。
template
ここでは、handlerbars.jsを使いました。
templateは、多くの種類から自由に選ぶ事が出来ます。今日、よく聞く選択肢として以下の物があるでしょう。
- ejs
- handlebars
- mustache
以下の比較記事を見ると良いかと。
el
BackboneのViewで独特でやっかいなのが、elという要素です。
これは、このViewが紐づくDOM要素のセレクタを指定するためのプロパティです。
Viewがnewされる際に、$el
に、elに指定したセレクタを元に取得した要素を格納します。
この後、説明していくEventや、View内の$等は、全部、$elに対して実施する操作になります。
elの指定のない場合
elは指定しない事もできます。その時、$el
に、tagNameやidの指定値を元にDOM要素を作り出して格納します。もし、tagNameの指定が無ければ、勝手にdivタグの要素を作成します。
これは、実際のhtmlと結びついていない状態になりますので、どこかで、この紐付きを書いてあげる必要があります。
- elの指定のないView
define([ 'jquery', 'underscore', 'backbone', 'templates' ], function ($, _, Backbone, JST) { 'use strict'; var BlogView = Backbone.View.extend({ template: JST['app/scripts/templates/blog.hbs'], render: function(){ this.$el.html(this.template(this.model.toJSON())); return this; } }); return BlogView; });
- viewを呼び出す所
var blog = new Blog({name: "yamada"}); var view = new BlogView({model: blog}); $('body').html(view.render().$el);
View自体は、elが無いだけで、特に変化はありません。 Viewを呼び出す所を見てみると、render()の後に、$elが付いています。 これで、このelの指定が無いViewで作成されたDOM要素を取り出してます。
return thisについて
render関数は、慣例として、最後にreturn thisを書かないといけません。これは、何故かというと、別のViewからel要素を取り出したい時があるためです。 丁度、上のel指定のないViewソースがそのシチュエーションに該当します。renderがthisを返しているから、render().$elしてDOM要素が取れるのです。
役割2:DOMとEventを紐づける
次にDOMとEventの紐付きについて見ていきましょう。
普段、jQueryを書いていると、長いjavascriptファイルの中で、 どこでDOMとEventと関数の紐付きを書いたのか分からなくなる事はありませんか? Backbone.Viewは、こういった混乱をおさえるために、DOMとEventと関数の紐付きを書く所を決めています。 この決まった所に書く事で、どこのDOMで、どんなイベントに、どのような処理を任せたいのかが明確になります。これも例を見てみましょう。
var BlogView = Backbone.View.extend({ events: { "click #btn" : "onClickButton" }, onClickButton: function(){ console.log('click!!!'); } });
Eventsの中にplainなjsのオブジェクトを書きます。
キーは、空白を挟んで2つの要素(この例ではclick
と#btn
)があり、
それぞれ、イベントとDOMセレクタを記載します。
値としては、このイベントで実行したい関数名を書きます。
どうでしょう?とても、直感的なルールではないでしょうか?
thisについて
勘の良い人は、ここで呼ばれた関数のthisコンテキストはどうなるのか、気になる人がいらっしゃるかもしれません。 以前のversionのBackboneでは、ここで呼ばれる関数のthisを自分でbindする必要がありました。が、最近のBackboneは、このbind処理が本体に入っているので、特に意識する必要はありません。
※ただし、events内の指定以外の所で、callbackから呼び出す関数等のbindingは自分で書く必要がありますので、注意して下さい。
うまくイベントがひっかからない場合
初めてViewを書く時によくあるのですが、Event書いたのにうまく実行されない事があります。その時は、以下をチェックしましょう。
- elが存在するのか?
- eventで指定した要素がelの配下に存在するのか?
$elに対して、イベントのbindをしているので、elが実際のhtmlに存在しなかったり、elの中に無い要素だとうまくいきません。
役割3:Model/Collectionと紐づける
BackboneのViewはModelを監視して、状態変化に伴い処理を実行します。これを書くために、Viewには、ModelやCollection用のプロパティが準備されています。 実際の使われ方としては、以下の様な流れになる事が多いでしょう。
- Viewをnewする時に、model(collection)を指定
- View内のinitializeで、modelに対してeventをbinding
- 何かしらのmodelの状態変化があれば、上記のbindingを辿って関数を実行
この時、最後の関数へのthisのbindingは自分で書く必要があるので、忘れないようにしましょう。
var PostView = Backbone.View.extend({ initialize : function() { this.listenTo(this.model, 'change', this.onChangeModel) }, onChangeModel : function() { console.log('has changed!'); } }); post = new Backbone.Model({name: "first"}); post.set('name', 'seconds');
データバインディング
さて、modelやcollectionとの紐付けについて、基本的な話から少し脱線したいと思います。 最近、Angularjsの勢いが激しく、非常に活発になってきました。 AngularjsとBackboneの違いでよく出てくるのが、データバインディングの有無です。Backbone本体にはデータバインディングが無いので、一見劣っているように見えますが、実は、プラグインを入れると簡単に使う事が出来ます。 実際に実装する時に、データバインディングがあるかないかでソースの量が大分違うので、簡単に見てみましょう。
stickit
NYTimes社のpluginです。 使い方がとてもsimpleで良いので、見てみましょう。
http://lxyuma.hatenablog.com/entry/2013/11/07/233930
※ここは後で書く。
実装パターン
Viewを書く上で非常に頻繁に出て来るシチュエーションが、親子階層を伴うViewです。最後に、このパターンについて見てみましょう。
親子View
Backboneで開発していると、どうしても、親子の階層を持つViewを書く必要が出てきます。ここでいう「親子の階層を持つView」とは、例えば、ulタグが親のViewでliタグが子供のViewであるような階層を持ったViewの事です。さて、何故、このViewを親子の階層に分ける必要があるのでしょうか?
もし、1つのViewでこの親子階層を表現しようとすると、毎回、どのViewがどのModelを指すのかの紐付け処理を書く必要がでてきます。最悪、htmlの属性要素にカスタム属性を入れなくてはならず、せっかくjavascriptのframeworkを使っているのにスマートな形ではありません。そこで、親と子供のそれぞれをViewとして分離して、1つの子供Viewのインスタンスが、elとmodelを紐づけて持っておき、Eventに対応した処理もそのViewの中でやってしまう事で非常にsimpleでスマートな実装にする事ができるのです。
さて、実際これをソースで書くと結構面倒くさい事になります。親から見て子供の追加があったら関数A、削除があったら関数B、等々毎回同じソースを書く事になり、また、書き方も沢山あるので、場合によっては非常に複雑なソースが出来てしまいます。
そこで、MarionetteのCollectionViewとItemViewが使えます。
※ここも後で書く。
最後に
一通り、Viewがどんなものか分かった所で、是非、ソースを読んでみると良いと思います。 Backbone.Viewが恐ろしくsimpleで、非常にソース行数が少ない事に驚くと思います。 細かな動き等は、適宜Backbone.Viewを見て確かめる事をお勧めします。その方が早いです。
そうです。Backboneが非常にsimple/compactだからこそ、出来る事です。困ったらソースをgrepしてしまいましょう。
普通のrailsアプリのBackboneにMarionette付けて思った事
以前書いた記事の反省を元にMarionetteに移行した。
思った以上に快適!
大規模になったらMarionette.js使えとか書いているのは嘘で、普通にBackbone使うときは、初めから使うべき。
Backboneで一番恐ろしいのは、各現場/各開発者毎に異なるオレオレ実装。オレオレ実装作るコストに加え、使う人の思わぬバグや学習コストやスイッチングコスト等諸々考えると、特別な理由がない限りMarionetteみたいな既存のframework使うべき。
あんど。データバインディングを提供してくれる、stickitと一緒に使うと、より一層効果的。
めっちゃ、ソースコードの量が減って、ソースの意図が明快になった。悩みも少ない。工数も勿論減る。
ここから、幾つか思った事を、サッカー見ながらお酒飲みながら、ダラダラ書く。※ちなみに、日本vsオランダ戦見てる。
railsアプリでのjs周辺の作りの推移
今作ってるrailsアプリのjs周辺の作りを振り返ると、以下の流れで推移してきた。
ステップ1)jQuery => jQuery + coffee script
coffee script導入でclassが気軽に使えて、初期化処理と共通処理の記述が明快になった。
が、結局、画面毎に分かれたfile内で自由にjsで動き回り、非常に分かり辛い共通化されたextend元のjsがあり、これはこれで非常にスパゲッティなソースを多々生んだ。この辺りの問題は昔のblogで書いた通り。
ステップ2)ステップ1の状態 => jQuery + coffee + Backbone
これも劇的な変化をもたらした。Backboneでjsをmodularに書く事で、今迄書いてきたソースがどれだけ罪深かったかと思った。もう、元のjQueryダラダラソースには戻れない。
だが、Backbone自体は、非常にsimpleで、色々と構成が悩ましく、ちゃんと仕組み策定やルール化をしないと、数々のオレオレ実装を生んだ。
ステップ1と比べると遥かにマシなのだが、まだまだ多くの課題があった。
この生Backboneに絶望して、他のFWに移った方の気持ちは分かります。生Backboneだけで実際の現場ソース書くのは大変で現実的でない。ただ、次のplugin入れたら大分変わった。
ステップ3)ステップ2の状態 => jQuery + coffee + Backbone + Marionette + StickIt
今がコレ。大分この構成で落ち着いた。バック開発でrails書いてるのと同じ様な感覚で書ける。
基本的な作りがMarionetteに埋め込まれているので、ソースの量とテストの量も減った。
データバインディングもstickitでバッチリ機能してくれる。
とりあえず、これで、そんなに大きな問題は無くなった。この構成での、思った事をかいていく。
どのViewを使うべきか?
生Backboneだけの時は、Viewは1つしか無かったので、それを使えばよいのだが、 Marionette使うと、Viewの選択肢が一気に増えるので、どれを使うべきか考えないといけない。
- 選択肢
- CollectionView / CompositeViewなのか?
- ItemViewなのか?
- Marionette.Viewなのか?
- Backbone.Viewなのか?
例えば、ulタグ + liタグのような親子階層があれば、MarionetteのCollectionViewとItemView使えばいい。
用途がはっきりしてて、Marionetteの想定の範囲内で、ここに悩みは無い。
Viewで悩む時
それ以外の、例えば、erb(railsのview template。jspみたいな物)で既にhtmlは描写している物に対して、幾つかEvent等追加したい時に、どのView使うべきか悩む事になる。
このシチュエーションは、single page appliっぽい作りだとあまり無いのだろうが、普通のアプリに適用する時は頻繁に起こる。
初めは、Backbone.View使えばいいと思ってた。generator-marionetteもBackbone.View出すしね。 template要らないからItemView要らないし、Marionette.Viewは単体で使うメリットが無いように思えた。
Marionette.View!
ところが、色々書いてみて、結局、Marionette.Viewを頻繁に使うようになった。
何が良いかというと、最大の利点は、ui
プロパティ。
このViewで頻繁に使う要素のselectorを定数みたいなimageで登録しておけば、後で、jqueryオブジェクトとして使える仕組みで、本当にちょっとした機能なのだが。
これ書くと大分スッキリする!動きも分かり易い。何か変更ある時も気軽にまとめて変える事が出来る点とても良い。
但し、このまま使うと、this.uiを要素として使えないので、initializeの中に、 bindUIElements
書くのを忘れないように。
※確か、itemViewはtemplateが伴わないとerrが出てたので、ここでの例の用途に合わなかった
composite view
ちなみに、結局、compositeVIewを書く機会はあまり無かった。殆ど、CollectionView + ItemViewで済む。
追記12/22:そう思ってたんだけど、最近ようやくcompositeViewの意義が分かった
http://lxyuma.hatenablog.com/entry/2013/12/03/230652
普通のアプリの初期処理をどこに書くか問題。
初期処理をどこに書くか、結構悩んだ。
これもSinglePageAppliなら、初めのpage決まってるだろうし、そのままApplicationにaddInitializerすれば良い。
普通のアプリと初期化処理
が、普通のアプリで、色んな所から呼び出すのに、globalな初期化書くのはおかしい。この書く場所が個別の画面毎に分かれていてもやっぱり、おかしい。(Applicationの初期化じゃないからね。)
それで、結局、Marionette.AppRouterから呼び出されるController内の関数毎に初期処理を分けた。 まあ、複数の画面毎にやる初期処理全然違うし、もし、railsと同じ感覚なら、controllerのaction毎に処理を分けるのは自然の事だし、これで良い気がする。
※ちなみに、全ての画面に共通するアプリケーション自体の本当の初期化はApplication.setInitializerに入れて良いと思う。ここで話しているのは、個別の初期処理の話。
erbとtemplateどっちが良いか問題
Backbone書くようになって、erb離れてtemplateに生html(+軽いlogic)を頻繁に書くようになった。
初めは、railsのviewのhelperが強力なので、極力erb使って、template書かないように、と考えていた。
生html
ところが、生html書くのが思った以上に、(railsのviewと比較すると)快適。
何を言っているかというと、railsのviewのhelperって、メソッド名やオプションの細かい仕様が独特で、頭に入らず、いつもググってる自分がいて、
これが、生htmlになると、殆どそのままガリガリ書けるので、生産性が高い。
また、出来上がった物も、確かに、railsと比べると冗長さはあるのだが、明快で変な動きをする要素が無い。
なので、生html化していくのも悪くは無い。
※完璧にrailsのviewのhelper暗記してたらそれがいいかもだが
特に親子階層があって、通信&イベントが発生する所は、初めからMarionettteのCollectionとItemViewを使わないと、最終的に大変になるので、初めからバラしてしまうべき。
Viewと紐づけるセレクタはView内のelに書かず、regionから持って来る。
上のerbの話の続きになるが、全体の見取り図となるviewは、erbに書いておくべき。最初に書いておけば、初期処理の段階で、region書いてしまえる。
Viewクラス内には、el書かず、必ずregionから持って来るようにすると、 region見れば、全部のViewを、見渡せるので、概要掴み易いし、作業もし易くなる。
コンポーネント間の連携event
生Backboneを書いてる時、どうしても連携の都合で、1Viewに複数のModelを持ってて、複雑なbindingを書いたりしてしまった。
これは、罪深い事で、間違っていたと思う。
非常に複雑になってしまった。こういうのがオレオレ実装。
1Viewは関連する1つのリソースだけ、それとbindするべき。Marionetteのイベントもthis.model/this.collectionしか書けない。この仕組みに従うと綺麗になる。
イベント連携の悩み
そうすると、今度は、イベント連携どうするか、悩む。 この解決策は多分複数あるが、書いてて行き着いたのは、
- viewのイベントであればMarionetteのtriggers
- それ以外は、Applicationのイベント(reqres/cmd含む)。
そうして、以下の様なイメージで動かした。
- それぞれのViewはそれぞれのViewに関係する事しか行わない前提。
- 関係ない事は、trigger or Applicationのイベントを発火!
- このApplicationのイベントは、担当しているControllerのAction内でbindしている。
- 大抵、そこから、他のViewに働きかける。
こうすると、とにかく、Controller見れば、大体の全体の流れが掴める!
うん。railsと一緒だ。
Controllerあれば、一々、親となるApplicationViewみたいなの作る必要もない。
なんか、めっちゃスッキリした。
ここは、異論でそう。
stickit
前にも書いたけど、stickitが非常に良い。
これも、フォームと通信を伴うような、普通のアプリであれば、目を瞑って初めから導入してしまうべき。
ただ、注意すべきは、動きが見えなくなる点。やっぱり、想定外の動きをする事がある。
有り難くない2way binding
stickitは2way bindingを適用してくれるのだが、View => modelはとても有り難いのだが、 model => Viewの更新が、非同期でsave()してる時に邪魔になる事がよくある。
※編集中の項目が元に戻ったりとかね。作り次第の話なのだが。
こういうシチュエーションは、updateView: falseばんばん設定して1wayにするようにしてる。
テキストフィールド以外の動きが怪しい
テキストフィールドの動きはばっちり同期するのだが、これが、radioやcheck_box等、
他のinputだと、おかしな動きをする事がある。
これも、stickitの方でデフォルトで準備してくれてるハズなのだが、
chromeならOKだけど、FFでうまく値取ってくれてない時があったりした。
これは、もう仕方ないので、getVal()内で調整コード書いた。
※これは、いつかpull req出すかも。気が向いたら。
なんか困ったらgetVal上書きしよう。
終わり
また、書いてみたら長くなってしまった。
Marionette + sitckitで一通りの事は苦も無く使えるようになった。
動きが分からない所はソースをgrepすれば解決してしまう。
他の複雑な動きをするframeworkと比べるとこういう気軽な所が良い。
事例も沢山、困った時にググると情報も沢山ある。
他のframework色々流行ってますが、まだまだBackboneも使えそうです。
ちなみに、日本とオランダは2-2で、引き分け!惜しい!
Backboneでデータバインディング(stickit)
最近、angularが熱くて、twitter見てると、angular周りが活発に流れて来るのだが、
angularのウリにしてるデータバインディング、Backboneでも同じ様な事がpluginで出来る。
Backbone自体は、非常にミニマルに出来ているので、(おそらく今後も本体にこういう機能入らなそう...)
こういう機能は自分で追加しないといけないのだが、pluginさえ入れれば動く。
幾つか類似pluginあるようだが、ここでは、stickitの事を書く。
stickitについて
https://github.com/NYTimes/backbone.stickit
NYTimes社のもの。
使い方
使い方は非常にsimpleでViewの中に
- セレクタと属性の紐付けを書く
- 対象となる要素がある状態で、stickit()と書く
これだけ。
公式ページのsampleを実際にやってみる。
step1:セレクタと属性の紐付けを書く
Viewの中で、bindingsを準備する。その中に、
html要素のセレクタ : 紐づけたいthis.model内の属性名
をobject形式で記述する。
bindings: { '#title': 'title', '#author': 'authorName' }
この例だと、1行目は、<input type="text" id="title"/>
みたいなhtml要素に対して
View内のthis.modelのtitleプロパティと紐づけようとしている。
step2:stickit()
この後、render()等、上記bindingsで指定したhtml要素が存在しているタイミングで、
this.stickit()
と書くだけ。
例。
render: function() { this.$el.html('<div id="title"/> <input id="author" type="text">'); this.stickit(); }
これだけ。
めっちゃ簡単。
angularのデータバインディング
http://docs.angularjs.org/guide/databinding
上記angularのページで説明しているような two way bindingが、
stick itでも上記の記述だけでデフォルトで出来る。
また、bindingsの中に、色々な細かい設定を書く事が出来て、
そこで、updateModel/updateViewをfalseとか書くと、 one way bindingも出来る。
他、同期のタイミングでのデータ加工等、色々な設定が出来る。
実際使ってみて
Backbone書いてると、同じ様な#title要素から@model.title詰めて、
@model.titleの内容を#title要素に書いて、みたいな処理を何度も書く事になるので、
これがあると、めっちゃ記述量が減る。
あんど、marionetteと一緒に使うと、ソース記述量がかなり減る。
やばい。
気持ち良い。
困ったときは、それぞれのソースを読めばどうにかなってしまうので、
あくまで自分の目の届く範囲で収まっているという所が良いですね。Backbone。
和訳:BackboneとAngularを比較する
この記事は、Victor Savkin氏の「Contrasting Backbone and Angular」を翻訳したものです。
※本人から翻訳の許可も頂きました。
和訳自体は自信なく、細かな所は意訳しているので正確にはオリジナルを参照して下さい。日本語として、冗長になってしまった所はすみません。
それと、ちなみにAngularはよくわかってないので、変な和訳してるかも(気づいた方、コメントで教えて下さい)
※ちなみにtitleはcontrastingなので、両者の違いを明らかにするため対比する、なのですが、冗長だったので短くしました。
以下より、本文
アイディアとツールを比較する事は、その対象をより良く知るとても良い方法です。この記事のシリーズで、webアプリケーションを構築する時に、日常的に取り組む必要がある事柄を書きとめ、そしてBackboneとAngularがそれぞれどのように手助けをしてくれるのか示したいと思います。
私たちは何を解決したいのか?
web開発者として私たちが取り組む事の多くは、次のカテゴリーのどれかに当てはまります。
驚く程の事ではありませんが、多くのClientサイドのFrameworkはこれらについての対応をしてくれています。
Backbone
まずはBackboneがこれらの問題を解決するために我々に何を与えてくれるのか見てみる所から始めましょう。
- ビジネスロジック
- Backbone Model と Collection
- DOMの構築
- Handlebars
- 宣言的なViewロジック
- Backbone View
- 命令的なViewロジック
- Backbone.View
- ViewとModelの同期
- StickIt
- UI相互作用の管理
- JS ObjectsとMarionette Controllers
- 状態とRoutingの管理
- Backbone.Router
- コンポーネントを作る事と繋ぐ事
- 手動
http://engineering.nulogy.com/images/backbone_angular/backbone_architecture.png
Backboneについて
平凡なBackboneとAngularを比較する事は公平ではありません。そのため、ここでのBackboneは、Backbone+Marionette+その他プラグインの事とします。
ビジネスロジック
あるアプリケーションの大きなビジネスロジックの小片は、BackboneではModelとCollectionに行く事になります。 しばし、これらのObjectはバックエンドのリソースと一致する事になります。その上、これらはViewを支援する為に使われます。
DOMの構築
BackboneはDOMを構築するためにTemplateエンジンを使います。理論上、あなたが望むエンジンであれば何でも繋ぐ事が出来ます。にも関わらず、実際の所、MustacheとHandlebarsがよく使われています。結果として、BackboneのTemplateはしばしロジックがなく、文字ベースのものですが、必ずしもそうあるべき物ではありません。
Viewロジック
Viewロジックを宣言と命令に分離するアイディアは、古い物です。(元々のMVCに回帰してしまいます。)イベントハンドリングの設定とデータバインディングは宣言的です。それに対して、イベントハンドリング自体は命令的な物です。Backboneはこの2つに厳密なラインを引きませんでした。両者ともにBackbone.Viewに行きました。
ModelとViewの同期
Backboneのミニマリストの性質の為に、データバインディングについての組み込みのサポートはありませんが、幾つかのPluginにより可能になります。(Backbone.StickItのように)
複雑なUI相互作用の管理
全てのUI相互作用は、単純な物と(ObserverSyncronizationを使っての管理)複雑な物(FlowSynchronizationが必要な時)に分ける事が出来ます。
上述の通り、simpleな相互作用はBackbone.Viewがデータバインディングとイベントハンドラを使う事でハンドルする事ができます。また、幾つかは、Backbone.Viewを複雑な相互作用のハンドルをする事でも使いますが、私はこれをお勧め出来ません。Backbone.Viewは既にあまりに多すぎるのです。これこそ、単純な昔ながらのjavascriptのobjectをその代用として使う事の方が、個人的に好きになった理由です。
コンポーネントを作る事と繋ぐ事
Backboneでは、あなたのアプリケーションに最もぴったりとあてはまる方法で、コンポーネントを作って繋ぐための自由を手に入れます。その否定的な側面は、あなたが書かなくてはならない多くの決まったコードの存在があり、付け加えて、よく整理されたコードを維持し続ける事を求める規律があります。
Angular
今から、Angularが同じ問題に対してどのようなアプローチをとっているのか比較してみましょう。
- business logic
- JS Objects
- DOMの構築
- Directives
- 宣言的なViewロジック
- Directives
- 命令的なViewロジック
- Controllers
- ViewとModelの同期
- 組み込みのメカニズム
- UI相互作用の管理
- Controllers
- 状態とRoutingの管理
- Angular UI Router
- コンポーネントを作る事と繋ぐ事
- 依存性の注入
http://engineering.nulogy.com/images/backbone_angular/angular_architecture.png
ビジネスロジック
Angularが目に見えるプロパティーを使わない為に、Modelの実装する時に直面する制限はありません。extendするclassはありませんし、何かに従うインターフェースもありません。あなたは、欲しい物であれば何でも使うのは自由です。(既存のBackboneのModelも含めて)実際の所、多くの開発者は単純な昔ながらのJavascriptのObjectを使っているようです。
TemplateとView
Angularのテンプレートはコンパイル前のDOMの破片です。コンパイルしている間、AngularはDOMサブツリーを変換して、そこに幾つかのJavascriptを取り付けます。このコンパイルの結果、Viewとなる、元とは別のDOMサブツリーが出来ます。他の言葉で言うと、あなたは自身でViewを作らないのです。AngularはTemplateをコンパイルする事でこれを行います。
DOM構築
BackboneはDOMの構築をViewロジックから明確に分けます。前者は、Templateエンジンによって、後者はデータバインディングと命令的なDOMの更新によって行います。その一方で、Angularはこの2つを分けません。どちらもdirectivesという同じメカニズムを使って、DOMの構築を行い、宣言的なViewの態度を明確にします。
Viewロジック
Angularはしかしながら、宣言的なViewロジックと命令的なViewロジックにラインを引きます。前者は、Viewによって、そして後者はcontrollerによって行われます。
ModelとViewの同期
Angularは多くのクライアント側のフレームワークと対照的に、データバインディングを組み込みでサポートしており、それは目に見えるプロパティーに依存する事はなく、代わりに、dirty checkingを使います。
複雑なUI相互作用の管理
既に述べた通り、ControllerはUI要素の命令的なロジックの実装についての責務があります。その上、複雑なUI相互作用を調整するSupervising Presenterとして使われる事が可能です。
状態とRoutingの管理
Backboneと同じように、Angularの中の組み込みのrouterがとても基本的な物で、本物のアプリケーションと繋ぐには十分とは言えません。感謝したいのは、Angular UI Routerプロジェクトの存在です。アプリケーションの状態、View、そしてネストのサポートを管理します。言い換えれば、あなたがrouterに期待する全ての事をやってくれるのです。しかし、あなたはこれに限る事はないでしょう。Backboneのように、他のroutingライブラリー(router.js等)で同じ事が出来るのです。
コンポーネントを作る事と繋ぐ事
AngularはIoC コンテナーを持っていて、それは、いわゆる依存性の注入のようなもので、あなたにmodularなコードを書く事を強制します。これは、再利用性、テストし易さを向上させ、多くの同じ様なコードを取り除く事を助けてくれるでしょう。否定的な側面としては、複雑性が増してしまい、どのようにコンポーネントが作られているのかについてのコントロールが及ばなくなってくる事でしょう。
要約
以上が、webアプリケーションを開発する時に毎日対処している中心的な問題について、BackboneとAngularがどのように取り組んでいるのかについての短い概要でした。このシリーズのパート2で私はBackboneとAngularのビジネスロジックの実装について説明していこうと思います。