轉載: http://www.ruanyifeng.com/blog/2016/01/flux.html
過去一年中,前端技術大發展,最耀眼的明星就是React。
React 本身只涉及UI層,如果搭建大型應用,必須搭配一個前端框架。也就是說,你至少要學兩樣東西,才能基本滿足需要:React + 前端框架。
Facebook官方使用的是 Flux 框架。本文就介紹如何在 React 的基礎上,使用 Flux 組織代碼和安排內部邏輯,使得你的應用更易於開發和維護。
閱讀本文之前,我假設你已經掌握了 React 。如果還沒有,可以先看我寫的《React入門教程》。與以前一樣,本文的目標是使用最簡單的語言、最好懂的例子,讓你一看就會。
一、Flux 是什么?
簡單說,Flux 是一種架構思想,專門解決軟件的結構問題。它跟MVC 架構是同一類東西,但是更加簡單和清晰。
Flux存在多種實現(至少15種),本文采用的是Facebook官方實現。
二、安裝 Demo
為了便於講解,我寫了一個Demo。
請先安裝一下。
$ git clone https://github.com/ruanyf/extremely-simple-flux-demo.git $ cd extremely-simple-flux-demo && npm install $ npm start
然后,訪問 http://127.0.0.1:8080 。
你會看到一個按鈕。這就是我們的Demo。
三、基本概念
講解代碼之前,你需要知道一些 Flux 的基本概念。
首先,Flux將一個應用分成四個部分。
- View: 視圖層
- Action(動作):視圖層發出的消息(比如mouseClick)
- Dispatcher(派發器):用來接收Actions、執行回調函數
- Store(數據層):用來存放應用的狀態,一旦發生變動,就提醒Views要更新頁面
Flux 的最大特點,就是數據的"單向流動"。
- 用戶訪問 View
- View 發出用戶的 Action
- Dispatcher 收到 Action,要求 Store 進行相應的更新
- Store 更新后,發出一個"change"事件
- View 收到"change"事件后,更新頁面
上面過程中,數據總是"單向流動",任何相鄰的部分都不會發生數據的"雙向流動"。這保證了流程的清晰。
讀到這里,你可能感到一頭霧水,OK,這是正常的。接下來,我會詳細講解每一步。
四、View(第一部分)
請打開 Demo 的首頁index.jsx
,你會看到只加載了一個組件。
// index.jsx var React = require('react'); var ReactDOM = require('react-dom'); var MyButtonController = require('./components/MyButtonController'); ReactDOM.render( <MyButtonController/>, document.querySelector('#example') );
上面代碼中,你可能注意到了,組件的名字不是 MyButton
,而是 MyButtonController
。這是為什么?
這里,我采用的是 React 的 controller view 模式。"controller view"組件只用來保存狀態,然后將其轉發給子組件。MyButtonController
的源碼很簡單。
// components/MyButtonController.jsx var React = require('react'); var ButtonActions = require('../actions/ButtonActions'); var MyButton = require('./MyButton'); var MyButtonController = React.createClass({ createNewItem: function (event) { ButtonActions.addNewItem('new item'); }, render: function() { return <MyButton onClick={this.createNewItem} />; } }); module.exports = MyButtonController;
上面代碼中,MyButtonController
將參數傳給子組件MyButton
。后者的源碼甚至更簡單。
// components/MyButton.jsx var React = require('react'); var MyButton = function(props) { return <div> <button onClick={props.onClick}>New Item</button> </div>; }; module.exports = MyButton;
上面代碼中,你可以看到MyButton
是一個純組件(即不含有任何狀態),從而方便了測試和復用。這就是"controll view"模式的最大優點。
MyButton
只有一個邏輯,就是一旦用戶點擊,就調用this.createNewItem
方法,向Dispatcher發出一個Action。
// components/MyButtonController.jsx // ... createNewItem: function (event) { ButtonActions.addNewItem('new item'); }
上面代碼中,調用createNewItem
方法,會觸發名為addNewItem
的Action。
五、Action
每個Action都是一個對象,包含一個actionType
屬性(說明動作的類型)和一些其他屬性(用來傳遞數據)。
在這個Demo里面,ButtonActions
對象用於存放所有的Action。
// actions/ButtonActions.js var AppDispatcher = require('../dispatcher/AppDispatcher'); var ButtonActions = { addNewItem: function (text) { AppDispatcher.dispatch({ actionType: 'ADD_NEW_ITEM', text: text }); }, };
上面代碼中,ButtonActions.addNewItem
方法使用AppDispatcher
,把動作ADD_NEW_ITEM
派發到Store。
六、Dispatcher
Dispatcher 的作用是將 Action 派發到 Store、。你可以把它看作一個路由器,負責在 View 和 Store 之間,建立 Action 的正確傳遞路線。注意,Dispatcher 只能有一個,而且是全局的。
Facebook官方的 Dispatcher 實現輸出一個類,你要寫一個AppDispatcher.js
,生成 Dispatcher 實例。
// dispatcher/AppDispatcher.js var Dispatcher = require('flux').Dispatcher; module.exports = new Dispatcher();
AppDispatcher.register()
方法用來登記各種Action的回調函數。
// dispatcher/AppDispatcher.js var ListStore = require('../stores/ListStore'); AppDispatcher.register(function (action) { switch(action.actionType) { case 'ADD_NEW_ITEM': ListStore.addNewItemHandler(action.text); ListStore.emitChange(); break; default: // no op } })
上面代碼中,Dispatcher收到ADD_NEW_ITEM
動作,就會執行回調函數,對ListStore
進行操作。
記住,Dispatcher 只用來派發 Action,不應該有其他邏輯。
七、Store
Store 保存整個應用的狀態。它的角色有點像 MVC 架構之中的Model 。
在我們的 Demo 中,有一個ListStore
,所有數據都存放在那里。
// stores/ListStore.js var ListStore = { items: [], getAll: function() { return this.items; }, addNewItemHandler: function (text) { this.items.push(text); }, emitChange: function () { this.emit('change'); } }; module.exports = ListStore;
上面代碼中,ListStore.items
用來保存條目,ListStore.getAll()
用來讀取所有條目,ListStore.emitChange()
用來發出一個"change"事件。
由於 Store 需要在變動后向 View 發送"change"事件,因此它必須實現事件接口。
// stores/ListStore.js var EventEmitter = require('events').EventEmitter; var assign = require('object-assign'); var ListStore = assign({}, EventEmitter.prototype, { items: [], getAll: function () { return this.items; }, addNewItemHandler: function (text) { this.items.push(text); }, emitChange: function () { this.emit('change'); }, addChangeListener: function(callback) { this.on('change', callback); }, removeChangeListener: function(callback) { this.removeListener('change', callback); } });
上面代碼中,ListStore
繼承了EventEmitter.prototype
,因此就能使用ListStore.on()
和ListStore.emit()
,來監聽和觸發事件了。
Store 更新后(this.addNewItemHandler()
)發出事件(this.emitChange()
),表明狀態已經改變。 View 監聽到這個事件,就可以查詢新的狀態,更新頁面了。
八、View (第二部分)
現在,我們再回過頭來修改 View ,讓它監聽 Store 的 change
事件。
// components/MyButtonController.jsx var React = require('react'); var ListStore = require('../stores/ListStore'); var ButtonActions = require('../actions/ButtonActions'); var MyButton = require('./MyButton'); var MyButtonController = React.createClass({ getInitialState: function () { return { items: ListStore.getAll() }; }, componentDidMount: function() { ListStore.addChangeListener(this._onChange); }, componentWillUnmount: function() { ListStore.removeChangeListener(this._onChange); }, _onChange: function () { this.setState({ items: ListStore.getAll() }); }, createNewItem: function (event) { ButtonActions.addNewItem('new item'); }, render: function() { return <MyButton items={this.state.items} onClick={this.createNewItem} />; } });
上面代碼中,你可以看到當MyButtonController
發現 Store 發出 change
事件,就會調用 this._onChange
更新組件狀態,從而觸發重新渲染。
// components/MyButton.jsx var React = require('react'); var MyButton = function(props) { var items = props.items; var itemHtml = items.map(function (listItem, i) { return <li key={i}>{listItem}</li>; }); return <div> <ul>{itemHtml}</ul> <button onClick={props.onClick}>New Item</button> </div>; }; module.exports = MyButton;
九、致謝
本文受到了Andrew Ray 的文章《Flux For Stupid People》的啟發。
(完)