Hướng dẫn Flux qua ví dụ

Giới thiệu về Flux

Flux là một kiến trúc phát triển ứng dụng mà Facebook dùng để xây dựng phần client-side cho những ứng dụng web của họ. Nó giúp làm việc với các components của React một cách dễ dàng bằng cách sử dụng luồng dữ liệu một chiều (Unidirectional Data Flow). Chúng ta cần lưu ý rằng, Flux là một kiến trúc, chứ không phải là một framework.

Tổng quan về kiến trúc của Flux

architecture_flux.jpg

Những khái niệm chính

  • Store: Trung tâm dữ liệu, xử lí hành động liên quan đến dữ liệu, và gửi đi thông báo cho các listener khi dữ liệu thay đổi.
  • Dispatcher: Đối tượng trung gian, gửi những hành động (actions) từ view đến stores.
  • View: Nhận dữ liệu và các cập nhật từ Store, và gọi các actions.
  • Action: Phát tán (dispatch) actions bằng Dispatcher đến những nơi đã đăng kí (register) nhận action đó.

Làm một ứng dụng theo kiến trúc Flux

Những khái niệm này nghe khá trừu tượng và các bài viết về lí thuyết kiến trúc của Flux trên Viblo cũng đã có nhiều, mình cũng không muốn đi quá chi tiết vào chúng nữa. Trong bài viết này, mình sẽ hướng dẫn các bạn tìm hiểu về Flux thông qua việc từng bước xây dựng một ứng dụng CRUD (Create-Read-Update-Delete) đơn giản theo mô hình Flux. Với mình thì việc học qua các ví dụ luôn là dễ hiểu nhất.

Mình sẽ xây dựng một ứng dụng quản lí sinh viên. Cho phép người dùng xem danh sách sinh viên, thêm sinh viên mới, sửa và xoá các sinh viên hiện tại. Mã nguồn các bạn có thể xem tại: https://github.com/hieubm/flux-sample

Đầu tiên, chúng ta sẽ tìm hiểu vể Store bằng cách xây dựng StudentStore, là nơi lưu trữ thông tin về tất cả sinh viên.

stores/student-store.jsx

var _ = require("underscore"),
    EventEmitter = require('events').EventEmitter;

var _students = [{name: 'Nguyen Van A'}, {name: 'Bui Thi B'}];

var StudentStore  = _.extend(EventEmitter.prototype, {
    getStudents: function() {
        return _students;
    },
});

Trong đoạn code này, chúng ta tạo một mảng _students lưu trữ thông tin về sinh viên. Mình có để mặc định dữ liệu ban đầu bao gồm 2 sinh viên "Nguyen Van A" và "Bui Thi B".

Một lưu ý là trong kiến trúc Flux, đối tượng Store chỉ có hàm get chứ không có hàm set, vậy nên trong StudentStore mình chỉ viết hàm getStudents và trả lại mảng _students

Sau khi có store, chúng ta sẽ cần hiển thị chúng ra.

components/main.jsx

var React = require("react"),
    StudentStore = require("../stores/student-store"),
    StudentList = require("./student-list");

var Main = React.createClass({
    getInitialState: function() {
        return {
            students: StudentStore.getStudents(),
        }
    },
    render: function() {
        return (
            <div className="row">
                <h1 className="text-center">Student Management</h1>
                <div className="col-md-4 col-md-offset-4">
                    <StudentList students={this.state.students} />
                </div>
            </div>
        );
    }
});

module.exports = Main;

components/student-list.jsx

var React = require("react"),

var StudentList = React.createClass({
    render: function() {
        var studentList = this.props.students.map(function(student, index) {
            return (
                <tr key={index}>
                    <td>{student.name}</td>
                </tr>
            );
        });

        return (
            <div>
                <table className="table">
                    <tbody>
                        {studentList}
                    </tbody>
                </table>
            </div>
        );
    }
});

module.exports = StudentList;

Trong main.jsx chúng ta lấy ra danh sách sinh viên từ store và gán vào state trong hàm getInitialState. Sau đó truyền danh sách này vào trong component StudentList, component này có nhiệm vụ hiển thị toàn bộ sinh viên ra. Và đây là kết quả:

1.png

Tiếp đến, để thêm sinh viên mới, chúng ta sẽ tạo component StudentForm có nhiệm vụ làm form nhập liệu để add student.

var React = require("react"),
    StudentActions = require("../actions/student-actions");

var StudentForm = React.createClass({
    _onClickAdd: function() {
        StudentActions.addStudent({name: this.state.name});
    },
    _onChangeName: function(e) {
        this.setState({
            name: e.target.value,
        });
    },
    getInitialState: function() {
        return {
            name: "",
        }
    },
    render: function() {
        return (
            <div className="row" style={{margin: "10px"}}>
                <div className="col-md-2">
                    Name:
                </div>
                <div className="col-md-6">
                    <input value={this.state.name} onChange={this._onChangeName} />
                </div>
                <div className="col-md-2">
                    <input type="button" value="Add" className="btn btn-primary" onClick={this._onClickAdd} />
                </div>
            </div>
        );
    }
});

module.exports = StudentForm;

Như các bạn thấy, trong file này, chúng ta có một text-field Name và một button Add. Mỗi khi người dùng nhập liệu vào trong text-field, thì this.state.name sẽ được cập nhật giá trị. Khi người dùng ấn Add, hàm _onClickAdd sẽ được gọi và hàm này gọi đến một action với tham số là tên của sinh viên đã nhập StudentActions.addStudent({name: this.state.name});

Tiếp đến, chúng ta sẽ xây dựng file actions/student-action.jsx

var StudentConstants = require("../constants/student-constants"),
    AppDispatcher = require("../dispatcher/app-dispatcher");

var StudentActions = {
    addStudent: function(student) {
        AppDispatcher.dispatch({
            action: StudentConstants.ACTION_ADD,
            student: student,
        })
    },
}

module.exports = StudentActions;

Actions trong Flux làm nhiệm vụ chính là phát tán (dispatch) thông tin về action này đến tất cả những nơi đã đăng kí (register) nhận thông tin về action đó. Việc phát tán thông qua hàm dispatch của một đối tượng đặc biệt là Dispatcher. Để sử dụng Dispatcher này, chúng ta cần khởi tạo nó trong file dispatcher/app-dispatcher.jsx

var Dispatcher = require('flux').Dispatcher;

module.exports = new Dispatcher();

Hàm dispatch nhận vào một đối tượng bất kì. Trong ví dụ này, chúng ta truyền vào một đối tượng có 2 thuộc tính. Thứ nhất là tên action và thứ hai là đối tượng student cần add. Tên action chúng ta sử dụng hằng số ACTION_ADD đặt trong actions/student-actions.jsx.

module.exports = {
    ACTION_ADD: "ACTION_ADD",
}

Bây giờ chúng ta cần chỉnh sửa một chút trong student-store.jsx

// ...
var CHANGE_EVENT = 'change';

var _students = [{name: 'Nguyen Van A'}, {name: 'Bui Thi B'}];

function _addStudent(student) {
    _students.push(student);
}

var StudentStore  = _.extend(EventEmitter.prototype, {
    getStudents: function() {
        return _students;
    },
    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    }
});

AppDispatcher.register(function(payload) {
    switch (payload.action) {
        case StudentConstants.ACTION_ADD:
            _addStudent(payload.student);
            StudentStore.emitChange();
            break;
    }

});
// ...

Chúng ta hãy để ý đến đoạn AppDispatcher.register. Đây là nơi đăng kí nhận các event do các actions gửi đi thông qua hàm dispatch. Ở đây, chúng ta chỉ nhận xử lí action ACTION_ADD và gọi đến hàm nội bộ _addStudent. Hàm _addStudent sẽ push đối tượng student mới vào trong mảng _students. Sau khi thêm thành công, chúng ta cần thông báo cho View biết về sự thay đổi này thông qua hàm StudentStore.emitChange(). Hàm này sẽ broadcast CHANGE_EVENT đến toàn bộ các đối tượng đã đăng kí nhận thông tin thay đổi bằng hàm addChangeListener. Bây giờ chúng ta sẽ quay lại main.jsx để đăng kí nhận các thay đổi.

//...
var Main = React.createClass({
    componentDidMount: function() {
        StudentStore.addChangeListener(this._onChange);
    },
    _onChange: function() {
        this.setState({
            students: StudentStore.getStudents(),
        })
    },
//...

Đoạn code này nghĩa là, sau khi các component được mount, chúng ta đăng kí nhận các thông tin cập nhật mới nhất về dữ liệu students. Khi dữ liệu về students có thay đổi, hàm _onChange sẽ được gọi, và cập nhật lại state của Main component.

Chúng ta hãy chạy thử chương trình, và thử thêm một sinh viên mới, và đây là kết quả:

2.png

Đến bước này thì chúng ta đã nắm khá rõ về chức năng và cách hoạt động của các thành phần View, Action, Store, Dispatcher trong Flux rồi. Tương tự như vậy, chúng ta sẽ làm tiếp tính năng xoá sinh viên. Ý tưởng là bên cạnh mỗi sinh viên sẽ có một button Remove, khi click vào thì sinh viên đó sẽ bị xoá khỏi danh sách. Để thực hiện, chúng ta quay lại với student-list.jsx

var React = require("react"),
    StudentActions = require("../actions/student-actions");

var StudentList = React.createClass({
    render: function() {
        var studentList = this.props.students.map(function(student, index) {
            return (
                <tr key={index}>
                    <td>{student.name}</td>
                    <td className="col-md-1">
                      <input type="button" value="Remove" className="btn btn-danger"
                             onClick={StudentActions.removeStudent.bind(null, index)} />
                    </td>
                </tr>
            );
        }.bind(this));

        //...

Với mỗi student, chúng ta thêm một button Remove, khi click vào chúng ta gọi đến StudentActions.removeStudent với tham số là index của sinh viên đó trong mảng.

Tiếp đến chúng ta cần thêm phần xử lí cho removeStudent tương tự như cách chúng ta đã làm addStudent

actions/student-actions.jsx

//...
removeStudent: function(index) {
    AppDispatcher.dispatch({
        action: StudentConstants.ACTION_REMOVE,
        index: index,
    })
}
//...

constants/student-constants.jsx

module.exports = {
    ACTION_ADD: "ACTION_ADD",
    ACTION_REMOVE: "ACTION_REMOVE",
}

stores/student-store.jsx

//...
function _removeStudent(index) {
    _students.splice(index, 1);
}
//...
AppDispatcher.register(function(payload) {
    switch (payload.action) {
        //...
        case StudentConstants.ACTION_REMOVE:
            _removeStudent(payload.index);
            StudentStore.emitChange();
            break;
    }

});
//...

Đây là kết quả:

3.png

Tương tự như vậy, chúng ta có thể xây dựng tiếp phần sửa thông tin cho sinh viên. Bạn có thể xem mã nguồn đầy đủ tại trang Github: https://github.com/hieubm/flux-sample

Tham khảo

https://viblo.asia/thangtd90/posts/NznmMd34Rr69 https://viblo.asia/tungshooter/posts/jdWrvwjzGw38 http://blog.andrewray.me/reactjs-for-stupid-people/ http://blog.mimacom.com/introduction-to-react-and-flux/ http://facebook.github.io/flux/docs/overview.html