Flux 架構入門教程

阮一峰發表於2016-01-15

過去一年中,前端技術大發展,最耀眼的明星就是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 的最大特點,就是資料的”單向流動”。

  1. 使用者訪問 View
  2. View 發出使用者的 Action
  3. Dispatcher 收到 Action,要求 Store 進行相應的更新
  4. Store 更新後,發出一個”change”事件
  5. 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》的啟發。

相關文章