この記事は仮想DOM/Flux Advent Calendar 2015の25日目……に入れようと思ってたけどもう埋まってた……。
オマケということで頼む!!!!!
24日目は JavaScript - 実践:MagJS で TodoMVC - Qiita でした。
メリークリスマス!!!!!!!!!!
こんにちは id:amagitakayosi です。
みなさん今月も Flux 書いてますか?
僕はオレオレ実装をIsomorphic対応したけど昨日Revertしたところです!!!!!ウオー!!!
今日は↓12/2の記事↓の続きを書いていきます!
もくじ
前回のあらすじ
- Fluxの登場
- 戦国時代
- Reduxの爆発的な流行
2015年末、はたして誰が生き残るのか……!!!!!
flux-utils
2015-08-20にリリースされた facebook/flux v2.1.0 では、flux-utils という便利ツールが含まれるようになりました。
様々なFluxフレームワークを参考に、facebook社内で固まってきたベストプラクティスを実装に落としたもの、といった所でしょうか。
flux-utils は以下の4つのモジュールで構成されています。
Store
: Dispatcher の監視、イベントの発行ReduceStore
: state の更新処理を実装MapStore
: Immutable.Map を利用Container
: Reactコンポーネントから Store へのアクセス手段を提供
3種類の Store
クラスは Store を作成する際に使用します。
これらは Store
<- ReduceStore
<- MapStore
という継承関係にあります。
Store クラスを直接使うことはあまり無いでしょう。
ユーザーは ReduceStore 又は MapStore の子クラスを作成し、reduce メソッドに state の更新処理を実装します。
従来の facebook/flux では、Store の更新を Component に知らせるためにイベントを発行する必要がありましたが、ReduceStore では dispatch 前後で state が変更されたかを判定し、必要ならば勝手に emit('change')
してくれます。
具体的な使い方についてはazuさんの解説記事が詳しいです。
はてなブックマーク検索を作りながらFlux Utilsについて学ぶ | Web Scratch flux-utilsについて · GitHub
また、flux-utils はドキュメントだけだと動作が想像しづらいので、使いたい場合はコードを読んだほうがいいかもしれません……。
flux/src/stores at 36ebc18075d28556a3605cc6a2271cb67843e5db · facebook/flux · GitHub
reduce()
で新しいオブジェクトを作って返したり、Container でReactコンポーネントに props を渡す所など、Redux その他Fluxフレームワークの影響を受けていることがわかります。
flux-utils 内のクラスと Redux の概念との対応関係を考えてみると
- ReduceStore : reducer
- Container : connect / selector
といったところでしょうか。
ただし、flux-utils の Store には Store 間の依存関係を waitFor
で管理するための API が存在する*1など、従来の facebook/flux との整合性を意識した設計になっています。
個人的には __emitChange
などの仕組みが facebook/flux と密結合すぎてなんか使いたくない気もします。
Container vs View
flux-utils のドキュメントには、これらツールの使い方だけではなく、facebook の推奨する Best practices についても書かれています。
この中で、Flux での Reactコンポーネントの役割を Container
と View
の2つに分類する、という考え方が紹介されていました。
Reactコンポーネントを2つに分類するというアイデアは以前から存在しており、React.js Conf 2015のトークや Redux 作者のブログ記事など、様々な箇所で解説されています。
Flux 界隈では、非同期処理や View へのデータ受け渡しなど、Action / Dispatcher / Store / Component のどこにも属さない処理をどこに書くかという問題がしばしば議題に挙がります。
今日における現実的な解決策のひとつとして、UIロジックとは無関係な Container コンポーネントを用意し、その中で処理を行うというアプローチがとられています。
後述する Relay や、つい先日公開された heroku/react-refetch なども Container の一種と見做すことができるでしょう。
この概念には様々な呼び名があって混乱しやすいので、表にまとめてみました。
flux-utils | Container | View |
Redux ドキュメント | Container Component | Presentational Component |
Dan Abramovのブログ記事 | Smart Component | Dumb Component |
React.js Conf 2015のトーク | Container | components |
それぞれの解説を見比べてみると面白いでしょう。
flux-utils のドキュメントでは Container が UI ロジックを持たない事が明示されているなど、時代とともに少しずつ洗練されているのかもしれません……。
Cycle.js
Cycle.js は RxJS と virtual-dom を組み合わせたフレームワークです。
Flux の非同期 Action の難しさに疑問を持った André Staltz 氏によって開発されました。
Cycle.js には driver という概念があります。
DOM操作や通信など、RxJS の世界を離れ何らかの副作用をもたらす処理は全て driver に詰め込む、という設計になっています。
driver の出力は、DOMイベントや通信の結果を流す Observable としてまとめられます。
これによって、Observable をこねこねするだけでアプリケーションが構築できます。
Flux では Store の更新を受けて Action を発行することが難しく、前述した Container Component で処理するしかありません。
Container はUIロジックを持たないとはいえ、殆どの場合Reactコンポーネントで書くことになるため、View 層にドメイン知識を漏らすような気がして扱いが難しいです。
Cycle.js では driver は Observable さえ返せばよく、virtual-dom との結びつきが弱いため、より素直に実装できるようになっています。
その他の詳しい設計については省略します。
Cycle.js は r7kamura さんのブログ記事がバズって少し流行ったので、日本語の情報も見つけやすいと思います。
僕もちょっと試して感想書いたりしました。
r7kamura.hatenablog.com amagitakayosi.hatenablog.com
flux-challenge
Cycle.js の作者 Staltz 氏はまた、flux-challenges というリポジトリを公開しています。
こちらは、ある要件を満たすWebアプリの実装を募集し、様々な実装を比較することでFluxアプリケーションの設計について考察するという試みです。
氏は Flux における非同期 Action の実装方式に疑問を持っており、この実験を通して Cycle.js のアプローチの妥当性を検証しようとしているようです。
2015-12-24現在では24個の実装がsubmitされています。
使用されているライブラリ一覧を見ると、これらの実装は Flux や Redux 以外にも、3種類のアプローチに分類できるように感じられます。
以下では各アプローチについて、代表的な submit とともに紹介します。
Rx系
この分類のライブラリ: baconjs, DDOM, thisless-react, Yolk, Mobservable、
JavaScript界でのRx系ライブラリでは RxJS が有名ですが、他にも似たようなアプローチをとっているライブラリが多数存在します。
Cycle.js は RxJS を利用していましたが、他は baconjs, kefir などが有名ですね。
flux-challenge には、Cycle.js 同様に有名Rxライブラリの良い使い方を模索するもの、オレオレ Observable ライブラリでてアプリケーションを構築するものの2通りのタイプが見られました。
余談ですが、ここで紹介する Rx 系のライブラリは今年後半に開発開始した物が多いです。
(thisless-react, Yolk が9月、DDOM が11月)
Rx界隈で何か祭りでもあったんでしょうか?
thisless-react, Yolk
thisless-react は rx-redux の作者によるライブラリで、RxJS, Redux と組み合わせて開発を行います。
React と同じく、UIの定義とイベントハンドラを一緒に定義し、View のツリーのルートでアプリケーションを管理するという設計になっています。
UIの記述には同作者の react-reactive-component を利用します。
こちらはコンポーネントの props に Observable をそのまま渡すことができ、Observable が値を更新する度に自動で表示や振る舞いも更新してくれるというものです。
Yolk は RxJS + virtual-dom を利用したフレームワークです。Cycle.js と同じ組み合わせですね。
こちらも thisless-react 同様 React に近い書き方でコンポーネントを作成でき、props には Observable を渡すことができます。
派手な機能はないですが、CustomComponent クラスを継承することで Observable を受け取るコンポーネントを簡単に定義できるようになっています。
これらのライブラリには Cycle.js でいう driver のような概念は存在しませんが、その分 React のユーザーにはとっつきやすいと思います。
非同期処理の問題は RxJS があるから大丈夫、ということでしょうか。
DDOM, Mobservable
github.com DerivableJS API Documentation
DDOM は同作者の DerivableJS というオレオレ Observable ライブラリを利用したフレームワークです。
DerivableJS には Derivable, Reactor という概念が存在します。
Derivable はRxでいうところの Observable, Reactor は Subscriber みたいな感じです。
また、Derivable は単純な値への参照である Atom と、Atom から算出される Deravation に明確に区別されます。
README.md には They are not streams
とあり、Rxとは異なる考え方が説明されています。
Derivation は常に Atom を参照して同期的に値を計算するため、Atom の値とのズレを考える必要がありません。
また、Reactor が登録されていない or Derivable があった場合、DerivableJS はグラフを解析し、不要な Derivable の計算を勝手に省略してくれます。
DDOM 自体は Derivable を扱えるようにした virtual-dom みたいな感じであんまり特別な点はないです。
Why Mobservable? | Introduction
Mobservable も Derivable 同様のオレオレ Observable ライブラリです。
Observable をReactコンポーネントに渡せる仕組み付きですね。
これらは flux-challenge の主眼であるサーバとの通信は特に改善されていませんが、Rx勢の盛り上がりを示す面白い例になっていると思います :P
ImmutableJS系
この分類のライブラリ: Freezer, Fluxlet, ImmutableJS, (Immstruct)
React を使ったアプリケーションでは Immutable.js も良く使われます。
Immutable.js はその名の通りイミュータブルなデータ構造を実現するライブラリですが、複雑にネストしたオブジェクト内の値を簡単に取得/更新するためのメソッドも提供しています。
このような高機能なオブジェクトにイベント発行機能をつければ、それだけでお手軽Observerパターンが実現できるのでは?と考える人々が現れました。
Freezer はシンプルな Immutable.Map + EventEmitter って感じですね。
ImmutableJS と違って、内部では Plain Object にデータを保存してるみたいです。
データ更新時の処理も工夫してるとの事ですが、僕は ImmutableJS をよく知らないのでわかりませんでした…… 🙇🙇
Immstruct は イベント発行機能に加え、Undo/Redo 機能まで実現したものです。
flux-challenge では利用されていませんが、Freezer と同じアプローチといえるでしょう。
Time traveling も簡単に実装できて最高っぽい……。
こちらは ClojureScript のフレームワーク Omniscient と組み合わせて使われることが多いようです。
ClojureScript系
この分類のライブラリ: re-frame, Hoplon
Om の影響は偉大ということなんでしょうか。
flux-challenge にも ClojureScript を用いた実装がいくつか submit されています。
re-frame は Reagent という React ラッパーを使用したフレームワークです。
README で「Om より Elm や Hoplon に近い」とある通り、Reagent は FRP を重視した作りになっており、ratom, reaction と言った Observable っぽい仕組みも提供しています。
re-frame では data, query, view, control の4つのレイヤーに分けて設計を行うようです。
無理やり Flux / Redux の概念に当てはめると Store, Selector, Component, Reducer という感じっぽいです。
オレオレ React もオレオレ Observable もあって欲張りですね。
READMEでも We read up on Flux, Pedestal App, Hoplon, OM, Elm, etc and re-frame is the architecture that emerged.
って言ってる。
ただデータフェッチングのための特別な仕組みはなくて、ごく普通の Flux の非同期 Action と同じように dispatch 呼びまくる、という方針らしいです。
Talking To Servers · Day8/re-frame Wiki · GitHub
あと関係ないけどドキュメントがユーモラスで面白いです。クソ長いけど。
しかも「FRP知らない人はこの4つの記事を読め、そしてこの40分の動画をみろ」とか言ってくる。
Hoplonはクライアント側だけでなく、サーバ側の処理やクライアント/サーバ間の通信処理まで面倒見てくれるフルスタックフレームワークです。 Isomorphic というわけでは無さそうですが、クライアント/サーバ共に Clojure で統合的に開発できるようです。 実際には Javelin, Castra, Ring をまとめたメタフレームワークといったところでしょうか。
Javelinはリアクティブなデータフローを実現するライブラリです。!!!!!Spreadsheet-like
って書いてある!!!!!
Input Cells と Formula Cells を組み合わせて望む出力を手に入れるようです。
すごいほんとにスプレッドシートだ……
DerivableJS の Atom, Derivation みたいな感じでしょうか?
Castra はクライアント/サーバ間の通信をうまいことするライブラリですね。
README.md には RPC library
とあります。
Hoplon ではリソースを取得する処理を クライアントから呼び出し、Javelin の Cell につなぐことで、ロードが完了したら自動的に表示を更新する、といった使い方ができるようです。
こちらの図がわかりやすいですね。
あまり魔術っぽいことはしてませんが、後述する Relay や Falcor に近いポジションにも見えます。
ClojureScript の人たちは独自の生態系を築いてるな……
flux-challengeまとめ
当初の主眼であった「サーバーとの通信の絡む非同期Action」について、どの submission も解決策を提示できてないような気もしますね……。
要件で示された通信処理が簡単すぎたので、みんなfluxでスラスラ書けてしまう、というのもあるかもしれません。
とはいえ、submit 一覧は界隈の情勢がよく表れている結果になっていると思います。
データフェッチング
前述のとおり、Fluxアプリケーションでは非同期処理、特にサーバサイドとの通信をどこで行うかが問題となります。
非同期 Action を簡単に発行できる Redux ミドルウェアや Isomorphic な fetcher など、色々と工夫はなされていましたが、データ取得の仕方を根本から変えるようなアプローチは存在しませんでした。
最近ではこの領域でいくつかの解決策が提案されてきています。
本記事ではこの処理をデータフェッチングと呼びます。
(なんか流行ってる呼び方あるんでしょうか?)
ここでは Relay, Falcor について取り上げます。
Relay
Relay は React アプリケーションにおけるデータフェッチングの為のフレームワークです。
React.js Conf 2015 でアナウンスされ、2015-08-12 に実装が公開されました。
Relay では GraphQL という独自のクエリ言語を採用しており、データの取得を宣言的に書けるようになっています。
Reactコンポーネントを作成する際、ユーザーはそのコンポーネントが必要とするデータの構造を GraphQL で記述し、Relay.createContainer
に与えます。
Relay は受け取った GraphQL クエリを解析し、データの取得、変換を自動で行い、コンポーネントの props に渡します。
公式ページの最も簡単な例:
class Tea extends React.Component { render() { var {name, steepingTime} = this.props.tea; return ( <li key={name}> {name} (<em>{steepingTime} min</em>) </li> ); } } Tea = Relay.createContainer(Tea, { fragments: { tea: () => Relay.QL` fragment on Tea { name, steepingTime, } `, }, });
この例では、 name
steepingTime
を持つ tea
を取得し、Tea の props に渡されていますね。
データ取得中は this.props.tea === undefined
となるため、props の有無によってロード状態を判定できます。
このように、ユーザーは通信処理の詳細を書くことなく、必要な場所で必要なデータを取得できる、というのが Relay の特長です。
コンポーネントと GraphQL の定義を同じ場所で行うことで、必要なデータの構造が変わる度あちこちのファイルを修正する必要がなくなる、というメリットもあります。
僕としては、GraphQL に馴染めなかったり、データ構造や通信処理などの知識がView層に漏れているように感じられるなど、Relay のアプローチはまだしっくり来てません。
しかしよく考えると、元々View層は描画に必要なデータ構造を知っているはずですし、通信処理はRelayが勝手にやってくれるということで、それほど不自然ではないのかもしれません??
Falcor
Falcor は Netflix が公開しているデータフェッチングライブラリです。
Relay はアプリケーション全体の構成に関わるため framework
と呼ばれますが、Falcor のドキュメントには middleware
と書かれています。
公式で express や hapi と連携するためのパッケージも用意されておりちゃんとミドルウェア然としてる。
Falcor には Relay に似た概念が複数登場します。
Virtual JSON は、クライアント/サーバ間で統一的にデータを扱うための概念です。
必要となる全てのデータを単一の JSON として扱えるようにすることで、全てのリソースへのアクセスを Falcor から配信できるようになります。
複数のDBを利用するサービスの場合はこんなイメージらしいです:
クライアント/サーバを問わず、リソースを必要とする場所では Virtual JSON をラップする falcor.Model
を生成します。
これを通して Virtual JSON にアクセスすることで、どのノードからも同じ方法でリソースを取得することができます。
具体的なコード例*2は↓のようになります。
クライアント/コード共に model.getValue()
で値を取得しているのがわかるでしょうか。
// server var model = new falcor.Model({ cache : { user : { name : 'Frank', address : '1600 Pennsylvania Avenue, Washington, DC', } }, }); model.getValue('user.name') .then((name) => console.log(surname)); // client var model = new falcor.Model({ source : new falcor.HttpDataSource('/model.json'), }); model.getValue('user.name') .then((name) => console.log(surname));
先ほど「全てのデータをJSONとして扱う」と書きましたが、実際には JSON Graph という記法を用います。
JSON Graph は JSON に reference, atom, error という3つの型を追加したものです。
最も特徴的なのは reference でしょう。
同じ UUID を持つオブジェクトを複数箇所に書きたい場合、普通のJSONで書いて JSON.parse()
すると、同じ内容だけど別々のオブジェクトが生成されてしまいます。
そのため、データの更新を行うとき片方だけ更新されたりしてデータに不整合が起きやすくなってしまいます。
reference を使うと、同じオブジェクトを指す参照をいくらでも記述できます。
更新処理でも常にひとつのオブジェクトを更新すればよいため、不整合が起きなくなります。
// JSON Graph { todosById: { "44": { name: "get milk from corner store", done: false, prerequisites: [{ $type: "ref", value: ["todosById", 54] }] }, "54": { name: "withdraw money from ATM", done: false, prerequisites: [] } }, todos: [ { $type: "ref", value: ["todosById", 44] }, { $type: "ref", value: ["todosById", 54] } ] }
Falcor の Router はたったひとつのエンドポイントしか持ちません。
REST APIのように、リソースの種類毎に定められたエンドポイントを叩くのではなく、必要なリソースを全てURLクエリパラメータでリクエストするという方式です。
例えば、↑の JSON Graph から todos[0].name
と todos[0].done
を取得する時のリクエストURLは次のようになります:
/model.json?paths=[["todos",0,["name","done"]]]
Router が一つのエンドポイントしか持たないというのは Relay と似ていますね。
Relay では GraphQL でクエリを表現するのでした。
ここまでの説明ではなにがなんだかわからないかもしれません……
重要なのは、Falcor が Virtual JSON の構造を知っている事、及び全ての通信を Falcor に任せる、という事です。
これにより、Falcor はクエリを分析し、自動で Caching, Path Optimization, Batching を行い、無駄なリクエストが発生しないようにしてくれます。
このように、Falcor では Relay と同じく
- 柔軟なクエリを発行できる
- 通信の詳細を考える必要がない
といった特長を持っています。
フェッチングまとめ
両ライブラリの違いとしては、
- Relay : Reactアプリケーションを構築するためのフレームワーク
- Falcor : 任意のアプリケーションでクライアント/サーバ間のリソースを透過的に共有するためのライブラリ
というスタンスの違いが大きいと感じます。
この他にも、データフェッチングを目的としたプロダクトが登場してきているようです。
turbine は GraphQL を使いますが、普通の REST API 上でも使えるというライブラリです。 Relay よりとっつきやすそう。
Elide Elide : Simplify Your CRUD | Yahoo Engineering
Elide はつい先日 Yahoo が発表したライブラリです。
JSON-API でリクエストするデータの構造を指定できる、という感じ?
ブログでのアナウンスを見ると、GraphQL や Falcor と比べてユーザー認証の仕組みがしっかりしている、と書いてあります。
ただし、今のところサーバサイドで Java を使うことが前提になっているので、使える環境は限られるでしょう。
まとめ
元々 View ライブラリとして現れた React。
当初は Backbone 等のMV*フレームワークと組み合わせて用いられることも多かったですが、現在のReactコミュニティでは Flux による開発が一般的になりました。
2014年〜2015年のFlux戦争を経て、今なお勢いのあるフレームワークは
- Redux
- Fluxible
- Alt
の3つではないでしょうか。!!!!!!!!!個人の感想です!!!!!!!!
Redux はやはり最も派手!
皆が戦争に疲れてきた頃に出てきた、というのも大きかったと思います。
現在の Star 数は 11,000 超。
これからどこまでコミュニティが育っていくのが楽しみです。
Fluxible はブランドバリューだけでなく、ドキュメントやサーバサイドと連携するためのツールも充実しています。
仕事で使いたいという人にも勧めやすいですね。
Isomorphic なサービスを作るには最も向いてるのでは無いでしょうか。
学習コストが高いのがネックかな……。
Alt は……何でですかね??
多分みなさん自分でシンプルなオレオレFlux作ろうとしたら Alt か material-flux に似た感じになると思います。
最もこれまでのパラダイムと乖離が少ない???うーん????いや良いとは思うんだけど
2015年後半には、Flux の弱点であるサーバ側との通信や非同期処理について、様々なアプローチが出現しました。
Cycle.js を経て、Rxやら Clojure やら魔物っぽい人たちが勢いづいてたみたいですね。
また、クライアント/サーバ間でのリソースの共有を主眼においた Relay, Falcor などのデータフェッチングライブラリも登場しました。
現時点では正直どれも敷居が高いような気がしますね。
Relay がブランド力で押し切るのか、Falcor が普及するのか、それとも……?
来年が楽しみですね!!!!
それでは!!!!良いお年を!!!!!!!!!
-
ところで、Polymer コミュニティや Angular 界隈、Riot 周辺についてはどうなっているんでしょうか。
年末だし誰か書いてくれてるはず……??