ES5の範囲でOption<T>型を表すライブラリ、option-t を作った

動機

初期状態で未選択なラジオボタンがあるようなフォームを作っている場合、ラジオボタンに対応するモデルの値を「この値は未選択である」というのをJSで表現するのは結構面倒くさい。チェックボックスであれば, booleanのどちらかで状態が確定するが、ラジオボタンだと取りうる値は複数になるし、初期状態で選択されているか否かの問題が発生する。選択されていない状態を専用にフラグとして持つのは気持ち悪いが、かといって、未選択の状態を-19999ないしnullundefinedで表現するのは危うい。コードを書いた本人しかわからない。

RustやScalaなどのようにOption<T>/Maybeがある言語なら、こんなまどろっこしい思いはせずに、明示的に値の有無を表現できる。 というわけで、ないなら作ってしまえば良いじゃないメソッドで作った。

Option<T>型について

私が説明するよりもわかりやすいであろう先行の説明があるので、それを紹介する。

作ったもの

元々、仕事で書くコードの為に作ったものなので、もしかしたら会社のgithubアカウントの方に移管するかもしれない。

設計思想

  • Option<T> をECMA262 5thの範囲内で実装する
  • APIモデルについては、Rustのstd::optionをベースとした
  • 詩的な名前は思い浮かばなかったので質実剛健な名前にした
    • あまり文学的な名前にしても後で忘れる。
  • 主対象環境は、TypeScriptおよび生のES5

使い方

こんな感じ

var OptionT = require('option-t');

// `Some<T>`
var some = new OptionT.Some(1);
console.log(some.isSome); // true
console.log(some.unwrap()); // 1

// `None`
var none = new OptionT.None();
console.log(none.isSome); // false
console.log(none.unwrap()); // this will throw `Error`.

おなじみのmap(), flatMap()の他に、値を取り出す為のunwrap()、デストラクタ代わりのdrop()あたりを実装している。最終的には、Rustのstd::optionAPIのほとんどを実装したいところ。

Some<T>NoneインスタンスOption<T>であることを、どう表現するか?

これが一番悩んだ。最初はOptionTypeのような一つのオブジェクトを用意し、コンストラクタに渡す引数がundefinedか否かを確認するようにしていたんだけど、「正常系でundefinedを返すケースでも、暗黙的にNoneに変換されてしまう」ため、断念して、結局別個のオブジェクトを作る事になった( OptionTypeについては移行期間中のため、deprecated扱い)

ここで、それぞれのオブジェクトがOption<T>であることを表現する為に、Option<T>というインターフェースを用意して、TypeScript向けにはそれぞれがOption<T>を実装していると見せることにした。

一方、生のJS向けにはOptionTというオブジェクトをプロトタイプチェーンの祖先に突っ込んで(基底クラスに突っ込んで)、option instanceof OptionTで確認できるようにした。が、これについてはあくまでも型システムにインターフェースを所持しない環境向けであり、TypeScriptのようにインターフェースが普通に使える環境には露出させていない。

Promise<T>Maybeモナドのように使うのではダメだったか

2015年の我々には、モナドと非同期コンテナを混ぜ込んだ、Promise<T>という共通インターフェースがある. これを使うのも考えたが、少なくともES5にはawaitに相当する仕組みが無く, Promise<T>では値を同期的に取り扱うことができないので止めた。RxなどのObservable<T>についても同様。同期的に扱えるコンテナが欲しかった

なぜES5? ES6でもbabelでも良いじゃん?

ES5で十分に書ける範囲の内容なので、transpilerは(要らないと)判断した. そのうち気が向いたらTypeScriptやbabelに移行するかもしれないけどね。

browserifyありきなんですが

browserifyありきですね

先行事例

作った後に気がついたんだけど、極めて類似の先行事例にopty (npm)があった。こちらはRustのAPIモデルをベースにTypeScriptで書いたもの。

先にこっちに気がついていればoptyを使う可能性もあったけれど、テストコードが無かったのと、JSONシリアライズ時の表現が特に記述されていなかったのと、RustのそれよりもAPIが(無駄に)増えていたので、音楽性の問題で当面は別個に開発するつもり。将来的には、あっちに合流して大統合してもいいかなとは感じる

まとめ

感想お待ちしております

CSSのcanvasとviewportとposition:fixedとpinch zoom

  • specを元にした概念の整理
  • 間違いあったら教えて欲しい

CSS 2.1におけるviewport

CSS 2.1におけるviewportを説明するにあたり、以下のterminologyが必要となる

canvas

For instance, user agents rendering to a screen generally impose a minimum width and choose an initial width based on the dimensions of the viewport.

viewport

User agents for continuous media generally offer users a viewport (a window or other viewing area on the screen) through which users consult a document.

When the viewport is smaller than the area of the canvas on which the document is rendered, the user agent should offer a scrolling mechanism.

initial containing block

The containing block in which the root element lives is a rectangle called the initial containing block. For continuous media, it has the dimensions of the viewport and is anchored at the canvas origin; it is the page area for paged media. The 'direction' property of the initial containing block is the same as for the root element.

  • ブラウザ(=screen mediaのUA)のスクロールバーを伴った描画領域 = viewport
  • レイアウトはcanvasに描画される
  • initial containing block(デフォルトのcanvas領域)のサイズはviewportの幅と高さに依存する.

viewportが1000pxの場合にhtml要素のwidthを100000pxにしたらどうなるのか?については、overflowプロパティによって定義されていて、

UAs must apply the 'overflow' property set on the root element to the viewport. When the root element is an HTML "HTML" element or an XHTML "html" element, and that element has an HTML "BODY" element or an XHTML "body" element as a child, user agents must instead apply the 'overflow' property from the first such child element to the viewport, if the value on the root element is 'visible'. The 'visible' value when used for the viewport must be interpreted as 'auto'. The element from which the value is propagated must have a used value for 'overflow' of 'visible'.

のように,

  • root要素のoverflowプロパティがviewportに対しても適用される
  • viewportのoverflowプロパティがvisibleである場合、autoとして解釈されるので、スクロール機能の有無はUA依存となる

position:fixedの挙動

In the case of handheld, projection, screen, tty, and tv media types, the box is fixed with respect to the viewport and does not move when scrolled.

For other elements, if the element's position is 'relative' or 'static', the containing block is formed by the content edge of the nearest ancestor box that is a block container or which establishes a formatting context.

要は、対象のボックスがviewportに対しての固定座標に配置されるということ.

CSS Device Adoptationでの拡張

CSS Device Adaptationでは、meta[name="viewport"]でのviewportサイズの指定を可能にしたiOS Safariなどの実装を追認を行う形で仕様が策定されており、ここでは、initial viewportとacutual viewportの二種類が定義されることになった。

initial viewport

This refers to the viewport before any UA or author styles have overridden the viewport given by the window or viewing area of the UA. Note that the initial viewport size will change with the size of the window or viewing area.

actual viewport

This is the viewport you get after the cascaded viewport descriptors, and the following constraining procedure have been applied.

たとえば、meta[name="viewport"][content="width=device-width"]を指定した場合は以下のようになる

  1. initial viewportがブラウザのウィンドウサイズや描画領域に基づき決定される
  2. UAスタイルおよびユーザースタイルによるmeta[name="viewport"]@viewportの計算が行われる

    1. meta要素によるwidth=device-width@viewport { width: 100vw; }として取り扱われる
    2. ここでの100vw1で計算されたものが取り扱われる
    3. width: 100vw;max-widthmin-widthマップされる
    4. 3でマップされた結果を元に、actual viewportの幅を計算する
  3. 計算の結果、deviceの幅=ウィンドウの幅を自身の幅としたactual viewportが算出される

  4. actual viewportを元にinitial containg blockを再度計算する

ここでもUAスタイルがあるために、仮にviewportの指定が行われていない場合は、従来のサイトとの互換性のために、モバイルブラウザでは自動的にviewportの横幅が1000px前後に設定されたりするようになる。

デスクトップブラウザについては@viewportUAスタイルを持たないと解釈すればいいのだろうが、dbaronによるissue表記があるので、完全に定義が確定しているわけではないのだろう。

では、メディアクエリはどこで計算されるのか? それは以下のように定義されている:

  1. Cascade all @viewport rules using the initial viewport size for values and evaluations which rely on viewport size
  2. Compute the actual viewport from the cascaded viewport descriptors
  3. Cascade all other rules using the actual viewport size

つまり、viewportの計算が全て終わらないとmedia queryをはじめとした計算は起こらない

zoom

ズームについては、まず、CSSOM Viewにて二種類のズームがあることが述べられている:

There are two kinds of zoom, page zoom which affects the size of the initial viewport, and pinch zoom which acts like a magnifying glass and does not affect the initial viewport or actual viewport.

この詳細についてはCSSOM View側では「CSS Device Adaptationを参照」とあるのだが、CSS Device Adaptationには明確なterminologyが設定されているわけではなく、断片的に文章中に記述されているだけである。

When the actual viewport cannot fit inside the window or viewing area, either because the actual viewport is larger than the initial viewport or the zoom factor causes only parts of the actual viewport to be visible, the UA should offer a scrolling or panning mechanism.

This is a magnifying glass type of zoom. Interactively changing the zoom factor from the initial zoom factor does not affect the size of the initial or the actual viewport.

position:fixedとpinch zoomを組み合わせるとどうなるのか

今までに述べた内容をまとめると、

  • viewportはブラウザ(=screen mediaのUA)のスクロールバーを伴った描画領域
  • position: fixedは、対象のボックスをviewportに対する固定座標に配置する
  • pinch zoomはactual viewportに影響を与えない

となるのだが、pinch zoomをした場合に、Android BrowserやiOS Safariでは常にUAの描画領域の枠の固定位置にposition: fixedが配置されるのは、仕様上正確なのかどうか分かり難い…… pinch zoomではviewportに影響を与えないとあるが、zoomをしている以上は、UAとして表示している領域としてのviewportの横幅は相対比で小さくなっているはず。ちなみにW3C Bugzillaでは特に何も見つからず……

pinch zoomとpostion: fixedへの各UAの対応

IE11 Mobileや最近のChromiumでは、この問題に対して、「pinch zoomを行っても、position: fixedを常にUAとして表示している領域に配置しないようにした」(私の知りうる限り、Firefox修正を検討中である)

どのような変更を行ったのかはChromiumの変更を開設したスライドがわかりやすい。

このスライドでは、viewportとされるものは正確には、

  • visual viewport: UAとして表示している領域という意味でのviewport
  • layout viewport: レイアウト計算に用いられるviewport

の二種類が存在するとしている(正確には、この二種類が存在しているとすることで、問題を解決することに成功した)。

pinch zoomの存在しなかった時代(CSS 2.1での定義)では両者は統一して扱われるものであったのだが、モバイルブラウザ(およびUIとしてのpinch zoom)の登場により、分割して扱う必要が出てきた。しかし、現行のCSS Device Adaptationではそこにまで踏み込んだ定義がされていないために話がややこしくなっている。いや、この一件に限らず、CSS Device Adaptationは出来がいいspecとは思えないですけどね……

ちなみにWindow.scrollX/Ywindow.innerWidth/Heightなどのviewportのスクロールに絡む座標は、Chrome Beta for Android 40.0.2214.69で確認したところ、visual viewportを基準に算出されるようになる。まあそうするしかないですよねcompat的にも。なので、

The innerWidth attribute must return the viewport width including the size of a rendered scroll bar (if any), or zero if there is no viewport.

The scrollX attribute attribute must return the x-coordinate, relative to the initial containing block origin, of the left of the viewport, or zero if there is no viewport.

以上の定義中にある"viewport"は, visual viewport。ややこしい。

まとめ

図にするとこんな感じ。仕様+実装を元に、viewportというものは、このような概念であると解釈が出来る。

尚、visual viewportとlayout viewportという単語は、Chromiumのスライドのやつが都合がいいから使ってるだけね。

   +---------------------+----------+
   |                     |          |
   |  *--------*         |          |
   |  |        |         |          |
   |  |  visual viewport |          |
   |  |        |         |          |
   |  |        |         |          |
   |  |        |         |          |
   |  *--------*         |          |
   |                     |          |
   |                     |          |
   |   layout viewport   |          |
   |                     |          |
   |                     |          |
   +---------------------+          |
   |                                |
   |       canvas                   |
   |                                |
   +--------------------------------+

  • directionがltrの場合, 左上からactual viewportが始まる
  • layout viewport = actual viewport
  • layout viewportはinitial contaning blockの大きさを決める
  • visual viewportはpinch zoomなどにより虫眼鏡のようにサイズが 可変したり、スクロールしたりする
    • overflow:hiddenがviewportに適用される場合はinitial containg blockを超えて動けない
    • Window.scrollX/Y, Window.innerWidth/Heightなどのスクロール関係値は、visual viewportを元に算出されていると解釈できる
  • IE11 Mobileや最近のChromiumを除く従来の実装では、position:fixedはvisual viewportを元にレイアウトされていると考えられる

2014年やったこと(物書き業編)

MozillaのLevel 3 Committerになった

だいたい三月ぐらいに。

Mozillaにはコミッタにアクセス権限がレベル形式で設定されていて、レベルに応じてパッチをlandできるリポジトリに制限がある。Core ProductsであるFirefoxなどのリポジトリmozilla-central/inbound/fx-teamには)に投入するには、Level 3が必要。Githubに置かれてるリポジトリは、ものによって微妙に異なるけど、概ね、この規約に基づいている。

アクセスレベルが足りない場合は、パッチのレビューが終わった時点で、だれかコミット権を持っている人にlandingを頼む必要がある。が、自分からアクティブにメンターを探さないといけないわけではなくて、だいたいは該当bugにcheckin-neededフラグを立てておけば、モジュールオーナーだったりが見つけ次第landしてくれる。

自分は2012年ごろにLevel 1は取っていて、Mozillaのビルドサーバーとテストインフラを使えるようにはなっていたんだけど、(レビューをもらっても)landが自由にできないので結構苦労する時があった。複数人が現在進行形で触っている領域へのパッチなどの場合、こちらのパッチが先にレビュー完了しているのに、checking-neededに気付かれずに、他の人のパッチがlandされた結果、conflictを解消して再度やり直したり…… まあダルいよね。

というわけで、Level 3 Committerを取りました。これで自由に(もちろんr+済みの)パッチをlandできるようになりました。Level 1のときはtry-serverを使えるだけだったのでコミッタと名乗るのにちょっと抵抗があったけれども、これで胸を張ってコミッタと名乗れるようになりました。

Servo関係

色々プルリクを投げた。

一番わかりやすいあたりだと、SpiderMonkeyGCとの統合周りのデザインについてのドキュメントを書いたり、DOMまわりの細々とした変更を行ったり。主にscript関係をいじっていた感じ。結果、DOM bindingに関する知見がそれなりに増えた。

Servoのissue, pull requestは全部(流し読み含めて)目を通しているので、ほとんどのコードに何が入ってるのか大きいトピックは知っているけど、細かくコードレベルでは結構微妙。なので、2015年の抱負としては、もっと色々やっていきたい

Servoのpeerになったり、script以下のreviewer業始めたり

Githubの仕様上、issueを閉じるには権限が必要で、閉じられなくて困ってた旨をircで愚痴ってたら、peer権限もらった。モジュールオーナーというわけではないけれども、編集権限持ちです。ある種のコミッタ業(Servoの場合はpull requestをbotが自動でマージするので、コミッタ権限がそこまで重要ではないのだけれども)。

それと、主にscript以下のreviewer業も始めることになりました。とりあえずgood-first-bug級の簡単なパッチについては問題なくレビューできる感じだったり(たぶん)

余暇としてのコミッタ業

やっぱり、それなりに難しい。issueとか見忘れるとすぐに溜まる。ここらへん、もう少しなんとかうまくやれんかなというのが来年のチャレンジになりそう。

コミュニティ的な何か

Mozilla Japan様に会場を毎回お世話になった(ありがとうございます)。

Rust Language関係

Rust Samuraiをやったりした。

コミュニティ運営業は個人的にそこまで重点を置いていないのと、そろそろ言語の構文とかコンセプトじゃなくて、何ができるかとかやったほうがいい気はしているので、来年もコミュニティ業やるかのモチベーションは微妙だったり。

ブラウザエンジン先端観測会

自分の問題意識として「ド濃密な会が少ない」というのがあって、少ないならば自分でやるしかないメソッドでやった。

幸いにして好評だった模様で、次回開催について聞かれるものの、これが結構難しい… 理由は

  • 半年スパンでポンポンとエンジンが面白いことを幾つもやるわけではない
    • やっていても、とっつきやすい題材ではない。
  • speakerを探すのが大変

な辺り。前者に関しては、「わかるやつだけわかればいい」を突き進めればいいんだけど、それでも後者は難しい問題だったり。

他に面倒臭いのはマッチングなんだけど、長くなるので今回は省略する。

LL Diverに出た

縁あって、LL Diverの「帰ってきただめ自慢」のRust枠で登壇しました(資料)。

言語そのものとしては、当時はまだ1.0のリリース時期も見えなかったのと、マルチパラダイム志向でだいたいの機能を持っていたのと、常に変更し続けるスタイルなので悪いところがあってもそのうち直る可能性が非常に高いので、特にダメ自慢するところはないだろうと踏んで、主にエコシステムをダメ自慢することに。まあ、処理系が開発中でマイルストーンしか出てない言語なんで、エコシステムもへったくれもないわけですが;

その後、codegenについてはparallelに動作するようになってきたり、cargoがRust界デフォルトになってきたり、シングルスレッド性能も一部ベンチだとC++と同等を示す程度になってきましたが、相変わらずライブラリだけは少ない。ここらへんは1.0リリース後に期待したいところ。FFI bindingだけならば割と簡単にかけるので結構すぐに揃う気はする。フルRustはその後でしょうな。

2015年に向けて

通年としては

  • もう少しServo業うまくやりたい
  • 論文とかもちょくちょく読んでいきたい

短期的には

  • Rust 1.0リリース記念会は、たぶん、やります
  • ブラウザエンジン先端観測会 vol.2も、やり方変えれば目処立ちそう

みたいな、そんな感じ。

Fluxとはなんだったのか + misc at 2014

はじめに

VirtualDom - なぜ仮想DOMという概念が俺達の魂を震えさせるのか - Qiita を読んでいる前提で話を進めます。

結局”Flux”なんだったのよ

詳細については過去に自分が覚え書きを書いたのでそっちを読んでいただけると良いと思うけど、あれは MVCの変形亜種に、オブザーバーパターンを乗せ、データを単一方向に流すことを規定した」ものに、Facebook命名したものでしかない。極端に目新しいものでもない。その最大の功績はアーキテクチャそのものではなく、「試行錯誤を踏む中で誰もが一度はやっていたであろう似たようなことを上手く実践法則としてまとめた上で、共通認識としての名前をつけた」こと。概念に名前をつけて共有することで、事前説明が簡略化され、本質的な問題に取り組む時間が増える。これをFacebookのブランド力でねじ伏せるように広めたことこそが重要かつ評価すべきポイントだと思う。

Virtual DOMと組み合わせて語られることが多いけれど、別にVirtual DOMがないとFluxパターンができないわけではない。StoreからViewに向けて放たれるメッセージの粒度を、「n番目にデータを追加」「n番目から削除」「n番目を変更」などのように細かく刻んでいけばViewが何のライブラリで組まれていようが成立する。メッセージ化した時点でコマンドパターンになっていくわけだしね。

ただ、メッセージを細かく刻んでいく作業は結構だるい。そこでVirtual DOMと組み合わせると、一度に全てのデータを渡しても、どうせVirtual DOMの差分検出が面倒を見てくれるので、メッセージ粒度が「データ構造に変更あり」という粗さでも大丈夫なようになるという話。そして一回のメッセージと状態の変更を紐付けるのが容易になり、状態のスナップショットも確保しやすくなり、undo操作などの実装がシンプルなものになるというだけの話。

Fluxフレームワークについて

特にフレームワークなくても実践できるだろ

なぜ私はReact.jsを使うのか

なぜ私はReact.jsを使うのか?

  • MVCで言うところのViewとなるDOMのサブツリーの生成をテンプレートエンジン的に扱いたい
  • 適切な粒度でビューコンポーネントを分割して閉じ込めたい
  • Facebookという胴元がちゃんと自分たちで使ってることへの信頼
  • ドキュメントが非常にマトモ
  • dangerouslySetInnerHTMLとかgetDOMNode()のような緊急ハッチがある

これだけ。特に最初の2つが重要で、UIを作ろうとした場合に大きな助けとなる。

Reactの発明は成立由来からしても、UIのコンポーネント化・テンプレート化をカジュアルに行える点であり、Virtual DOMは彼らにとっては速度面のトレードオフを抑え込むためのトリックにすぎない。

別に同じVirtual DOMを使うだけであればReactよりも速いライブラリもあるのだけど、そんなものはReact.jsでパフォーマンスが問題になった時に使えばいいと思ってるし、究極的には生のDOM操作を書けば解決する問題であるし、それでも遅いならその時考えればいい話なので、全く気にしていない。

Rustプログラミングにおけるデバッグ入門

これはRust Language Advent Calendar 2014の第1日目となります。

尚、本情報はRust 0.13.0-nightly@fac5a0767時点の情報を基にしております。

現実においてプログラムを書くにあたりデバッグを行わないということはありません。ですので、通常、我々はデバッグ技法というものを習得しなければなりません。

デバッガ

RustではDWARF形式のデバッグシンボルを出力できるため

といったツールの使用が可能です。WinおよびLinuxではgdbを使えばいいでしょう。OSXでは、個人的にはlldbを使えばいいと思います(尚、gdb向けの拡張スクリプトもrustリポジトリの中には存在しています)

また、DWARF形式のシンボルを用いた各種ツールチェインも使用が可能となっていますが、各種バグには注意を配る必要があるかもしれません。

log crate

デバッガを用いずとも、変数の内容などをダンプするデバッグというのも非常に有用あるのは皆さんご存知のことと思います。殊、Rustのように平行・並列実行を指向したコードを記述する場合、デバッガでアタッチしたりrrのようなツールチェインを使わずとも済ませたい・もしくは前述のツールチェインを使いことが困難なこともあると思うので、この手法にはお世話にならざるを得ません。

そこで使うのがこの log crateです。 詳細な使い方はリンク先を参照していただくとして(安定度もexperimentalですしそちらの方が確実でしょう)、概要を説明しますと、

  1. log
  2. debug
  3. info
  4. warn
  5. error

のレベルに分けてログを出力することができます。

出力にあたっては、実行時にRUST_LOG=path::to::module=log_level環境変数を設定することで、「ログのレベル」と「ログを出力する対象であるモジュール」の指定が可能です。

この指定は、正規表現および用いた複雑な表現が可能であり、/の後にwarn/bar*fooのように続けることで「ログレベルwarn以上で、正規表現bar*fooにマッチするメッセージを含むログを出力する」などのような指定を環境変数経由で実行時に指定することが可能です。

また、原則としてmacroで実装されているので、現在の実装では、debug!に関しては--cfg ndebugコンパイルオプションとしてrustcに指定した場合に、自動で無効化されます.

アサーション

Rustには実行時アサーション複数デフォルトで存在しています.

debug_assert系では、--cfg ndebugrustcに対して渡すことにより、これらのアサーションを無効にしたコードの生成が行われます。

ユニットテストベンチマーク

Rustではコード中に#[test]または#[bench]といった指定を行ってコードを記述することで、コンパイルオプションによりユニットテストまたはマイクロベンチマーク用のバイナリを簡単に生成することが可能です。

詳しくはThe Rust Testing Guideを参照するのが良いでしょう。

バックトレース

実行時に環境変数としてRUST_BACKTRACE=1を設定することで、クラッシュ時の詳細なバックトレースを得ることができます。

cargo buildrustc —cfg ndebug相当のことをする

cargoの設定のprofileで明示的に指定を行えば、対応するビルドプロファイルに反映されます。

デフォルトでは--releaseをつけた場合にrustc —cfg ndebug相当となります

まとめ

よいRustプログラミングをお楽しみください. 明日の担当予定は@omasanoriさんです。

Fluxの枠にURLルーティングを収める試行

JSer.info 200回記念祭の懇親会でざっくりアイディアだけ話していた記憶(酔っ払っていたので正確には覚えていない)なんだけど、実際に必要になったので試しに作ってみたという話。

モチベーション

Fluxパターンを用いた設計を行なっている場合というのは往々にしてSingle Page Applicationであるので、URLに基づくルーティングを要しない、純然たるアプリケーションなケースが多い。だが、アプリケーションの性質によっては、パーマネントリンク的な機能の再現をしたいことがあり、ルーティング機構が欲しかったりする。

で、そういう場合については語られてる事例をあんまり見かけなかったので、作ってみた。

デザイン

  • Storeに基本ロジックを閉じ込めるのは変わらない
  • URLに基づく履歴情報は、ユーザーインターフェースの一種と捉える。ので、Viewと考える

使ったライブラリ

基本要件として

  • URL path文字列のパースとそれに応じたルーティングの実施を行うルーティング処理
  • URL pathを書き換えるための、History API操作

のそれぞれが独立して動くようにする必要がある。 JSのルーティングライブラリは、どういうわけかBackboneとBackboneベースの派生カスタムライブラリに乗っていることが多いので、探すのに苦労した。が、幸運なことに見つかった(見つからなかったら自作せざるを得なかった)

サンプルコード

だいたいこんな感じでいいのはないでしょうか:

var crossroads = require("crossroads");
var Dispatcher = require("flux").Dispatcher;
var EventEmitter = require("events").EventEmitter;
var hasher = require("hasher");

var RoutingActionCreator = {

    moveTo: function (path) {
        RoutingDispather.dispatch({
            actionType: "Routing::moveTo",
            path: path,
        });
    },
};

var RoutingDispather = {
    _dispatcher: new Dispatcher(),

    dispatch: function (val) {
        this._dispatcher.dispatch(val);
    },

    register: function (callback) {
        this._dispatcher.register(callback);
    },
};

var RoutingStore = {
    _emitter: new EventEmitter(),

    EVENT_CHANGE: "RoutingStore::change",

    dispatchToken: null,

    setup: function () {
        crossroads.addRoute("/bar", () => {
            // route毎に必要な処理
        });

        crossroads.addRoute("/foo", () => {
            // route毎に必要な処理
        });

        // 各route毎の処理が終わったら, storeのchangeイベントを起こす
        crossroads.routed.add((path) => {
            this.emitChange(path);
        });
    },

    addChangeListener: function (callback) {
        this._emitter.addListener(RoutingStore.EVENT_CHANGE, callback);
    },

    emitChange: function (path) {
        this._emitter.emit(this.EVENT_CHANGE, path);
    },
};

RoutingStore.dispatchToken = RoutingDispatcher.register(function(payload){
    switch (payload.actionType) {
        case "Routing::moveTo":
            var path = payload.path;
            crossroads.parse(path);
            break;
    }
});


var URIHistoryView = function () {
    this.init();
};
URIHistoryView.prototype = {

    init: function () {
        RoutingStore.addChangeListener(this.onChange.bind(this));

        // URLの変更に応じてActionを起こす
        var parseHash = function parseHash(newHash){
            RoutingActionCreator.moveTo(newHash);
        };
        hasher.initialized.add(parseHash);
        hasher.changed.add(parseHash);

        hasher.init();
    },

    onChange: function (path) {
        // Storeのchangeイベントのコールバック変更のため, URLの変更だけする.
        hasher.changed.active = false;
        hasher.setHash(path);
        hasher.changed.active = true;
    },
};

function main() {
    RoutingStore.setup();
    new URIHistoryView();
}

上のコードに書いてない点

URLに紐づく具体的なViewの生成箇所について

上のサンプルコードだと明示していない。Store内の各ルーティングのコールバック内で、適当にControllerって名前をつけた関数を呼んでもいいかもしれない。Fluxの原則に外れるけど、まあどうせ生成するViewの粒度なんて大きなものだし、枠外に外れるのはここだけだし、XXX とかつけておいてドキュメントも書いておけば良いだろう。良くないか。普通にStoreから発行されるchangeイベントを監視するのもいいかもしれない

ルーティングを回避して、URLだけ更新したい場合

専用のメッセージ作ってルーター呼ばずにそのままStoreのchangeイベントを発行してViewに伝えりゃ良いじゃろ

まとめ

本サンプルコードが動かなくても気にせず、雰囲気で行きましょう。