你為什么應該試一試Reflux?
一點背景
React在設計之初就只關注在View本身上,其余部分如 數據的獲取 , 事件處理 等,全然不在考慮之內。不過構建大型的Web前端應用,這些點又實在不可避免。所以非死book的工程師提出了前端的 Flux 架構,這個架構的最大特點是 單向數據流 (后面詳述)。但是 Flux 本身的實現有很多不合理的地方,比如單例的Dispatcher會在系統中有多種事件時導致臃腫的 switch-cases 等。
這里是非死book官方提供的提供 Flux的結構圖 。
其實整個Flux背后的思想也 不是什么新東西 。在很久之前,Win32的消息機制(以及很多的GUI系統)就在使用這個模型,而且這也是一種被證實可以用來構建大型軟件的模型。
鑒于Flux本身只是一個架構,而且非死book提供的參考實現又有一些問題,所以社區有了很多版本的Flux實現。比如我們這里會用到的 Reflux 。
Reflux簡介
簡而言之, Reflux 里有兩個組件:Store和Action。Store負責和數據相關的內容:從服務器上獲取數據,并更新與其綁定的React組件(view controller);Action是一個事件的集合。Action和Store通過convention來連接起來。
具體來說,一個典型的過程是:
- 用戶的動作(或者定時器)在組件上觸發一個Action
- Reflux會調用對應的Store上的callback(自動觸發)
- 這個callback在執行結束之后,會顯式的觸發(trigger)一個數據
- 對應的組件(可能是多個)的state會被更新
- React組件檢測到state的變化后,會自動重繪自身 </ol>
- 一個用來
fetch數據,存儲數據的store (BookmarkStore) - 一個用來表達事件的
Action(BookmarkActions) - 一個列表組件(BookmarkList)
- 一個組件條目組件(Bookmark) </ol>
- 一個SearchBox組件
- 一個新的
Action:search BookmarkStore上的一個新方法onSearch- 組件
SearchBox需要和BookmarkActions關聯起來
</ol>
一個例子
我們這里將使用React/Reflux開發一個實際的例子,從最簡單的功能開始,逐步將其構建為一個較為復雜的應用。
這個應用是一個書簽展示應用(數據來源于我的Google Bookmarks)。第一個版本的界面是這樣的:
要構建這樣一個列表應用,我們需要這樣幾個部分:
定義Actions
var Reflux = require('reflux');
var BookmarkActions = Reflux.createActions([
'fetch'
]);
module.exports = BookmarkActions;</pre>
第一個版本,我們只需要定義一個 fetch 事件即可。然后在 Store 中編寫這個Action的回調:
定義Store
var $ = require('jquery');
var Reflux = require('reflux');
var BookmarkActions = require('../actions/bookmark-actions');
var Utils = require('../utils/fetch-client');
var BookmarkStore = Reflux.createStore({
listenables: [BookmarkActions],
init: function() {
this.onFetch();
},
onFetch: function() {
var self = this;
Utils.fetch('/bookmarks').then(function(bookmarks) {
self.trigger({
data: bookmarks,
match: ''
});
});
}
});
module.exports = BookmarkStore;</pre>
此處,我們使用 listenables: [BookmarkActions] 來將Store和Action關聯起來,根據 convention , on +事件名稱就是回調函數的名稱。這樣當 Action 被觸發的時候, onFetch 會被調用。當獲取到數據之后,這里會顯式的 trigger 一個數據。
List組件
var React = require('react');
var Reflux = require('reflux');
var BookmarkStore = require('../stores/bookmark-store.js');
var Bookmark = require('./bookmark.js');
var BookmarkList = React.createClass({
mixins: [Reflux.connect(BookmarkStore, 'bookmarks')],
getInitialState: function() {
return {
bookmarks: {data: []}
}
},
render: function() {
var list = [];
this.state.bookmarks.data.forEach(function(item) {
list.push(<Bookmark title={item.title} created={item.created}/>)
});
return <ul>
{list}
</ul>
}
});
module.exports = BookmarkList;</pre>
在組件中,我們通過 mixins: [Reflux.connect(BookmarkStore, 'bookmarks')] 將Store和組件關聯起來,這樣List組件 state 上的 bookmarks 就和 BookmarkStore 連接起來了。當 state.bookmarks 變化之后, render 方法就會被自動調用。
對于每一個書簽,就只是簡單的展示內容即可:
Bookmark組件
var React = require('react');
var Reflux = require('reflux');
var moment = require('moment');
var Bookmark = React.createClass({
render: function() {
var created = new Date(this.props.created * 1000);
var date = moment(created).format('YYYY-MM-DD');
return <li>
<div className='bookmark'>
<h5 className='title'>{this.props.title}</h5>
<span className='date'>Created @ {date}</span>
</div>
</li>;
}
});
module.exports = Bookmark;</pre>
這里我使用了 moment 庫將 unix timestamp 轉換為日期字符串,然后寫在頁面上。
最后, Utils 只是一個對 jQuery 的包裝:
var $ = require('jquery');
var Promise = require('promise');
module.exports = {
fetch: function(url) {
var promise = new Promise(function (resolve, reject) {
$.get(url).done(resolve).fail(reject);
});
return promise;
}
}</pre>
我們再來總結一下, BookmarkStore 在初始化的時候,顯式地調用了 onFetch ,這個動作會導致 BookmarkList 組件的 state 的更新,這個更新會導致 BookmarkList 的重繪, BookmarkList 會依次迭代所有的 Bookmark 。
更復雜一些
當然,Reflux的好處不僅僅是上面描述的這種單向數據流。當 Store , Actions 以及具體的 組件 被解耦之后,構建大型的應用才能成為可能。我們來對上面的應用做一下擴展:我們為列表添加一個搜索功能。
隨著用戶的鍵入,我們發送請求到服務器,將過濾后的數據渲染為新的列表。我們需要這樣幾個東西
為了讓用戶看到匹配的效果,我們需要將匹配到的關鍵字高亮起來,這樣我們需要在 Bookmark 組件中監聽 BookmarkStore ,當 BookmarkStore 發生變化之后,我們就可以即時修改書簽的 title 了。
搜索框組件
var React = require('react');
var BookmarkActions = require('../actions/bookmark-actions');
var SearchBox = React.createClass({
performSearch: function() {
var keyword = this.refs.keyword.value;
BookmarkActions.search(keyword);
},
render: function() {
return <div className="search">
<input type='text'
placeholder='type to search...'
ref="keyword"
onChange={this.performSearch} />
</div>
}
});
module.exports = SearchBox;</pre>
BookmarkStore
var $ = require('jquery');
var Reflux = require('reflux');
var BookmarkActions = require('../actions/bookmark-actions');
var Utils = require('../utils/fetch-client');
var BookmarkStore = Reflux.createStore({
listenables: [BookmarkActions],
init: function() {
this.onFetch();
},
onFetch: function() {
var self = this;
Utils.fetch('/bookmarks').then(function(bookmarks) {
self.trigger({
data: bookmarks,
match: ''
});
});
},
onSearch: function(keyword) {
var self = this;
Utils.fetch('/bookmarks?keyword='+keyword).then(function(bookmarks) {
self.trigger({
data: bookmarks,
match: keyword
});
});
}
});
module.exports = BookmarkStore;</pre>
我們在 BookmarkStore 中添加了 onSearch 方法,它會根據關鍵字來調用后臺API進行搜索,并將結果 trigger 出去。由于數據本身的結構并沒有變化(只是數量會由于過濾而變少),因此 BookmarkList 是無需修改的。
書簽高亮
當搜索匹配之后,我們可以將對應的關鍵字高亮起來,這時候我們需要修改 Bookmark 組件:
var React = require('react');
var Reflux = require('reflux');
var moment = require('moment');
var BookmarkStore = require('../stores/bookmark-store.js');
var Bookmark = React.createClass({
mixins: [Reflux.listenTo(BookmarkStore, 'onMatch')],
onMatch: function(data) {
this.setState({
match: data.match
});
},
getInitialState: function() {
return {
match: ''
}
},
render: function() {
var created = new Date(this.props.created * 1000);
var date = moment(created).format('YYYY-MM-DD');
var title = this.props.title;
if(this.state.match.length > 0) {
title = <span
dangerouslySetInnerHTML={{
__html : this.props.title.replace(new RegExp('('+this.state.match+')', "gi"), '<span class="highlight">$1</span>')
}} />
}
return <li>
<div className='bookmark'>
<h5 className='title'>{title}</h5>
<span className='date'>Created @ {date}</span>
</div>
</li>;
}
});
module.exports = Bookmark;</pre>
mixins: [Reflux.listenTo(BookmarkStore, 'onMatch')] 表示,我們需要監聽 BookmarkStore 的變化,當變化發生時,調用 OnMatch 方法。 OnMatch 會修改組件的 match 屬性,從而觸發 render 。
在 render 中,我們替換關鍵字為 <span class="highlight">$keyword</span> ,這樣就可以達到預期的效果了:
結論
從上面的例子可以看到,我們從一開始就引入了Reflux。雖然第一個版本和React原生的寫法差異并不是很大,但是當加入 SearchBox 功能之后,需要修改的地方非常清晰:添加 Actions ,在對應的 Store 中添加callback,然后在組件中使用。這種方法不僅可以最大程度的使用 React 的長處(diff render),而且使得代碼邏輯變得較為清晰。
隨著業務代碼的不斷增加,Reflux提供的方式確實可以在一定程度上控制代碼的復雜性和可讀性。
完整的 代碼地址在這里 。
其他參考
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!