Trong bài viết này, chúng ta sẽ cùng nhau thảo luận về Flux, bao gồm các thành phần và hoạt động của Flux. Tôi cũng sẽ cung cấp một hướng dẫn nhỏ để xây dựng một module giỏ hàng đơn giản sử dụng Flux. Hãy cùng tiếp cận với Flux qua những câu hỏi thường gặp khi bạn mới bắt đầu tìm hiểu về nó.
Có nên sử dụng Flux không?
Nếu ứng dụng của bạn làm việc với dữ liệu động, thì câu trả lời là có, bạn nên sử 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ẻ 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, vì Flux sẽ không có tác dụng trong hoàn cảnh này.
Flux là gì?
Flux là một kiến trúc được Facebook sử dụng khi làm việc với React. Flux không phải là một framework hay thư viện, mà chỉ đơn giản là một kiểu kiến trúc mới hỗ trợ cho React và xây dựng ý tưởng về luồng dữ liệu một chiều (Unidirectional Data Flow).
Một kiến trúc Flux cơ bản được tạo ra bằng cách kết hợp thư viện Dispatcher của Facebook và Module NodeJS EventEmitter để tạo ra một hệ thống sự kiện (Event System) quản lý trạng thái của ứng dụng.
Cấu trúc của Flux
Tương tự những mô hình khác như MVC, MVVM, Flux cũng được chia thành các khối thành phần cơ bản như sau:
- Actions – Làm nhiệm vụ truyền dẫn dữ liệu tới Dispatcher (được coi như các Helper Method).
- Dispatcher – Nhận thông tin từ Actions, truyền tải dữ liệu (payload) tới các nơi đã đăng ký nhận thông tin.
- Stores – Là nơi lưu trữ trạng thái và logic của hệ thống, đồng thời đăng ký nhận dữ liệu với Dispatcher.
- Controller Views – Chính là các React Components, nhận trạng thái từ Stores và truyền dữ liệu (dưới dạng props) cho các thành phần con.
Mô hình hoạt động của Flux
Dưới đây là sơ đồ chung về quan hệ giữa các thành phần trong Flux:
Chúng ta có thể hiểu đơn giản như sau:
- Views chính là thành phần hiển thị nội dung ứng dụng (tương tự thành phần V trong MVC).
- Khi người dùng tương tác với ứng dụng và thay đổi trạng thái của ứng dụng (ví dụ: thêm, sửa, xóa dữ liệu cá nhân), Views sẽ thông qua Actions gửi thông tin thay đổi tới Dispatcher, bao gồm:
action_name
: tên của Action (ví dụ: 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 (ví dụ: Object chứa thông tin ID, số lượng, giá cả,… của sản phẩm).
- Sau khi nhận thông tin từ Action, Dispatcher truyền tải payload đã nhận được tới các Stores đã đăng ký lắng nghe sự kiện thay đổi trước đó.
- Stores nhận thông tin và tiến hành cập nhật dữ liệu (tương tự như việc cập nhật state của Component).
- Sau khi cập nhật, Stores phát sự kiện xuống Views để cập nhật hiển thị cho người dùng.
- Ngoài ra, sơ đồ còn có một thành phần API để lấy dữ liệu từ máy chủ từ xa.
Sơ đồ này đảm bảo luồng dữ liệu trong Flux di chuyển theo một đường nhất định.
Xây dựng module giỏ hàng với Flux
Trong bài viết này, tôi sẽ không đi quá sâu vào lý thuyết và phân tích chi tiết về Flux. Thay vào đó, tôi sẽ tập trung vào việc xây dựng một ứng dụng nhỏ để bạn có cái nhìn trực quan hơn về cách hoạt động của mô hình Flux.
Để có thể hiểu và thực hiện bài hướng dẫn nhỏ này, tôi giả định rằng bạn đã có kiến thức cơ bản về ReactJS, bao gồm:
- Đã xây dựng được ứng dụng chào thế giới đơn giản bằng JSX.
- Đã viết được một số Component trong React, nắm được khái niệm về state và props trong Component.
- Lưu ý nhỏ là 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 giỏ hàng (Cart) 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 sản phẩm muốn mua (Increase).
- Giảm số lượng sản phẩm muốn mua (Decrease).
Hình ảnh của Module khi hoàn thiện như sau:
Đầu tiên, chúng ta hãy bắt đầu với cấu trúc thư mục của ứng dụng 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ó file package.json
, bạn chỉ cần vào thư mục làm việc và chạy 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 có 2 nhiệm vụ chính:
browserify
: sử dụng reactify để chuyển đổi code từ JSX sang JS, đồng thời sao chép fileapp.js
sang thư mụcdist/js
.copy
: chỉ sao chép fileindex.html
từ thư mụcsrc
sang thư mụcdist
.
Để 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 mặc định (default
).
Như vậy, chúng ta đã hoàn thành việc cài đặt các công cụ hỗ trợ cho việc phát triển ứng dụng. Bây giờ, chúng ta đã có sẵn thư viện react và thư viện flux. Hãy bắt đầu 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, chúng ta có thể chia màn hình thành các Component như hình ảnh sau:
Từ đó, chúng ta có thể xây dựng 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 sẽ 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. Đối với các chức năng Remove From Cart, Increase, Decrease, bạn vui lòng xem source code để hiểu chi tiết hơn.
Khi thực hiện chức năng Add To Cart, chúng ta sẽ 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 xem source code của component add-to-cart.js
:
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}>Thêm vào giỏ hàng</button>
);
}
});
module.exports = AddToCart;
Khi nhấp vào nút “Thêm vào giỏ hàng”, Action addItem
sẽ được gọi và truyền thông tin sản phẩm được chọn (dưới dạng Object). AppActions
là nơi đăng ký các Action của ứng dụng và truyền thông tin (payload) cho Dispatcher.
Hãy xem file src/js/actions/app-actions.js
để biết chức năng của nó:
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;
Trong đoạn mã trên, chúng ta đã gọi hàm addItem
của AppActions
kèm với thông tin sản phẩm được chọn. Khi đó, AppActions
sẽ truyền các thông tin này cho Dispatcher:
actionType
: tên của Action, được quản lý thông quaAppConstants
(đây 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 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 các tham số là my_item
, cart_item
, v.v… 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 như thế nào và thông báo cho Store như thế nào. 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 của Facebook. Chúng ta cũng có thể thấy hàm handleViewAction
mà AppActions
của chúng ta đã gọi trước đó. Khi đó, AppDispatcher
sẽ làm nhiệm vụ trung chuyển thông tin và phát lệnh gửi thông tin tới các Store.
File src/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: "Sản phẩm #1", cost: 1 },
{ id: 2, title: "Sản phẩm #2", cost: 2 },
{ id: 3, title: "Sản phẩm #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ý 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 phát sự kiện để thông báo cho các View rằng dữ liệu đã được thay đổi. Điều này được thực hiện nhờ sự hỗ trợ của module EventEmitter
.
AppStore.emitChange();
AppStore
cung cấp hàm getCart()
để lấy thông tin của biến _cartItems
. Bạn 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 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>Sản phẩm</th>
<th>Số lượng</th>
<th></th>
<th>Thành tiền</th>
</tr>
</thead>
<tbody>
{items}
</tbody>
<tfoot>
<tr>
<td colSpan="4" className="text-right">Tổng cộng</td>
<td>${total}</td>
</tr>
</tfoot>
</table>
);
}
});
module.exports = Cart;
View được khởi tạo luôn lắng nghe sự thay đổi từ Store:
componentWillMount: function() {
AppStore.addChangeListener(this._onChange);
}
Khi Store thay đổi thông tin, View gọi hàm 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 và 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, tôi đã mô tả một luồng hoạt động đơn giản của ứng dụng với mô hình Flux. Đối với các chức năng khác, vui lòng xem source code để hiểu chi tiết hơn. Hy vọng bài hướng dẫn nhỏ này sẽ giúp bạn trong bước đầu tìm hiểu về mô hình Flux.