Reactjs - Bài 18 - Kiến trúc Flux

Home page:
http://facebook.github.io/flux/
Overview:
“Flux is the application architecture that Facebook uses for building client-side web applications. It complements React’s composable view components by utilizing a unidirectional data flow. It’s more of a pattern rather than a formal framework, and you can start using Flux immediately without a lot of new code.”
Có 3 ý chính:
- Flux là architecture hơn là một formal framework.
- Sử dụng cho việc xây dựng client-side web applications.
- Nó bổ sung khả năng kết hợp các view components của React bằng việc sử dụng một unidirectional data flow
Kiến trúc Flux trên Homepage Flux:
Flux Architech
Như hình minh họa trên, tôi nêu sơ lược các phần trong Flux, còn cụ thể như thế nào thì bạn phải bắt tay vào code thì mới hiểu rõ nhất.
  • View : là các React Component.
  • Store: là nơi chứa, xử lý dữ liệu cho App.
  • Action: là nơi khởi tạo action.
  • Dispatcher: là nơi phát action.
Bây giờ, chúng ta sẽ bắt đầu tìm hiểu về Flux thông qua ví dụ mà chúng ta đã sử dụng. Tôi sẽ đi theo hướng là thay đổi lần lần cách thức hoạt động cũ thành Flux, cho các bạn dễ dàng nhận ra sự khác biệt.
Bạn có thể clone source code trên Github theo link bên dưới, chuyển sang nhánh flux, và chọn từng node commit để theo dõi theo trình tự mà tôi sẽ trình bày sau.
https://github.com/phungnc/reactjs-tut-demo/tree/feature/flux/flux-client

Store - (Static) View

Khoan đi vào Actions, Dispatcher, trước mắt chúng ta thử xem mối quan hệ giữa View và Store.
Chia phần ứng dụng thành các file:
- Member.react.js đóng vai trò như là 1 Item View
- MemberApp.react.js đóng vai trò như là 1 List Item View
- app.js đóng vai trò như là root app, sẽ chứa MemberApp
- index.html vẫn như cũ
- MemberStore.js đóng vai trò như model data của Member.
js
|_index.html
|_app.js
|_stores
| |_ MemberStore.js
|
|_views
| |_ Member.react.js
| |_ MemberApp.react.js
MemberStore.js
// MemberMemberStore.jsjs
var EventEmitter = require('events').EventEmitter;

var _members = {
      members: [
        {id:1, name: "Avatar 1", like: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
        {id:2, name: "Avatar 2", like: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
        {id:3, name: "Avatar 3", like: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
      ]
};

class MemberStore extends EventEmitter {
  constructor() {
    super();

  }
  getMembers() {
    return _members;
  }
}
module.exports = new MemberStore();
Ở đây MemberStore chỉ đơn thuần là một Object có phương thức getMembers() trả về dữ liệu members
Ở file MemberApp.react.js, ta chỉ cần thay đổi nội dung trả về từ members bằng việc trả về kết quả từ phương thức getMembers() từ MemberStore. Tất nhiên để làm được chuyện này ta phải require MemberStoreMemberApp.react.js
Với source này tôi có đưa syntax ES6 vào cho nên chúng ta cần babelify kết hợp với browserify.
Rồi dùng command như sau để bundle up file bundle.js
browserify app.js -t babelify -o bundle.js
Sau đó bạn thử open file index.html, thử thao tác các button, ¯_(ツ)_/¯ mọi thứ vẫn như xưa. Như vậy chúng ta đã xong việc lấy dữ liệu từ Store để hiển thị lên View.

View - Actions

Phần này ta sẽ đi vào mối quan hệ giữa View và Action.
Trong kiến trúc Flux, Action đóng vai trò tiếp nhận action từ View vào tạo action cho App.

Thử với delete action - xây dựng mối quan hệ

Thêm file MemberActions.js trong thư mục action. Đầu tiên ta chỉ cần có một destroy action:
module.exports = {
  destroy: id => {
    alert ("Destroy " + id);
  }
};
Trong file, Member.react.js, import file MemberActions.js
var MemberActionCreators = require('../actions/MemberActions');
Nhờ vậy Member.react.js có thể gọi method destroy từ MemberActions.js.
 _onDelete() {
   MemberActions.destroy(this.props.id);
 },

Actions and Action Creators

Ở trên, ta đã xây dựng được mối quan hệ giữa View và Actions: hễ User click vào button delete ở View thì Actions có nhiệm vụ alert dòng chữ “Destroy {id}” với id là dữ liệu được truyền từ View thông qua method MemberActions.destroy();. Chúng ta thử phân tích về thông tin mà nó alert lên:
  • Destroy: là một loại action hay Action Type
  • id: là data mà View truyền tới.
2 yếu tố trên tạo thành 1 action. Chúng ta có thể xem 1 Action = {Action Type, Data}.
Thực ra nếu bạn đã quen thuộc với khái niệm Event thì chắc hẳn sẽ thấy Action rất giống Event.
Action Creators đóng vai trò như một handling giữa Actions và Dispatcher. Tập hợp các Actions Creator sẽ tạo thành một module chứa các API mà ta sẽ dễ dàng sử dụng sau này.
  destroy: id => {
    AppDispatcher.dispatch({
      type: MemberConstants.MEMBER_DESTROY, 
      id: id
    });
  }
{
  type: MemberConstants.MEMBER_DESTROY, 
  id: id
}
là một Action, việc gắn Action với Dispatcher ta gọi là Action Creator.

Dispatcher

Dispatcher đóng vai trò như một bộ phát tín hiệu, phát đi Actions mà nó đã được ‘gắn’ vào.

View - Actions - Dispatcher - Store (- View)

Dispatcher - Store

Dispatcher sẽ phát ‘tín hiệu Actions’ tới Store.
(View thông qua Action Creators để tạo Action và nhờ Dispatcher phát Action này đến Store). Store lúc này sẽ làm nhiệm vụ phân loại Action để đưa ra xử lý, phần xử lý này sẽ nằm trong method constructor():
  constructor() {
    super();
    _dispatchToken = AppDispatcher.register(action => {
      switch(action.type) {

        case MemberConstants.MEMBER_DESTROY:
          alert(action.type + ' ' +action.id);
          break;
      }
    });
  }
Để Dispatcher có thể phát Action Type, ta cần Contants để chứa Action Type đó.
MemberConstants.js
module.exports = {
  MEMBER_DESTROY : 'MEMBER_DESTROY'
};
Hãy thử chạy souce code, bây giờ MemberStore đã nhận được tín hiệu Action bao gồm Action Type và Action Data từ Dispatcher.

Store - View

OK, chúng ta đã thấy “dòng” Action từ View đến Store. Như đã giới thiệu ở trên, Store đóng vai trò quản lý dữ liệu của app, mọi thay đổi dữ liệu của App đều phải được xử lý ở Store. Và dĩ nhiên sự thay đổi dữ liệu này sẽ không có ý nghĩa với User nếu nó không phản ánh lên UI. Do đó chúng ta phải phản ánh Action này lên View: khi user delete Member Item thì Item này sẽ bị remove. Việc này bao gồm 3 bước:
1. Remove dữ liệu ở Store.
2. Phát sự kiện thay đổi từ Store.
3. Bắt sự kiện và thực hiện việc remove Item ở View

1. Remove dữ liệu ở Store:

Dữ liệu ở Store là Collection _members.
Method xóa một Item:
function destroy(id) {
  let memberIndex = _members.findIndex(member => member.id == id);
  delete _members[memberIndex];
};
Ở đây tôi dùng ES6 polyfill array.prototype.findindex để tìm Item cần remove ra khỏi Array _member.
Và tại nơi xử lý loại action, ta sẽ gọi method detroy() này
switch(action.type) {  
  case MemberConstants.MEMBER_DESTROY:
    destroy(action.id);
    break;
}

2. Phát sự kiện thay đổi từ Store.

Sau khi đã thực hiện xong việc remove Item ra khỏi Array, ta sẽ phát sự kiện thay đổi này:
switch(action.type) {  
  case MemberConstants.MEMBER_DESTROY:
    destroy(action.id);
    this.emit('change');
    break;
}

3. Bắt sự kiện và thực hiện việc remove Item ở View

  • Bắt sự kiện:
Việc thiết lập bắt sự kiện này sẽ được thực hiện sau khi component đã được render.
componentDidMount() {
  MemberStore.on('change',this._onChange);
}
Khi có sự kiện ‘change’ thì nó sẽ gọi handler _onChange (sẽ nói sau) để thực hiện việc update thay đổi đó.
  • Remove Item:
_onChange() {
  this.setState({members: MemberStore.getMembers()});
}
Khi có sự thay đổi ở Store, ta chỉ cần update lại state của MemberApp, khi đó MemberApp.react sẽ tự động render lại View (chắc bạn đã làm quen với điều này ở các bài trước).
  • Remove event
Vì sau khi remove Item, Component sẽ render lại, cho nên chúng ta cần remove Event để tránh bị double, và việc removeListener sẽ được thực hiện trước khi render().
componentWillUnmount() {
  MemberStore.removeListener('change',this._onChange);
}
Sau khi Component được render, chúng ta lại thiết lập lại việc bắt sự kiện như ở trên.
MemberApp.react.js
var React   = require('react/addons'),
    request = require('superagent'),
    Router  = require('react-router'),
    mui     = require('material-ui'),
    Member  = require('./Member.react')
    ;

var Route         = Router.Route;
var RouteHandler  = Router.RouteHandler;
var DefaultRoute  = Router.DefaultRoute;
var Link          = Router.Link;

var ThemeManager = new mui.Styles.ThemeManager();

var List     = mui.List;
var ListDivider  = mui.ListDivider;

var MemberStore = require('../stores/MemberStore');


var MemberApp = React.createClass({
  childContextTypes: {
    muiTheme: React.PropTypes.object
  },
  getChildContext() {
    return {
      muiTheme: ThemeManager.getCurrentTheme()
    };
  },
  getInitialState() {
    return {
      members: MemberStore.getMembers()
    }
  },
//  deleteItem(id){
//    this.setState({
//      members: this.state.members.filter(function(member){
//        return member.id !== id;
//      })
//    });
//  },
  componentDidMount() {
    MemberStore.on('change',this._onChange);
  },
  componentWillUnmount() {
    MemberStore.removeListener('change',this._onChange);
  },
  // Event hanlder for 'change' event comming from the MemberStore
  _onChange() {
    this.setState({members: MemberStore.getMembers()});
  },
  render() {
    var members = this.state.members.map(function(member){
      return (
        <div>
          <Member id={member.id} onDelete={this.deleteItem} name={member.name} initialLike={member.like} src={member.src}/>
          <ListDivider inset={true}/>
        </div>
        );
    }, this);
    return (<List>{members}</List>);
  }
})

module.exports = MemberApp;
Lưu ý: chúng ta không còn cần method deleteItem(id) nữa.
MemberStore.js
// MemberStore.js
var AppDispatcher = require('../AppDispatcher');
var MemberConstants = require('../MemberConstants');

var EventEmitter = require('events').EventEmitter;

var _dispatchToken;
// Member Collections
var _members = [
  {id:1, name: "Avatar 1", like: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
  {id:2, name: "Avatar 2", like: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
  {id:3, name: "Avatar 3", like: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
];
require('array.prototype.findindex');

function destroy(id) {
  let memberIndex = _members.findIndex(member => member.id == id);
  delete _members[memberIndex];
};

class MemberStore extends EventEmitter {
  constructor() {
    super();
    _dispatchToken = AppDispatcher.register(action => {
      switch(action.type) {
        case MemberConstants.MEMBER_DESTROY:
          destroy(action.id);
          this.emit('change');
          break;
      }
    });
  }

  getDispatchToken() {
    return _dispatchToken;
  }

  getMembers() {
    return _members;
  }
}

module.exports = new MemberStore();
Cấu trúc 1 Store về cơ bản sẽ như sau (theo hướng TOP-DOWN)
TOP:
- require(import) module cần dùng.
- private variables 
- private method: thực hiện việc xử lý dữ liệu, như `function destroy(id)`
MIDDLE:
- switch … case: phân loại Action Type, gọi method xử lý và phát sự kiện tương ứng.
BOTTOM:
- public method (getter) get data từ Store, như method `getMembers()`
Done!