@kyanny's blog

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

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 とかもこんな感じの使い方してた気がする。内部の実装も眺めたけどどのくらい(似てる|違う)のかはよくわからず。でもこういうのってこうやって作るんだーというのがぼんやり把握できただけでよしとする。

Backbone.Collection

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

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

  • toJSON が単に models それぞれの toJSON を map で呼んでるだけ、というのはきれい
  • add のなかで for を使ってるけど _.each とかではダメなんだろうか、 remove も同様
  • get と getByCid も同じロジックでよさそうなものなのに実装が違うのがなんか気になる
  • sort は comparator がないと例外を投げるようだけどデフォルトではなにも設定されないんだろうか
  • https://github.com/rails/rails/commit/a382d60f6abc94b6a965525872f858e48abc00de これみて pluck ってなんじゃらほいと思ってたけどまさかここで見るとは... (で未だにどういうニュアンスの単語なのかわからない)
  • Backbone.Model もそうだったけど通信発生するあたりをぜんぶ (this.sync || Backbone.sync ).call とかに移譲しててよくできてるなーと思う
  • reset と _reset とか、 API として公開するものと内部的なものとのポリシーがはっきりしている感じ、 API として公開するものは実装も自分自身や別クラス?の API を呼び出してるだけだったりして、実際に内部の値をいじるのは _hoge みたいなプライベートメソッドになるべく寄せる、みたいな
  • Backbone.Collection のインスタンス?が直接 .each とかを呼べるように prototype に _.each とかを突っ込んでいて、 JavaScript ではこうやって MixIn するのかーと

Rails の resources ルーティングの URL を collection の base url に指定すると何もかもうまくいくように作ってあって、なるほどねーと思った。 Rails を知ってたらどういうもののフロントエンドを作るときに使えばいいのかすごくイメージしやすいし、関数の命名も ActiveRecord に似せてるから「Model がインスタンスで Collection がクラスなのねはいはい」ととっつきやすい。 Rails の連中が作ったんじゃないのこれという気がしてくる (documentcloud がサーバサイドに何を使ってるのかは知らない)

Backbone.Model

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

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

  • initialize にデフォルトで何もしない関数を割り当てておくことで呼び出し時に isFunction とかでチェックしなくて済むワザ
  • escape の内容は初回呼び出し時にキャッシュ
  • set は呼び出し時に元の値をとっておいて値が同一でないもの(更新されるべき)だけ更新している。このチェックは変化した属性に対する change:xxx イベントの発火のために必要
  • unset でもバリデーションが走るのは何故だろう。 void 0 は undefined というグローバル変数の値にかかわらず常に undefined を返すんだっけか。 validObj に undefined をセットしてバリデーションを実行、やっぱりよくわからない。 !== undefined 的なバリデーションルールを考慮してるんだろうか
  • clear の for (attr in old) validObj[attr] = void 0; みたいに一行に詰め込むのを多用しているなーという印象がある、ブレース省略スタイルは個人的には採用したくないけどすっきり読めるとは思う
  • url を collection やら何やらからとってくるあたり
  • parse みたいな一段かます設計にしとくと汎用性が高まる

全体的に RubyActiveRecord っぽいなーと思った。メソッド名とか。そしてその思いは Backbone.Collection を読んでさらに強まるのであった。

30days Album はどのようにして画像にアクセス認証をかけているか

30days Album は画像の URL にもアクセス認証を入れています - 刺身☆ブーメランのはてなダイアリー の技術的な解説。基本的に 関西オープンソース 2008 30days Albumの裏側 のとおり。


ミドルウェアはこのスライドのときと比べてけっこう様変わりしている。 Perlbal は相変わらず使ってるけど。

  • リバースプロキシは nginx
  • バックエンドに Apache (Passenger) と Perlbal
  • 静的ファイルは nginx が配信
  • 画像の URL は Perlbal にプロキシ
    • 画像認証用の Perlbal Plugin がセッションストレージの Kyoto Tycoon に認証情報があるか問い合わせ
    • それ以外にも提携している外部サービスのために特定の IP アドレスは素通りさせたりしている
    • 画像ストレージは MogileFS なので X-REPROXY-URL などでよしなに配信
  • それ以外の URL は Passenger (Rails) にプロキシ
    • アルバムを閲覧するときは、
      1. Rails 側でログイン処理時にセッションストレージに認証フラグをたてる
      2. Perlbal はフラグをみてレスポンスを出し分ける

現状把握している問題点

  • ログイン認証なしで閲覧できるアルバムの場合は「最初にアルバムを閲覧したとき内部的にログインと同じ処理をする」のでアルバムの URL を一度も訪問してないと認証かけなくていい画像の URL も直リンクで閲覧できないまま(ブログとか外部サイトに貼り付けられない)
    • いまのところアドホックに「このアルバムは特別に許可」とかやってるけど oEmbed とかでちゃんと解決したい
    • アルバムじゃなくてフォトストレージのほうはインターネット全体に公開することも可能(新しく公開用の画像 URL ができる)

あと、規模的な話。 30days Album にある画像は MogileFS 調べで五億枚くらい (オリジナル画像だけじゃなくてリサイズしたサムネイル画像、あと一括ダウンロード用の ZIP ファイルも含む) なので、 Facebook, Picasa, mixi などと比べるとおそらく二桁か三桁、ひょっとすると四桁くらい少ないと思う(参考: はてなで約 41 億)その規模になっても今のような仕組みで画像へのアクセス認証の仕組みを維持できるかはわからない(このままではスケールしないと思うので)まぁ当然だけどこれをもって Facebook などより優れていると言うつもりはない。

こういう話も機会があればまた紹介したいですね。図を描くのが面倒だけど。