Xây dựng ứng dụng đơn giản với Flux
Bài đăng này đã không được cập nhật trong 3 năm
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ó
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ả +_+
Flux là gì ?
Flux là một kiến trúc mà Facebook sử dụng trong khi làm việc với React. Flux không phải là một framework hay một thư viện (library). Nó đơn giản chỉ là một kiểu kiến trúc mới hỗ trợ thêm cho React, đồng thời 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 điển hình là sự kết hợp giữa thư viện Dispatcher (được viết bởi Facebook) cùng với Module NodeJS EventEmitter để tạo nên một hệ thống sự kiện (Event System) quản lý các trạng thái của ứng dụng.
Cấu trúc của Flux
Như các mô hình khác (VD: MVC, MVVM, ...), Flux cũng được chia ra 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à các logic của hệ thống, đây chính là nơi sẽ đăng ký nhận dữ liệu với Dispatcher.
- Controller Views - Chính là các React Components, làm nhiệm vụ nhận các trạng thái (state) 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
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).
- 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.
Xây dựng module shopping cart với Flux
Trong bài viết này, tôi không đi quá sâu về lý thuyết và phân tích chi tiết về Flux. Tôi muốn tập trung vào việc xây dựng một ứng dụng nhỏ để có một 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 đượ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à khistate
thay đổi thì Component sẽ được Render lại, cònprops
thì không.
Bạn có thể xem đầy đủ Source của Tutorial này tại đây.
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).
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ủareactify
để chuyển code từ jsx sang js, đồng thời copyapp.js
sangdist/js
.copy
: chỉ làm nhiệm vụ copyindex.html
từsrc
sangdist
.
Để 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ủaAction
, để thuận tiện cho việc đặt tênAction
chúng ta quản lý thông quaAppConstants
(chủ yếu để quản lý các text tĩnh đặt tên choAction
):
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 tôi đã mô tả qua một flow đơn giản của ứng dụng với mô hình Flux
. 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 Tutorial nho nhỏ này sẽ giúp ích cho các bạn trong bước đầu tìm hiểu về mô hình Flux
.
Nguồn tham khảo
All rights reserved