@kyanny's blog

My thoughts, my life. Views/opinions are my own.

Backbone.sync

Backbone.sync を読んだ。ざっと眺めただけ。これだけ小文字 (Sync ではなく sync) なのがなんか気になる。と思ったらこれは new するためのクラス(関数)じゃなくて単なる関数だからだった。

http://documentcloud.github.com/backbone/docs/backbone.html

  • リモートサーバの API を呼び出して、 Backbone.Model なインスタンスのデータを取得したり更新したりする永続化ストレージと通信するためのクラス。デフォルトでは Rails の resources ルーティングのルールを想定した作りになっていて、違う感じの API をバックエンドにする場合はこのクラスを上書きしろ、という感じ。
  • ちょっとした変更は全部書き換えなくていいように専用の変更用インターフェースがある。 emulateHTTP とか。
  • デフォルトではリクエストは JSON でおくる。 emulateJSON が有効だとフォーム送信の形式を使う。
  • 送信用パラメータとかリクエストヘッダとかを調整した上で、最後は $.ajax を呼び出して丸投げしている。なので、 jQuey なりそういうライブラリが別途必要。このへんは割り切ってるなーと思う。

要するに、単なるパラメータ調整用のラッパー関数なので、短いしやってることもすぐわかる感じだった。 xhr.setRequestHeader('X-HTTP-Method-Override', type); とかは、まぁこれ使えば覚えておく必要はないんだけど、これを使わないで自前でこういう風にリクエストをおくるときに、こういうリクエストヘッダの指定漏れとかでうまく動かなくてデバッグに苦労するとかよくあるので、見たことがあるだけでも記憶の底にひっかかっていて解決の糸口になったりするので、かけ捨ての保険みたいな意味で読んでおいてよかったと思う。

Backbone.View

Backbone.View を読んだ。ざっと眺めただけ。

http://documentcloud.github.com/backbone/docs/backbone.html

  • model: とか collection: とか tagName: とかは viewOptions というのに定義されてる
  • selectorDelegate というやつで、 jQuery とかの selector の検索範囲を特定の DOM 要素以下に限定するようなのを使っていて、 this.$ にそれで得られたオブジェクトをバインドしている
  • initialize, render は最初は空っぽな感じで、上書きして使うことが想定されている
  • make というのがあって、動的に DOM 要素を作って返す。テンプレートが必要ないような小さい要素の場合に使う、みたいなメモがあるけどどう嬉しいのかよくわからず
  • delegateEvents は Backbone.View 独特の "click .button" : 'save' みたいなイベントハンドラの宣言を受け取って、実際にイベントハンドラをバインドする。 new Backbone.View() したときに実行される。 for in でループして一個ずつやっている。 underscore.js の _.bind と jQuery などの $.bind が入り交じってて、 underscore.js のほうの理解があいまいなのでぼんやりしてしまう。 this を固定するためのものだっけ _.bind は。
  • _configure で viewOptions が Backbone.View のインスタンスのプロパティに生える。
  • _ensureElement で el: の値を初期化する。 Backbone.View のインスタンスが、実際どの DOM 要素と対応してるのかを示すプロパティ。自前で指定すればそれを使い、そうでなければ tagName とかから自動生成。これも new Backbone.View() したときに実行される。

delegateEvent で this.el に対してイベントハンドラをセットするあたりがこのクラスのキモで、あとは render() のインターフェースを揃えて、 DOM 操作がカオスにならないようにする、くらいの意義なのかな。薄い印象を受けた。眺めただけなので細かいコーディングレベルまで踏み込まずぼんやりしてるため、この行のテクニックが!みたいな感想が書けなくて薄いのはこのエントリのほうだ。

Backbone.History

Backbone.History を読んだ。ざっと眺めただけ。

http://documentcloud.github.com/backbone/docs/backbone.html

  • history.pushState or URL のハッシュ部分 (# 以下) のどっちかをよしなに使ってクロスブラウザで履歴管理を楽にやるためのラッパークラス
  • handlers は Backbone.Router を読んだときに出てきた、「URL がルーティングにマッチしたら呼び出されるコールバック関数」を入れておいたりするためのもの
  • getFragment のなかで、 pushState 利用モードのときは URL をいろいろ調整したうえで fragment にあたる部分を取り出す。 pushState じゃない場合は location.hash をみるだけ。
  • start は多重呼び出しされないようになってる。あと History API が利用できるかどうかなどもここでチェック。そして古い IE の場合は iframe を使うので this.iframe を初期化して navigate を呼ぶ。
  • pushState がある場合は popState もあるので checkUrl を popstate にバインド。 onhashchange があれば checkUrl をバインド。どっちもなければ setInterval で監視。
  • checkUrl で URL の変化を監視して変化してれば loadUrl を呼び出す
  • loadUrl のなかでルーティングにマッチするか調べて、コールバックを実行する
  • navigate のなかで変化後の URL を履歴に保存する。 pushState があればそれを使い、なければ iframe の location に保存する。

このクラスはけっこう動作のキモになるところっぽいけど、具体的に自分でコードを書いて使ってみないとピンとこない。あと、まず自分で pushState なり hash fragment なりを使って URL 遷移を管理するコードを書いてみて、その大変さとか面倒くささとかを体感しないとありがたみが薄くて感動が少ないのかもなーと思った。あと、本当はそれなりに面倒くさいJavaScriptとhistoryとAjaxのお話 - 愛と勇気と缶ビールなどを読むと History API まわりをクロスブラウザでちゃんと扱うのはなかなか難しそうで、 hist.js のソースを眺めて見比べると Backbone.History でちゃんともろもろカバーできてるのか?という疑問が。いちおう、適当な URL をブックマークしておいたり直接アクセスしてきても正しい感じに動作するように書かれてる気はするけど・・・。もしダメだった場合に hist.js なり代替のもっと高機能な何かと置き換えられるか?という点については、 Backbone.History はその他のクラスと全然依存してないので、問題なさそう。

Backbone Demo: Todos の処理の流れを追う

Backbone Demo: Todos これ前に読んだは読んだけどどうやって動いてるのかわかんないので本体とあわせて追った。目スタックトレース、と書いてスクリプトデバッガ使えばよかったと気づいた。 annotated source code は github レポジトリにあるものと微妙にちがっていて当てにならない。あと localStorage から読み出すあたりは別の .js ファイルに書いてあるのでそっちも読まないとわからない部分もある。とりあえず AppView の render() が呼び出されるまでの流れ。

ついでに localStorage から保存済みの Todo を読み出す部分 (あとで書く)

  • window.Todos = new TodoList; todos.js#L68
  • window.TodoList = Backbone.Collection.extend({ todos.js#L35
  • localStorage: new Store("todos"), todos.js#L41
  • var Store = function(name) { backbone-localstorage.js#L17
  • var store = localStorage.getItem(this.name); backbone-localstorage.js#L19
  • this.data = (store && JSON.parse(store)) || {}; backbone-localstorage.js#L20
    • ここまでで localStorage からデータを読み終わってて Backbone.sync を backbone-localstorage.js で上書きしてるので Todos.fetch() したときに read イベントで store.findAll() が初回に走ってそのあとに collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); から reset -> add などと呼ばれていってあー見失った。。


なんでこんなことやろうと思ったのかもついでにメモ

  • new AppView() しただけでいろいろ表示されるのが魔法みたいで不思議だった
  • 魔法なんてあるわけないし、ちゃんと render() が呼ばれてるはずだけどどういう処理の流れで呼ばれるのかわからなかった
  • 「new したら render() が自動的に呼ばれます」で片付けるのは気持ち悪いので確かめてみたかった

Backbone.Router

Backbone.Router を読んだ。ざっと眺めただけ。

http://documentcloud.github.com/backbone/docs/backbone.html

  • そもそも Router とはなんぞや?というと、クライアントサイドでもサーバサイド同様に URL でもって処理変えましょうね、という。 History API が使えれば pushState を使い、でなければ hash fragment を使う。そこのところを透過的に扱える機能を提供、ということ。
    route : function(route, name, callback) {
      Backbone.history || (Backbone.history = new Backbone.History);
      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
      Backbone.history.route(route, _.bind(function(fragment) {
        var args = this._extractParameters(route, fragment);
        callback.apply(this, args);
        this.trigger.apply(this, ['route:' + name].concat(args));
      }, this));
    },
  • ここはどうなってるかというと:
    • Backbone はグローバルな名前空間にある Backbone オブジェクトで、 history プロパティを Backbone.history インスタンスで初期化する
    • 第一引数で渡された route が正規表現オブジェクトでなければ正規表現オブジェクトにする
    • Backbone.History.route を呼び出す。引数は↑で正規表現オブジェクトであることが保証された route と、第二引数にあたる部分が長いけど _.bind() で囲われた部分を丸ごと渡している
    • _.bind(function, object, [*arguments]) を呼び出しててこれの第一引数 function にあたるのが function(fragment) { ... } までで、第二引数は this なので new Backbone.Router.extend したインスタンス自身?
    • このでかい (function(fragment) { ... }) のなかで callback つまり route にマッチしたとき実行して欲しい関数が呼ばれる
    • this つまり new Backbone.Router.extend したインスタンス自身を引き回して callback.apply に渡してるのは、たぶん callback の定義は extend({ ... }) の中に書きたくて、それだと this.route('/foo/bar', 'foobar', function() { ... }) みたいになるから (この this が new Backbone.Router.extend したインスタンス自身なので) this が狂っちゃうとうまくないから、だと思う
    • Backbone.History.route は内部の handlers という配列に route と callback のペアを追加していくだけで、 Backbone.History.loadUrl が呼ばれるときにさっき渡した callback 関数も実行される
    • Backbone.History.route は (route, callback) を受け取るので _.bind() の返す関数オブジェクトを渡してて正解
    • Backbone.Router.route の第二引数に渡される name は trigger すべきイベント名を決めるために使われる
  • ということで書いてる自分にすらどうなってるか理解した上で書けてるのかはなはだ怪しいけどこんなもんにしとく
  • navigate も Backbone.History.navigate に丸投げしてるだけ
  • _bindRoutes は「後に定義された route を採用する」振る舞いのために unshift で空の配列に逆順で詰めなおしてから Backbone.Router.route で追加しなおしてる。 _bindRoutes は new Backbone.Router.extend({ ... }) したときに実行されるので、 extend に route の初期値を渡したときのためにある
  • _routeToRegexp はいろいろがんばって正規表現にしてるけど読んでない。あと最初のほうの capture 用っぽい正規表現も読んでない
  • _extractParams はどう動くのかよくわからん・・・たぶん /:id みたいなルートに対して /17 みたいな URL を渡すと [17] を返してくれる、んだと思うけど

というわけで、 Backbone.History がこれよりもっと重要っぽいことがわかった。こちらもざっと眺めてみた感じ、ブラウザ間の差異 (iframe 使うとか) とか、勉強になりそうな感じだし一つの関数のボリュームも大きそうだったけどまた別の機会に。

ルートの追加のところとかなんか見覚えあると思ったら CPAN モジュールの Router::Simple とかもこんな感じの使い方してた気がする。内部の実装も眺めたけどどのくらい(似てる|違う)のかはよくわからず。でもこういうのってこうやって作るんだーというのがぼんやり把握できただけでよしとする。