処理の流れは一方向で
Flux
React.jsの開発元のFacebookがFluxというアーキテクチャ概念を生み出しました。
アーキテクチャといえばMVC(Model、View、Controler)が有名です。
FluxはMVCで発生する処理の役割を明確化して流れをわかりやすく、シンプルにしたもののようです。
本家サイト( https://facebook.github.io/flux/docs/overview.html )でも図が提示されていますが、
図を見てお分かりの通り、一方通行になっています。
一方通行とすることで、例えばViewはViewの役割に集中できるであるとか、ソースが複雑化するのを防ぐメリットがあるらしいです。
本家サイトの図だと、WEB APIはActionで呼ぶとなっているのですが、特にこれじゃなきゃダメ、というわけではないようです。
データをPOSTするときはその前にバリデーションかけて、NGだったらPOSTしない、とかのフローを考えるとStoreでPOSTしたほうが都合が良い、じゃあActionで呼ぶのはGETしかないかなー、だったら、Storeに統一しよう!ということで、WEB APIは全てStoreから呼ぶことで整理しました。
ただし、本来Store自体は状態の保持しかしないシンプルなものになるべきらしいので、Store自体に直接あれこれゴリゴリ書くのは宜しくないようです。
なお、React.jsの場合、コンポーネントの中でイベントの定義やらhtmlの定義やらを行いますので、本格的なサービスを実装する場合は、Fluxを導入することでViewであるjsxファイルの記述量が結果的に減るという開発者的なメリットもあります。
redux( https://github.com/rackt/redux )など、
fluxアーキテクチャのライブラリはいろいろ出ているみたいですので、基本が分かったら、他のも実践してみると良いかもしれません。
実践
とりあえず基本を実践、ということで一番シンプルなfluxライブラリと、そのほか必要なライブラリも一緒にインストールをします。
$ npm install flux object-assign --save
今回はユーザーリスト画面をfluxに対応させてみます。
修正するソースはuserbox.jsxになります。
userbox.jsxで発生する主要なイベントは、
- ユーザー一覧の表示(UserBox.getUsers)
- ユーザー登録(UserForm.handleSubmit、UserBox.handleAddUser)
になります。この2つのイベントをfluxに対応させてみましょう。
Action
View(Reactコンポーネント)でイベントが発生した際に、まず実行されるのがActionクラスになります。Actionクラスで実行したい処理名を指定します。
client/scripts/actions/以下に、
userActions.jsというファイルを作成します。
var Dispatcher = require('../dispatchers/dispatcher');
var userActions = {
//ユーザー一覧の取得
load: function(target){
Dispatcher.handleServerAction({
type: 'load',
target: target
});
},
//ユーザーの登録
register: function (target) {
Dispatcher.handleServerAction({
type: 'register',
target: target
});
}
};
module.exports = userActions;
一行目に書いてあるDispatcherのインポートは次で説明します。
loadUsersとregisterが定義されています。で、それぞれでDispatcherのイベントを呼んでいます。
typeにはStoreクラスで実行したい処理を指定します。
targetにはStoreクラスに渡したい引数を指定します。
Dispatcher
次にDispatcherクラスを作成しましょう。
client/scripts/dispatchers/以下に、
dispatcher.jsというファイルを作成します。
var Dispatcher = require('flux').Dispatcher;
var assign = require('object-assign');
var appDispatcher = assign(new Dispatcher(), {
handleServerAction: function (action) {
this.dispatch({
source: 'server',
action: action
})
},
handleViewAction: function (action) {
this.dispatch({
source: 'view',
action: action
});
}
});
module.exports = appDispatcher;
object-assignは複数のobjectを結合するためのライブラリです。
今回でいうと、fluxライブラリのDispatcherに、新たに定義するhandleServerAction、handleViewActionを含ませてあげることで、ActionクラスからStoreへdispatchできるようにしている、ということですね。
Dispatcherの役割はさまざまなActionを目的のStoreへ伝播することなので、基本的に上の一ファイルしか作成する必要がありません。今回はクライアントサイドだけで完結する処理と、サーバー側でなんらかが行われる処理とを見た目で区別できるように2つ定義しました。
Store
次にStoreクラスを作成します。
client/scripts/stores/以下に、
userStore.jsというファイルを作成します。
var React = require('react');
var ajax = require('./storeUtils').ajax;
var Dispatcher = require('../dispatchers/dispatcher');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var userStore = assign({}, EventEmitter.prototype, {
data: {userData: []},
addLoadListener: function (callback) {
this.on('load', callback);
},
removeLoadListener: function (callback) {
this.removeListener('load', callback);
},
addRegisterListener: function (callback) {
this.on('register', callback);
},
removeRegisterListener: function (callback) {
this.removeListener('register', callback);
},
getAjaxResult: function(){
return userStore.data;
}
});
まずuserStoreそのものの定義をしています。
EventEmitterはあるイベントが発生したら、別のイベントを実行したい、という時に使えるライブラリです。
「別のイベント」を実行するには、emit()を実行することにより実現するようになっているため、条件によっては「別のイベント」を実行させない、ということもできます。
EventEmitterについては、以下が参考になりました。
http://befool.co.jp/blog/chainzhang/using-node-event-emmiter/
StoreからViewに結果を返すには、EventEmitterで紐付けたコールバックイベントを使うようになっています。addLoadListenerの、
this.on('load', callback);
は、
loadイベントとcallbackファンクションを紐付ける、ということですが、callbackファンクションはViewから指定することになっています。ここはまた後で触れます。
removeLoadListenerは、逆に紐付けを解除します。
getAjaxResultはView側から状態(ユーザー一覧)を取得できるように定義しておきます。
次にバリデーションメソッドです。
//バリデーション
var validation = function(target){
if (!target.name){
alert('名前を入力してください');
return false;
}
if (!target.mail){
alert('メールアドレスを入力してください');
return false;
}
return true;
};
これはPOST前の入力チェックについて記述しています。入力エラーがあったらfalseを返します。
最後に、Dispatcherから伝播されてきたイベント処理を実際に行うための定義を記述します。
userStore.dispatchToken = Dispatcher.register(function (payload) {
var registerCallback = function(err, res){
return callback(err, res, 'register');
}
var loadCallback = function(err, res){
return callback(err, res, 'load');
}
var callback = function(err, res, name){
if (err){
alert(res.text);
return;
}
userStore.data = {userData: JSON.parse(res.text)};
userStore.emit(name);
}.bind(userStore);
var actions = {
load: function (payload) {
//ajax通信する
ajax.get("/get_users", {}, loadCallback);
},
register: function (payload) {
if (!validation(payload.action.target)){
return;
}
//ajax通信する
ajax.post("/post_user", payload.action.target, registerCallback);
}
};
actions[payload.action.type] && actions[payload.action.type](payload);
});
module.exports = userStore;
Actionクラスの各メソッドで指定した、typeに対応したメソッドを実行するように記述しています。例えば、Actionの、
load: function(target){
Dispatcher.handleServerAction({
type: 'load',
target: target
});
},
は、Storeの
load: function (payload) {
//ajax通信する
ajax.get("/get_users", {}, loadCallback);
},
に対応していることになります。
なお、
var callback = function(err, res, name){
if (err){
alert(res.text);
return;
}
userStore.data = {userData: JSON.parse(res.text)};
userStore.emit(name);
}.bind(userStore);
のcallbackについてですが、サーバ通信の結果、エラーがあった場合はメッセージ表示の後、returnされます。EventEmitterはemit()を実行することにより、紐付けたイベントが実行されますので、上記の場合、エラーだった場合はnameのイベントに紐付けたイベントは実行されないことになります。
同様に、registerのvalidationの結果がfalseだった場合も、emit()は実行されることがないため、registerに紐付けたイベントは実行されません。
ajaxの部分は通信部分を共通化して切り出しただけですので、割愛します。
View
最後にViewである、userbox.jsxを修正します。
修正する箇所は、主に、
・ユーザー一覧データの取得
・ユーザー登録
に関してでした。
まずは必要なライブラリとして、userActions.js、userStore.jsがあります。
var React = require('react');
var ReactDOM = require('react-dom');
var UserActions = require('../actions/userActions');
var UserStore = require('../stores/userStore');
サーバー通信はStoreクラスに持って行ったのでsuperagentはここでは不要になりました。
次に、Storeで保持している状態(ユーザー一覧)を取得するメソッドgetUserStoreStatesを定義し、userBox作成時(getInitialState)、ユーザーリストの表示更新時(onViewUsers)に呼ばれるようにします。
//Storeで保持している状態を取得する
var getUserStoreStates = function(){
return UserStore.getAjaxResult();
};
//フォームとリストを一つにしたもの
var UserBox = React.createClass({
getInitialState:function(){
return getUserStoreStates();
},
:
:
onViewUsers:function(){
//ユーザー一覧データをセット
this.setState(getUserStoreStates());
},
onUpdatedUser:function(){
//更新が成功したらクリアする
ReactDOM.findDOMNode(this.refs.userform.refs.name).value = "";
ReactDOM.findDOMNode(this.refs.userform.refs.mail).value = "";
this.onViewUsers();
},
ユーザーの登録が成功した際に実行される処理としてonUpdatedUserを記述します。
次にStoreで定義したイベントと上記のイベントを紐付けます。EventEmitterの件です。
componentWillMount:function(){
UserStore.addLoadListener(this.onViewUsers);
UserStore.addRegisterListener(this.onUpdatedUser);
},
componentWillUnmount:function(){
UserStore.removeLoadListener(this.onViewUsers);
UserStore.removeRegisterListener(this.onUpdatedUser);
},
これで、Store側でemit(name)を実行すると、nameに応じたイベントが実行されるようになります。
次に、UserFormで追加ボタンをクリックした時の処理、UserBoxが画面表示された時の処理についてですが、UserActionsから目的のイベントがDispatcherに渡されるように、対応したメソッドを実行します。
handleAddUser:function(name, mail){
//ユーザーを登録
UserActions.register({name: name, mail: mail});
},
componentDidMount:function(){
//ユーザー一覧データを取得
UserActions.load();
},
render:function(){
return(
<div style={{width:"300px"}}>
<UserForm addUser={this.handleAddUser} ref="userform"/>
<hr/>
<UserList userData={this.state.userData}/>
</div>
);
}
});
onUpdatedUserでUserFormの名前やメールアドレスをクリアしたいため、UserFormにrefを追記しました。
UserList、Userについては変更ありません。
UserFormについても、handleSubmitの内容が若干変わった以外は変更ありません。
//ユーザーの入力フォームを定義
var UserForm = React.createClass({
propTypes:{
addUser:React.PropTypes.func.isRequired
},
handleSubmit:function(){
var name = ReactDOM.findDOMNode(this.refs.name).value.trim();
var mail = ReactDOM.findDOMNode(this.refs.mail).value.trim();
this.props.addUser(name, mail);
},
バリデーションの部分とデータクリアの部分が不要になりました。
以上により、ViewではActionとStoreを利用することでサーバ通信に関する記述やデータの状態をチェックする記述がなくなり、主にデータの表示についてだけ記述すれば良くなったので若干スッキリしました。
サンプルソース