material-flux を試す
React による Web アプリケーション開発で Flux に facebook/flux を採用していたけどシングルトンが扱いにくいので代替を検討した。
flummox
Flux ライブラリを採用するにあたり単純であることを重視。規模が小さく簡潔で、その気になれば自分で書き直せるようなものがよい。機能としては Store/Action だけ管理できれば十分である。
次に ES6 (これからは ES2015 と呼ぶべきだろうか?) に対応していること。ライブラリの提供する Store/Action を利用するにあたり Object.assign() などの Mix-In 機構ではなく ES6 class の継承で書きたい。
すこし前に以下の記事を読み、これらの条件を満たすものとして acdlite/flummox を採用するつもりだった。
しかし flummox は Store/Action と React コンポーネントを関連づけるため FluxComponent
という React コンポーネントを使う点がイマイチ。View 部分を React 以外 (dekujs/deku とか) へ変えたくなったとき困りそうだ。
Flux はデータフロー管理に徹してほしいので View ライブラリの依存は避けたい。というわけで却下。
material-flux
そもそも単純さを重視するなら自分で実装したほうがよくないか?というわけで設計を検討してみた。
しかしシングルトンを避けて Store/Action をシンプルに管理しようとすると babelify で ES6 を試すでも触れた material-flux みたいになった。ならばいっそこれを採用したほうがよいだろう。というわけで material-flux を試す。
material-flux は小規模で設計も簡潔。付属のサンプルと作者の解説記事を読むだけですぐに利用できるだろう。
問題があるとすれば Store/Action がシングルトンであることに依存していた箇所の修正である。また EventEmmiter
を直に使わず React コンポーネントのように setState
で更新通知する点も注意が必要。
以下のプロジェクトを facebook/flux から material-flux へ乗り換えたときの覚書をまとめる。
- facebook/flux による実装 examples-nw/audio-player at audio-player-v1.0.1-es6
- material-flux 乗り換え実装 examples-nw/audio-player at audio-player-v1.0.2-material-flux
提供されるクラス
material-flux の提供するクラスは以下の 3 種類。
- Action
- Store
- Context
Action と Store は他の Flux 実装でもお馴染みだが Context
は特徴的。これは dispatcher
をインスタンス化するもの。Action と Store も Context
継承クラス内で管理される。
Action
Action の実装は以下のようになる。
import { Action } from 'material-flux';
/**
* 音声プレーヤーの操作種別を定義します。
* @type {Object}
*/
export const Keys = {
play: Symbol( 'AudioPlayerAction.play' ),
pause: Symbol( 'AudioPlayerAction.pause' ),
stop: Symbol( 'AudioPlayerAction.stop' ),
seek: Symbol( 'AudioPlayerAction.seek' ),
volume: Symbol( 'AudioPlayerAction.volume' ),
unselect: Symbol( 'AudioPlayerAction.unselect' )
};
/**
* 音声プレーヤーを操作します。
*/
export default class AudioPlayerAction extends Action {
/**
* 音声を再生します。
*
* @param {Music} music 再生対象とする音楽情報。
*/
play( music ) {
this.dispatch( Keys.play, music );
}
// ...以下、略
}
Action はコンストラクタへ Context
が渡されて内部に参照が保存される。dispatcher
への Action 登録は dispatch
メソッド経由でおこなう。Action 識別子の定義はサンプルどおり Symbol
にした。この方法だと keyMirror に比べて単純な名前でも衝突を避けられて便利だ。
dispatch
メソッドは第一引数に Action 識別子、第二引数に可変長のパラメーターを指定。これは Store 側で同じ識別子に関連付けたメソッドの引数として渡される。プロパティ名を決める必要もなくパラメータの順番だけ気をつければよい。管理しやすい設計だ。
facebook/flux から乗り換える場合は Action 識別子を定義していた 〜Constans
系ファイルが不要になる。というか facebook/flux を利用する場合でも識別子は Action 系ファイル内に定義して export
するほうが分かりやすい。
Store
Store の実装は以下のようになる。
コメントにシングルトンとあるがこれは消し忘れ。実際には Context
内でインスタンス化される。サンプル プロジェクトの実装を見て混乱するかもしれないのでコメントがミスであることをここに明示しておく。
import { Store } from 'material-flux';
import { Keys } from '../actions/AudioPlayerAction.js';
import AudioPlayer from '../model/AudioPlayer.js';
/**
* 音声プレーヤーを操作します。
* このクラスはシングルトンとして実装されます。
*
* @type {AudioPlayerStore}
*/
export default class AudioPlayerStore extends Store {
/**
* インスタンスを初期化します。
*
* @param {Context} context コンテキスト。
* @param {MusicListStore} musicListStore 音楽リスト。
*/
constructor( context, musicListStore ) {
super( context );
this.register( Keys.play, this._actionPlay );
this.register( Keys.pause, this._actionPause );
this.register( Keys.stop, this._actionStop );
this.register( Keys.seek, this._actionSeek );
this.register( Keys.volume, this._actionVolume );
this.register( Keys.unselect, this._actionUnselect );
/**
* 変更監視される値。
* @type {Object}
*/
this.state = {
/**
* 再生対象となる音楽情報。
* @type {Music}
*/
current: null,
/**
* 再生状態。
* @type {PlayState}
*/
playState: PlayState.Stopped
};
// ... 以下、略
}
// ... 以下、略
}
Store もコンストラクタに Context
が渡される。Action との関連付けは register
メソッド経由で処理。第一引数に Action 識別子、第二引数に Action と関連付ける関数やメソッドを指定する。
複数 Store 間の依存は facebook/flux の dispatcher
的に waitFor
メソッドで管理可能。待機が不要ならこの例のようにコンストラクタで依存 Store の参照を受け取って保持してもよい。手抜きともいえるが。
もうひとつの特徴として state
がある。これは React コンポーネントと同様に監視対象とするデータを定義して setState
メソッドで更新すると変更通知が発生する。例えば playState
というプロパティが監視対象だとして、
this.setState( { playState: PlayState.Playing } );
のようにすると EventEmitter
による通知が発生する。プロパティを更新せずに通知だけ発生させたい場合は引数なしで実行すればよい。
this.setState();
material-flux のサンプルにはない用法だが Store クラスの実装をみると引数なしでもエラーにせず最後に this.emit('change')
していることを確認できる。
通知の購読は onChange
、解除は removeChangeListener
または removeAllChangeListeners
でおこなう。React.Component
継承クラスでは暗黙の bind(this)
が廃止された。そのため onChange
と removeChangeListener
へ同じインスタンスを渡すためには
// Store の館対象となる bind 済み Listener を一意にするためのフィールド
this.__onChangeAudioPlayer = this._onChangeAudioPlayer.bind( this );
のように bind(this)
したものを保存して渡す必要がある。こうした管理が面倒で Store に複数のコールバックを登録することがないなら removeAllChangeListeners
を利用するほうが楽。このメソッドはコールバックをすべて解除するため onChange
に登録したインスタンスとの整合を考えなくて済む。
facebook/flux から乗り換える場合は state/setState
関連の修正が大量に発生するため注意が必要。かわりに EventEmitter
関連の冗長な実装が不要となる。
データ更新と通知を React コンポーネントの state
風にしたのはよい設計。これらの処理はセットで実行されることが多いため記述が簡潔になる。引数なしの呼び出しで単純な通知になるのも、この用途で別メソッドを用意するよりスマートだ。
Context
Context
は Store/Action を結びつける。実装は以下のようになる。
import { Context } from 'material-flux';
import MusicListAction from './actions/MusicListAction.js';
import MusicListStore from './stores/MusicListStore.js';
import AudioPlayerAction from './actions/AudioPlayerAction.js';
import AudioPlayerStore from './stores/AudioPlayerStore.js';
/**
* アプリケーションを表します。
*/
export default class AppContext extends Context {
/**
* インスタンスを初期化します。
*/
constructor() {
super();
this.musicListAction = new MusicListAction( this );
this.musicListStore = new MusicListStore( this );
this.audioPlayerAction = new AudioPlayerAction( this );
this.audioPlayerStore = new AudioPlayerStore( this, this.musicListStore );
}
}
facebook/flux でシングルトンだったものが Context
内のインスタンスとして管理される。AudioPlayerStore
が MusicListStore
に依存することを考慮した順番でインスタンス化している。
Context の利用
いままで Action/Store を直に import
していた箇所を Context
に置き換える。まず Context
のインスタンスだが、これはどこで生成してもよい。Context
に含まれる Action/Store を必要とする箇所となるだろう。
管理が面倒ならアプリケーションのエントリー ポイントでインスタンス化して React コンポーネントへ props
経由で渡す。例えば以下のような感じ。
import AppContext from './AppContext.js';
import MainViewModel from './vm/MainViewModel.js';
let context = null;
window.onload = () => {
context = new AppContext();
React.render(
<MainViewModel context={ context } />,
document.querySelector( 'body' )
);
}
子コンポーネントに伝搬してゆくときは props
で渡してゆけばよい。こうすると React コンポーネントの受け取る外部データを props
へ統一しやすくなる。props にだけ依存するようにすればモックやスタブを渡しやすくなるため render
テストなどにも役立つだろう。
まとめ
facebook/flux からの移行は作業量が多くて苦労したが、それに見合う結果を得られたと感じる。シングルトンがなくなり処理の順序が明確になったことと Store の state
管理が特に気に入った。
実装が小規模なため不明な点をコードで確認しやすい点もよい。大きなフレームワークに乗るより、こうした小さなライブラリを組み合わせて実装するほうが好きだ。