Học ReactJS Full Đầy Đủ Nhất

Bài 14: ReactJS - Khái niệm Flux

Trong nội dung bài viết này, tôi cùng các bạn sẽ cùng nhau thảo luận về Flux, cụ thể về các thành phần và hoạt động của Flux, đồng thời tôi cũng đưa ra một bài hướng dẫn nhỏ (tutorial) để xây dựng một module shopping cart đơn giản bằng Flux. Hãy cùng tiếp cận bằng những câu hỏi mà bạn đang đặt ra trong đầu với Flux khi bạn chưa biết gì về nó 

1 . Có nên dùng Flux không ?

Nếu ứng dụng của bạn phải làm việc với dữ liệu động thì câu trả lời là có, bạn nên dùng Flux.
Nếu ứng dụng của bạn chỉ làm việc với dữ liệu tĩnh, không chia sẻ các trạng thái của ứng dụng, không lưu trữ hoặc cập nhật dữ liệu, thì câu trả lời là không bởi vì Flux không giúp ích gì cho bạn trong hoàn cảnh này cả 

2. Flux là gì ?

  • Flux và ReactJS cùng được tạo ra bởi Facebook để giải quyết một số những vấn đề rất đặc thù của bản thân Facebook
  • Trước khi có Flux và React thì hệ thống model và view của facebook như sau :
  • Do tương tác của người dùng là ở View nên đôi khi View cần phải update ngược lại dữ liệu ở Model khác. Điều này dẫn đến hiệu ứng "thác nước" khi một hành động của người dùng trigger một loạt các thay đổi, mỗi thay đổi lại có thể trigger ra một loạt các thay đổi khác, và tất cả các thay đổi là quá trình bất đồng bộ. Và nhìn vào mô hình trên sẽ khó khăn rất lớn khi debug nếu có lỗi xảy ra
Giải pháp: Luồng dữ liệu một hướng và Flux đã ra đời:
  1. Flux là architecture hơn là một framework hay một library
  2. Sử dụng cho việc xây dựng client-side web applications
  3. 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(dữ liệu một chiều)
  • 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.

a - Action Creators

  • Action Creators có nhiệm vụ tạo ra các action, là bước đầu tiên trong luồng mà các thay đổi và tương tác đều đi qua. Bất cứ khi nào trạng thái của web app hay là render của view thay đổi thì đầu tiên sẽ là một hành động được tạo ra.
  • Sau khi The Action Creator tạo ra action, anh ta sẽ gửi nó cho The dispatcher.

b - The dispatcher

  • The dispatcher về cơ bản là một tập hợp rất nhiều các callbacks. Khi một action được gửi đến The dispatcher, nó sẽ gửi chúng đến store tương ứng theo quy tắc đồng bộ.

c - The Store

  • The store là nơi chứa toàn bộ các trạng thái và logic chuyển trạng thái của app.Tất cả mọi thay đổi trạng thái đều được thực thi trực tiếp ở đây.
  • Mỗi khi bạn muốn thay đổi một trạng thái, bạn cần phải tạo một action, submit vào The action creator, đi qua The dispatcher rồi mới được The store xử lý.
  • Một store sẽ nhận rất nhiều các action, và trong store thường sẽ có một cấu trúc switch để quyết định xem có cần phải quan tâm đến action hay không.

d - The controller view and the view

  • The view có nhiệm vụ thu nhận lệnh thay đổi trạng thái và render hiển thị, cũng như nhận input từ người dùng.
  • Controller view như là một nhà quản lý cấp nhỏ đứng giữa store và view, nhận thông báo khi trạng thái thay đổi, tổng hợp những nội dung cần thay đổi và truyền đến những view trực thuộc.

3. Mô hình hoạt động

Sơ đồ chung về quan hệ giữa các thành phần trong Flux :
Ta có thể hiểu đơn giản như sau :
  • Views chính là thành phần làm nhiệm vụ hiển thị nội dung ứng dụng (có thể hiểu giống như thành phần V trong mô hình MVC).
  • Khi người dùng tương tác với ứng dụng làm thay đổi trạng thái (state) của ứng dụng (VD: thêm, sửa, xóa dữ liệu cá nhân), View sẽ thông qua Action gửi các thông tin thay đổi tới Dispatcher gồm có :action_name: tên của Action (VD: ADD_ITEM - thêm sản phẩm vào giỏ hàng).action_payload: thông tin chi tiết nội dung muốn gửi (VD: Object chứa thông tin ID, quantity, price, ... của sản phẩm).
  • action_name: tên của Action (VD: ADD_ITEM - thêm sản phẩm vào giỏ hàng).
  • action_payload: thông tin chi tiết nội dung muốn gửi (VD: Object chứa thông tin ID, quantity, price, ... của sản phẩm).
  • Sau khi nhận được thông tin từ Action, Dispatcher làm nhiệm vụ truyền tải (broadcast) nội dung nhận được tới các Store đăng ký lắng nghe sự kiện thay đổi từ trước đó.
  • Store sau khi nhận thông tin, tiến hành cập nhật dữ liệu (có thể hiểu việc cập nhật dữ liệu ở đây giống việc cập nhật state của Component).
  • Sau khi cập nhật, Store bắn sự kiện xuống View để tiến hành cập nhật hiển thị cho người dùng.
  • Ngoài ra trong sơ đồ trên còn có một thành phần API để lấy dữ liệu từ Remote Server.
Sơ đồ trên đảm bảo luồng dữ liệu di chuyển trong Flux bắt buộc đi theo một đường nhất định.

4 - Các ví dụ :

ví dụ 1 :  Để hiểu rõ hơn về cấu trúc hoạt động của flux, sau đây mình sẽ trình bày 1 demo nhỏ về crud bằng reactjs và flux
  • Yêu cầu bài toán là người sử dụng biết về reactjs cơ bản và sử dụng jsx và biết component và state và props
  • Bài toán đặt ra là xây dựng một app có đầy đủ các chức năng cơ bản:Xem danh sách bài viếtCác chức năng chính như: create, update, delete, search
  • Đây là cấu trúc thư mục reactjs với rails
Nhìn vào hình vẽ trên ta có thể thấy đầy đủ các thành phần của flux in reactjs như folder actions, dispatcher, stroes, hay components...
Sau đây mình sẽ đi chi tiết vào một chức năng đơn giản nhất đó là remove một article khỏi danh sách:
  • Khi Remove Article, chúng ta sẽ phải tìm id của article cần xóa và hiển thị lại thông tin danh sách articles. Sau đây là source code của component remove-from-article
###* @jsx React.DOM ###
define [
  "react-with-addons"
  "actions/app-actions"
], (React, AppActions) ->
  "use strict"
  removeFromArticle = React.createClass(
    handleClick: (e) ->
      AppActions.removeArticle @props.index
    render: ->
      `<button onClick={this.handleClick}>-</button>`
  )
  removeFromArticle
Khi click button remove article, Action removeArticle sẽ được gọi, đồng thời truyền theo thông tin id của article đang được chọn. AppActions là nơi đăng ký các Action của ứng dụng và chuyển tải thông tin (payload) tới Dispatcher.
Hãy cùng xem ../actions/app-actions.js thực hiện những công việc gì:
define [
  "actions/constants"
  "dispatcher/app-dispatcher"
], (Constants, AppDispatcher)->
  "use strict"
  AppActions =
    removeArticle: (index)->
      AppDispatcher.handleViewAction(actionType: Constants.REMOVE_ARTICLE, index: index)
  AppActions
Như trên chúng ta đã gọi đến function removeArticle của AppActions kèm theo thông tin id của article mà chúng ta chọn. Khi này AppActions sẽ chuyển các thông tin cho Dispatcher:
  • actionType: tên của Action, để thuận tiện cho việc đặt tên Action chúng ta quản lý thông qua AppConstants(chủ yếu để quản lý các text tĩnh đặt tên cho Action):
define [], ->
  "use strict"
  REMOVE_ARTICLE:    "REMOVE_ARTICLE"
Theo luồng hoạt động của Flux, hãy xem sau khi Action truyền thông tin cho Dispatcher thì Dispatcher sẽ xử lý thông tin và thông báo cho Store. Hãy xem file ../dispatcher/app-dispatcher.js:
define [
  "jquery"
  "dispatcher/dispatcher"
], ($, Dispatcher)->
  "use strict"
  AppDispatcher = $.extend(Dispatcher.prototype, {
    handleViewAction: (action)->
      @dispatch(
        source: 'VIEW_ACTION'
        action: action
      )
  })
  AppDispatcher
Ta có thể thấy function handleViewAction mà AppActions của chúng ta vừa gọi đến lúc trước. Khi này AppDispatcher sẽ phát lệnh gửi thông tin tới các Store.
File ../stores/app-stores.js
define [
  "jquery"
  "lodash"
  "dispatcher/app-dispatcher"
  "actions/constants"
  "eventemitter2"
], ($, _, AppDispatcher, Constants, EventEmitter)->
  "use strict"

  CHANGE_EVENT = "change"

  _articles = []

  _removeArticle = (index)->
    _articles.splice(index, 1)

  AppStore = $.extend(new EventEmitter(), {
    emitChange: -> @emit(CHANGE_EVENT)
    addChangeListener: (callback)-> @on(CHANGE_EVENT, callback)
    removeChangeListener: (callback)-> @removeListener(CHANGE_EVENT, callback)
    getArticle: -> _articles
    dispatcherIndex: AppDispatcher.register((payload)->
      action = payload.action

      switch action.actionType
        when Constants.REMOVE_ARTICLE then _removeArticle(action.index)

      AppStore.emitChange()
    )
  })
AppStores đã đăng ký trước với Dispatcher để lắng nghe các Action. Sau khi thực hiện xóa article, AppStore tiến hành truyền sự kiện để thông báo tới cho các View dữ liệu lưu trữ đã được thay đổi nhờ sự trợ giúp của module EventEmitter
Ví dụ 2 :
Xây dựng module shopping cart với flux 
Để có thể hiểu và thực hiện được bài hướng dẫn (tutorial) nho nhỏ này, tôi đặt giả thiết các bạn đã từng làm việc cơ bản với ReactJS :
  • Đã xây dựng được Hello Word Application bằng JSX.
  • Đã viết được Component nào đó trong React, nắm được khái niệm state và props trong Component.
  • Một chú ý nho nhỏ để phân biệt giữa state và props đó là khi state thay đổi thì Component sẽ được Render lại, còn props thì không.
Bài toán đặt ra là xây dựng một module Cart (giỏ hàng) cho phép người dùng thực hiện các thao tác :
  • Xem thông tin các mặt hàng đang có và đơn giá của từng mặt hàng.
  • Xem thông tin giỏ hàng hiện tại, số lượng các sản phẩm, giá từng mặt hàng và tổng giá trị đơn hàng.
  • Các nút chức năng thực hiện công việc:Thêm sản phẩm vào giỏ hàng (Add).Xóa sản phẩm trong giỏ hàng (Remove).Tăng số lượng (quantity) sản phẩm muốn mua (Increase).Giảm số lượng (quantity) sản phẩm muốn mua (Decrease).
  • Thêm sản phẩm vào giỏ hàng (Add).
  • Xóa sản phẩm trong giỏ hàng (Remove).
  • Tăng số lượng (quantity) sản phẩm muốn mua (Increase).
  • Giảm số lượng (quantity) sản phẩm muốn mua (Decrease).
Hình ảnh của Module khi hoàn thiện như sau :
Trước tiên hãy bắt đầu với cấu trúc thư mục của ứng dụng mà chúng ta sắp xây dựng:
dist/
    js/
        app.js
    index.html
node_modules/
src/
    js/
        actions/
        components/
        constants/
        dispatcher/
        stores/
        app.js
    index.html
gulpfile.js
package.json
File package.json có nội dung như sau:
{
  "name": "react-flux-stores",
  "version": "1.0.0",
  "description": "Building simple store with React and Flux",
  "main": "app.js",
  "scripts": {
    "test": "gulp"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/nguyenthanhtung88/react-flux-stores.git"
  },
  "keywords": [
    "react",
    "flux",
    "store"
  ],
  "author": "Tungshooter",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/nguyenthanhtung88/react-flux-stores/issues"
  },
  "homepage": "https://github.com/nguyenthanhtung88/react-flux-stores",
  "devDependencies": {
    "flux": "^2.0.1",
    "gulp": "^3.8.11",
    "gulp-browserify": "^0.5.1",
    "gulp-concat": "^2.5.2",
    "react": "^0.13.1",
    "reactify": "^1.1.0",
    "underscore": "^1.8.3"
  }
}
Sử dụng npm để quản lý các module liên quan như gulp, react, flux, underscore. Sau khi đã có package.json bạn chỉ việc vào thư mục làm việc và thực hiện lệnh:
npm install
để cài đặt các module sử dụng cho ứng dụng vào thư mục node_modules.
gulpfle.js
var gulp = require("gulp");
var browserify = require("gulp-browserify");
var concat = require("gulp-concat");

gulp.task("browserify", function() {
    gulp.src("src/js/app.js")
        .pipe(browserify({transform: "reactify"}))
        .pipe(concat("app.js"))
        .pipe(gulp.dest("dist/js"));
});

gulp.task("copy", function() {
    gulp.src("src/index.html")
        .pipe(gulp.dest("dist"));
});

gulp.task("default", ["browserify", "copy"]);

gulp.task("watch", function() {
    gulp.watch("src/**/*.*", ["default"]);
});
Gulp làm 2 nhiệm vụ chính:
  • browserify: có sự trợ giúp của reactify để chuyển code từ jsx sang js, đồng thời copy app.js sang dist/js.
  • copy: chỉ làm nhiệm vụ copy index.html từ src sang dist.
Để thực hiện gulp task, điều đầu tiên bạn cần làm là cài đặt global gulp:
npm install --global gulp
Sau đó vào thư mục làm việc và chạy lệnh gulp, khi đó các gulp task sẽ được tự động thực hiện theo task default.
Như vậy chúng ta đã hoàn thành việc cài đặt các công cụ trợ giúp cho việc phát triển ứng dụng. Chúng ta đã có trong tay thư viện react, thư viện flux. Giờ là lúc bắt đầu vào xây dựng View và các chức năng liên quan.
Với hình ảnh ứng dụng hoàn thành, ta có thể chia màn hình thành các Component như hình ảnh sau:
Từ đó xây dựng được thư mục src/js/components gồm các thành phần View:
src/
    js/
        components/
            add-to-cart.js
            cart.js
            catalog.js
            decrease.js
            increase.js
            main.js
            remove-from-cart.js
Trong phạm vi bài viết này tôi xin phép chỉ giới thiệu một luồng hoạt động của chức năng Add To Cart theo mô hình Flux. Với các chức năng Remove From Cart, Increase, Decrease các bạn vui lòng xem source code để hiểu chi tiết hơn.
Khi thực hiện Add To Cart, chúng ta sẽ phải thêm sản phẩm vào giỏ hàng và hiển thị thông tin cho người dùng. Hãy cùng nhau xem source code của component add-to-cart
var React = require("react");
var AppActions = require("../actions/app-actions");

var AddToCart = React.createClass({
    handleClick: function() {
        AppActions.addItem(this.props.item);
    },
    render: function() {
        return (
            <button onClick={this.handleClick}>Add To Cart</button>
        );
    }
});

module.exports = AddToCart;
Khi click button Add to cart, Action addItem sẽ được gọi, đồng thời truyền theo thông tin của sản phẩm đang được chọn (dưới dạng Object). AppActions là nơi đăng ký các Action của ứng dụng và chuyển tải thông tin (payload) tới Dispatcher.
Hãy cùng xem src/js/actions/app-actions.js thực hiện những công việc gì:
var AppConstants = require("../constants/app-constants");
var AppDispatcher = require("../dispatcher/app-dispatcher");

var AppActions = {
    addItem: function(item) {
        AppDispatcher.handleViewAction({
            actionType: AppConstants.ADD_ITEM,
            item: item
        })
    },
    removeItem: function(index) {
        AppDispatcher.handleViewAction({
            actionType: AppConstants.REMOVE_ITEM,
            index: index
        })
    },
    increaseItem: function(index) {
        AppDispatcher.handleViewAction({
            actionType: AppConstants.INCREASE_ITEM,
            index: index
        })
    },
    decreaseItem: function(index) {
        AppDispatcher.handleViewAction({
            actionType: AppConstants.DECREASE_ITEM,
            index: index
        })
    }
}

module.exports = AppActions;
Như trên chúng ta đã gọi đến function addItem của AppActions kèm theo thông tin object của sản phẩm mà chúng ta chọn. Khi này AppActions sẽ chuyển các thông tin cho Dispatcher:
  • actionType: tên của Action, để thuận tiện cho việc đặt tên Action chúng ta quản lý thông qua AppConstants(chủ yếu để quản lý các text tĩnh đặt tên cho Action):
module.exports = {
    ADD_ITEM: "ADD_ITEM",
    REMOVE_ITEM: "REMOVE_ITEM",
    INCREASE_ITEM: "INCREASE_ITEM",
    DECREASE_ITEM: "DECREASE_ITEM",
}
  • item: thông tin object của sản phẩm (giá cả, số lượng, ...)
Chúng ta có thể tùy biến các tham số truyền cho Dispatcher, ví dụ bạn có thể đặt tên cho các tham số kiểu như my_item, cart_item,... không có vấn đề gì.
Đúng theo luồng hoạt động của Flux, hãy xem sau khi Action truyền thông tin cho Dispatcher thì Dispatcher sẽ xử lý thông tin ra sao và thông báo cho Store thế nào. Hãy cùng xem file src/js/dispatcher/app-dispatcher.js:
var Dispatcher = require("flux").Dispatcher;
var _ = require("underscore");

var AppDispatcher = _.extend(new Dispatcher(), {
    handleViewAction: function(action) {
        this.dispatch({
            source: 'VIEW_ACTION',
            action: action
        })
    }
});

module.exports = AppDispatcher;
Ở đây chúng ta kế thừa Dispatcher từ module flux mà Facebook cung cấp. Ta có thể thấy function handleViewAction mà AppActions của chúng ta vừa gọi đến lúc trước. Khi này AppDispatcher sẽ coi như trạm trung chuyển thông tin, phát lệnh gửi thông tin tới các Store.
Source code js/stores/app-stores.js
var AppDispatcher = require("../dispatcher/app-dispatcher");
var AppConstants = require("../constants/app-constants");
var _ = require("underscore");
var EventEmitter = require("events").EventEmitter;

var CHANGE_EVENT = "change";

var _catalog = [
    {id: 1, title: "Widget #1", cost: 1},
    {id: 2, title: "Widget #2", cost: 2},
    {id: 3, title: "Widget #3", cost: 3}
];

var _cartItems = [];

function _removeItem(index) {
    _cartItems[index].inCart = false;
    _cartItems.splice(index, 1);
}

function _increaseItem(index) {
    _cartItems[index].qty++;
}

function _decreaseItem(index) {
    if (_cartItems[index].qty > 1) {
        _cartItems[index].qty--;
    } else {
        _removeItem(index);
    }
}

function _addItem(item) {
    if (!item.inCart) {
        item['qty'] = 1;
        item['inCart'] = true;
        _cartItems.push(item);
    } else {
        _cartItems.forEach(function(cartItem, i) {
            if (cartItem.id == item.id) {
                _increaseItem(i);
            }
        });
    }
}

var AppStore = _.extend(EventEmitter.prototype, {
    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },
    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },
    getCart: function() {
        return _cartItems;
    },
    getCatalog: function() {
        return _catalog;
    }
});

AppDispatcher.register(function(payload) {
    var action = payload.action; // This is action from handleViewAction

    switch (action.actionType) {
        case AppConstants.ADD_ITEM:
            _addItem(action.item);
            break;
        case AppConstants.REMOVE_ITEM:
            _removeItem(action.index);
            break;
        case AppConstants.INCREASE_ITEM:
            _increaseItem(action.index);
            break;
        case AppConstants.DECREASE_ITEM:
            _decreaseItem(action.index);
            break;
    }

    AppStore.emitChange();

    return true;
});

module.exports = AppStore;
AppStores đã đăng ký trước với Dispatcher để lắng nghe các Action:
AppDispatcher.register(function(payload) {
    var action = payload.action; // This is action from handleViewAction

    switch (action.actionType) {
        case AppConstants.ADD_ITEM:
            _addItem(action.item);
            break;
        case AppConstants.REMOVE_ITEM:
            _removeItem(action.index);
            break;
        case AppConstants.INCREASE_ITEM:
            _increaseItem(action.index);
            break;
        case AppConstants.DECREASE_ITEM:
            _decreaseItem(action.index);
            break;
    }

    AppStore.emitChange();

    return true;
});
Sau khi thực hiện cập nhật các dữ liệu liên quan (trong luồng hoạt động này là thêm mới 1 sản phẩm vào giỏ hàng), AppStore tiến hành lan truyền sự kiện để thông báo tới cho các View dữ liệu lưu trữ đã được thay đổi nhờ sự trợ giúp của module EventEmitter
AppStore.emitChange();
var AppStore = _.extend(EventEmitter.prototype, {
    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },
    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },
    getCart: function() {
        return _cartItems;
    },
    getCatalog: function() {
        return _catalog;
    }
});
AppStore cung cấp hàm getCart() để lấy thông tin của biến _cartItems. Ta có thể tưởng tượng việc AppStore thực hiện thay đổi thông tin của biến _cartItems tương đương với việc thay đổi state của Component. Cuối cùng thông tin đã được truyền đến View và render lại để nhận được dữ liệu mới nhất. Hãy cùng xem source code của src/js/components/cart.js:
var React = require("react");
var AppStore = require("../stores/app-store");
var RemoveFromCart = require("../components/remove-from-cart");
var Increase = require("../components/increase");
var Decrease = require("../components/decrease");

function getCartItems() {
    return {
        items: AppStore.getCart()
    }
}

var Cart = React.createClass({
    getInitialState: function() {
        return getCartItems();
    },
    componentWillMount: function() {
        AppStore.addChangeListener(this._onChange);
    },
    _onChange: function() {
        this.setState(getCartItems());
    },
    render: function() {
        var total = 0;
        var items = this.state.items.map(function(item, i) {
            var subtotal = item.qty * item.cost;
            total += subtotal;
            return (
                <tr key={i}>
                    <td><RemoveFromCart index={i}/></td>
                    <td>{item.title}</td>
                    <td>{item.qty}</td>
                    <td>
                        <Increase index={i} />
                        <Decrease index={i} />
                    </td>
                    <td>${subtotal}</td>
                </tr>
            );
        });
        return (
            <table className="table table-hover">
                <thead>
                    <tr>
                        <th></th>
                        <th>Item</th>
                        <th>Quantity</th>
                        <th></th>
                        <th>Subtotal</th>
                    </tr>
                </thead>
                <tbody>
                    {items}
                </tbody>
                <tfoot>
                    <tr>
                        <td colSpan="4" className="text-right">Total</td>
                        <td>${total}</td>
                    </tr>
                </tfoot>
            </table>
        );
    }
});

module.exports = Cart;
View khi khởi tạo luôn lắng nghe sự thay đổi từ phía Store
componentWillMount: function() {
    AppStore.addChangeListener(this._onChange);
}
Khi Store thay đổi thông tin, View có nhiệm vụ gọi và lấy dữ liệu mới từ Store
_onChange: function() {
    this.setState(getCartItems());
}
Sau khi lấy được dữ liệu mới nhất cùng với việc thay đổi state của mình, View sẽ tự động render lại với dữ liệu tương ứng.
Trên đây là các kiến thức mà mình tìm hiểu được về các thành phần trong react-flux, rất mong được sự góp ý của mọi người. Cảm ơn mọi người đã theo dõi .