この記事は2016年11月21日に社内ブログに掲載したものの公開版となります。
ソースコードは以下のリポジトリにアップロードしています。
Flux概要
Facebookが提唱したUI構築用のアーキテクチャ
- 特定のライブラリや実装ではない
Reactとの併用が推奨されている
- React.jsはMVCモデルのV(View)の部分の役割に特化している
データが単方向に流れる
fluxのメリット
イベント発生に合わせてどんな処理をどこの部品として実装すべきかが明確 → コードの見通しが良くなる
非同期イベントに伴うデータの流れが明確になる → デバッグが容易になる
各部品は疎結合かつ入出力が明確 → 再利用性やテスト容易性を確保できる
Rubyで実装してみる
- アーキテクチャであれば言語やフレームワークに依存しないはず
- 作るもの: CUIのカウンター
- 1ファイルにまとめると以下のようなコード
- これぐらいの規模だとわざわざfluxを採用する意味はないが、学習用として目をつぶる
count = 0 print 'input: ' while op = gets.chomp case op when '+' count += 1 when '-' count -= 1 end puts count print 'input: ' end
リポジトリ
https://github.com/kuranari/flux-ruby
EventEmitter
- いわゆるオブザーバーパターン
- イベントを登録しておくと、特定のイベントが発火したときにコールバックが実行される
class EventEmitter def initialize @handlers = Hash.new { |h, k| h[k] = [] } end def on(type, &handler) @handlers[type] << handler end def off(type) @handlers[type].clear end def emit(type, data = nil) @handlers[type].each do |handler| handler.call(data) end end end
require './event_emitter' event_emitter = EventEmitter.new event_emitter.on('click') do puts 'hello world' end event_emitter.on('input') do |params| puts params end event_emitter.emit('click') # => hello world event_emitter.emit('input', 42) # => 42
Dispatcher
Action → Dispatcher → Store ActionをStoreに配送する。
実装
EventEmitterそのものをDispatcherとして使用。 実際に使う場合は実行順序の制御機構などもう少し複雑になる。
require './event_emitter' class Dispatcher < EventEmitter end
Store
Dispatcher → Store → View - MVCでいうModelに相当。 - Storeが管理するデータはStore自身のみが更新できる - Storeの状態に変更があった場合EventEmitter経由でStoreに通知
require './event_emitter' class Store < EventEmitter attr_reader :count def initialize(dispatcher) super() @count = 0 dispatcher.on('UPDATE_COUNTER') do |payload| on_update_counter(payload[:value]) end end private def on_update_counter(count) @count += count emit('CHANGE') end end
ActionCreator
View → Action → Dispatcher - MVCでいうControllerに相当
Dispatcherに対しActionを作成して配信する。
Actionは
UPDATE_COUNTER
のようなアクションの識別子{value: 1}
など入力値(パラメータ)に相当するオブジェクト
のペアで構成される。
class ActionCreator def initialize(dispatcher) @dispatcher = dispatcher end def increment_counter @dispatcher.emit('UPDATE_COUNTER', value: 1) end def decrement_counter @dispatcher.emit('UPDATE_COUNTER', value: -1) end end
View(Component)
Store → View → Action
- ユーザーイベントの受付
- clickされたら◯◯するといった動作
- イベントハンドラで対応するActionを呼ぶ
- Storeの変更を検知して再描画
簡易版ReactComponent
state=
で状態が変わったら再度render
する。
雑に実装すると、ReactComponentがやってることは以下のようなことだと思う。
class ReactComponent def initialize @state = {} end # stateに変更があったら再描画 def state=(state) @state = state render end def render raise NotImplementedError.new end end
class Component < ReactComponent def initialize(store, action) @store = store @action = action @state = { count: @store.count } @store.on('CHANGE') do on_change end end def increment @action.increment_counter end def decrement @action.decrement_counter end private def on_change self.state = { count: @store.count } end def render puts "count: #{@state[:count]}" end end
Main
require './event_emitter' require './action_creator' require './store' require './component' dispatcher = EventEmitter.new store = Store.new(dispatcher) action = ActionCreator.new(dispatcher) component = Component.new(store, action) print 'input: ' while command = gets.chomp case command when '+' component.increment when '-' component.decrement end print 'input: ' end
イベントの末端にスタックトレースを仕込んでみる
Component#on_change
まとめ
- Store, Action, Viewを各々EventEmitter(Dispatcher)で監視することでユーザーの入力から画面表示までの1サイクルが回ることを確認した。
- fluxを理解するには実装するのが一番早いと思う(普通にjsで実装すればいいと思うが…)
参考文献
公式ドキュメント http://facebook.github.io/flux/
10分で実装するFlux http://azu.github.io/slide/react-meetup/flux.html
WEB+DB PRESS Vol.87, Emerging Web Technology研究室 【第13回】Flux ……フロントエンド開発の新しいアーキテクチャ http://gihyo.jp/magazine/wdpress/archive/2015/vol87