Flux - Under the hood

Flux là gì ?

Vào giữa năm 2013, các kỹ sư của Facebook giới thiệu đến với thế giới một Framework Javascript hoàn toàn mới, một Framework giúp họ tạo ra các Single Page Application một cách dễ dàng hơn. Nó mang tên React.

Không lâu sau khi ra đời, React nhanh chóng nhận được sự chú ý từ giới công nghệ với những tính chất, khái niệm đặc trưng nhưng đầy thú vị của riêng nó, như Virtual DOM, View Component, JSX ...

Người ta bắt đầu đem so sánh React với những Javascript Framework nổi tiếng và đã có được chỗ đứng khác như EmberJS, BackboneJS hay đặc biệt là AngularJS của người khổng lồ Google. Cũng có nhiều ý kiến được đưa ra là thật không công bằng khi đem React đi so sánh với những Framework MVC như thế. React chỉ đóng vai trò của phần V (View) trong mô hình MVC mà thôi. Nó không hề có phần M (Model) hay C (Controller).

Đương nhiên, khi sử dụng React cho service của mình, bạn có thể sử dụng React đơn thuần mà không cần đến thành phần M hay C. Tuy nhiên, khi mà Application của bạn phát triển và phình to lên, sẽ thật khó để debug cũng như kiểm soát code nếu nó không được tổ chức tốt. Và người ta đã phải tính đến việc: Làm thế nào để sử dụng React một cách hiệu quả ?

Một trong những cách dễ dàng đoán ra nhất đó là sử dụng kết hợp React kết hợp với các MVC Framework khác, chẳng hạn như Backbone, hay thậm chí là Angular. Và trong sự kết hợp đó React đóng vai trò của phần View. Facebook cũng đã từng làm theo cách đó!

Trong hội thảo F8, một conference được tổ chức hàng năm của Facebook nhằm giới thiệu những tính năng, công nghệ mới của mình, được tổ chức vào cuối tháng 4, 2014, Tom Occhino, Engineering Manager của Facebook đã chia sẻ rằng khi mà codebase và sự tổ chức code được mở rộng thì mô hình MVC "trở nên cực kỳ phức tạp một cách cực kỳ nhanh chóng" (Nguyên văn: MVC got really complicated really quickly).

mvc.png

Mô hình MVC trở nên rắc rối đối với một Application lớn như Facebook

Cuối cùng, ông đưa ra một nhận xét, gây khá nhiều tranh cãi, rằng: "MVC DOESN'T SCALE".

Cũng trong bài thuyết trình Hacker Way: Rethinking Web App Development at Facebook này, các kỹ sư của Facebook cũng đã lần đầu tiên giới thiệu một giải pháp cho vấn đề sử dụng React một cách hiệu quả. Họ đề xuất một Kiến Trúc mới, với tên gọi Flux.

Theo các kỹ sư của Facebook thì Flux sẽ giúp cho lượng code đồ sộ của họ trở nên dễ hiểu hơn, dễ debug hơn, và đặc biệt là "có thể đoán biết được" (predictable), điều mà họ vẫn luôn mong muốn.

Như vậy, điều đầu tiên các bạn cần biết đó là: Flux không phải là một Framework, mà là một Architecture. Nó như là một mô hình về cách tổ chức Code, giống như MVC vậy.

flux.png

Mô hình Flux theo cách giải thích của Facebook, rất đơn giản!

Hiện React, cũng như Flux, đang được sử dụng bởi rất nhiều những trang web nổi tiếng, ở các công ty lớn như Atlassian, Yahoo, và đương nhiên là cả Instagram và Facebook nữa.

Trong bài viết này, chúng ta sẽ cùng tìm hiểu về mô hình Kiến Trúc Flux gồm những thành phần gì, chúng hoạt động ra sao, Data Flow trong Flux diễn ra như thế nào, Flux có gì khác so với MVC truyền thống, và cách để tạo ra một Application đơn giản dựa trên Kiến Trúc Flux.

Structure

flux-simple-f8-diagram.png

Flux gồm các thành phần cơ bản sau:

  • Action: Là nơi đăng ký các hàm sẽ được view gọi đến khi cần thiết.
  • Dispatcher: Là nơi trung chuyển, đóng vai trò truyền các lời gọi từ Action đến với Store. Khi một Action được gọi, Dispatcher sẽ broadcast một event đến với tất cả các Store, với các thông tin về Action Type, hay các data cần thiết khác.
  • Store: Là nơi lưu trữ dữ liệu, cũng là nơi duy nhất có thể thực hiện việc thêm, sửa, xoá dữ liệu. Store sẽ lắng nghe các event được truyền đến từ Action thông qua Dispatcher, kiểm tra xem event đó có thuộc quyền xử lý của nó không, và thực hiện những thay đổi dữ liệu cần thiết, tương ứng với từng event. Sau khi thay đổi dữ liệu, Store sẽ phát ra một event khác, không báo về sự thay đổi của nó.
  • View: View lấy dữ liệu từ hàm getter được cung cấp bởi Store, và có nhiệm vụ hiển thị những dữ liệu đó. Ngoài ra View còn lắng nghe event từ Store, mỗi khi Store có sự thay đổi, View tự động được render lại với dữ liệu mới nhất. Khi user tương tác với View và có phát sinh sự thay đổi dữ liệu, View sẽ gọi đến Action cần thiết, để Action thực hiện tiếp nhiệm vụ của mình.

Data Flow

Flux được thiết kế bởi rất nhiều những quy tắc khắt khe liên quan đến Data Flow nhằm đảm bảo thiết kế "một chiều" (Unidirectional Flow).

Mọi thay đổi, hay các Action đều phải đi qua Dispatcher!

Bản thân Store chỉ public hàm getter, chứ không public hàm setter. Những xử lý thay đổi dữ liệu chỉ có thể được gọi bên trong chính Store đó mà thôi. Điều này đồng nghĩa với việc:

  • Bạn không thể thay đổi dữ liệu trong Store trực tiếp từ View.
  • Bạn cũng không thể thay đổi dữ liệu trong một Store này từ một Store khác. Bởi như đã đề cập phía trên, bản thân Store không cung cấp hàm setter cho một đối tượng nào khác gọi. Để thực hiện việc đó, bạn phải thông qua ActionDispatcher.

Tính chất trên khiến cho dữ liệu bên trong Store của bạn trở nên dễ quản lý hơn, và khi có sự thay đổi ta có thể dễ dàng debug xem thay đổi đó đến từ đâu.

Ta có thể thấy rõ hơn về kiến trúc Unidirectional data flow thông qua hình ảnh miêu tả cụ thể về Flux dưới đây:

flux-simple-f8-diagram-explained.png

Thiết kế Unidirectional Data Flow của Flux rất phù hợp với thiết kế one-way data binding của React. Nếu như ở các Javascript Framework nổi tiếng khác như AngularJS, EmberJS ... khác thì two-way data bindings luôn được quảng cáo như một trong những tính năng chủ chốt thì ở Facebook, các kỹ sư lại không nghĩ rằng đó là một ý hay cho hệ thống của họ. Bởi họ cho rằng two-way data bindings sẽ kiến cho "một object update sẽ kéo theo nhiều objects khác update, và từ đó lại trigger thêm nhiều update khác". Với thiết kế Unidirectional Flow, khi mà mọi thay đổi đều đi qua Dispatcher, họ có thể dễ dàng nắm rõ được một thay đổi đó đến từ đâu, và hệ thống của họ sẽ trở nên "predictable"!

Flux vs MVC

Đến đây chắc hẳn sẽ có nhiều bạn đặt ra thắc mắc rằng vậy rốt cuộc thì Flux có gì khác so với MVC.

Nếu nhìn vào biểu đồ Data Flow mà Facebook đưa ra khi nhắc đến mô hình MVC của họ (hình ảnh ở phần đầu bài viết) thì ta có thể thấy họ đã áp dụng MVC theo một cách rất không giống ai, khi mà chỉ có một Controller quản lý tất cả các Model, hay các View và Model tương tác 2 chiều trực tiếp với nhau, không qua Controller. (facepalm) Có vẻ như các kỹ sư của Facebook đã cố tình làm méo đi mô hình MVC rồi cho rằng nó là "doesn't scale" ?

Phải chăng Store trong Flux có vẻ như chỉ là tên gọi khác của Model trong mô hình MVC với vai trò quản lý dữ liệu. Dispatcher có vẻ như là Controller ? Phải chăng Facebook chỉ là đã đổi tên các thành phần của MVC, và đổi tên luôn cả kiến trúc đó thành một kiến trúc của riêng mình với tên gọi Flux ?

Đó đã từng là một chủ đề gây nhiều tranh cãi, được nhiều người quan tâm khi khi mà Flux được giới thiệu. Người ta luôn thắc mắc xem Flux phải chăng cũng chỉ là MVC, Flux khác MVC ở điểm nào, hay nên dùng Flux hay MVC ?

Cá nhân mình thì nghĩ rằng Flux chịu nhiều ảnh hưởng từ thiết kế MVC, nhưng nó cũng có nhiều điểm khác biệt, đủ để tạo ra những nét đặc trưng cho riêng mình, đủ để tạo nên một kiến trúc riêng mang tên Flux.

Vậy Flux đã tạo nên những sự khác biệt đó như thế nào ?

  • Thứ nhất là ở Data Flow. Flux là kiến trúc chỉ chấp nhận Unidirectional Data Flow. Ngược lại ở MVC ta có thể thấy một cách thường xuyên những mô hình Bi-directional Data Flow, nơi mà một thay đổi có thể trực tiếp quay lại trigger một thay đổi khác từ nơi gọi đến nó.
  • Thứ hai, Dispatcher không phải là Controller. Dispatcher hoàn toàn không chứa business logic. Dispatcher đơn giản chỉ đảm nhiệm vai trò là một "trung tâm điều phối", nơi đưa một action đến với mọi Stores. Trong mô hình MVC thì bạn có thể thiết kế bao nhiêu Controller cũng được, còn trong Flux, chỉ cần duy nhất một Dispatcher làm nơi trung chuyển, và mọi action đều đi qua Dispatcher đó. Ngoài ra, Controller trong mô hình MVC có thể gửi câu lệnh để thay đổi trạng thái của Model, hay thay đổi View thì Dispatcher hoàn toàn không thể. Tuy nhiên sự xuất hiện của Dispatcher là quan trọng bởi nó đảm bảo cho thiết kế Unidirectional Data Flow.
  • Thứ ba, Store có thể không quản lý dữ liệu gì, hoặc có thể quản lý nhiều trạng thái của Application, hay quản lý một lúc nhiều object. Trong khi đó, ở mô hình MVC thì Model thường là sẽ quả lý một object riêng biệt.

Implement Flux

Như đã trình bày thì Flux vốn không phải là một Framework, mà là một Kiến Trúc. Facebook trình bày những tư tưởng của kiến trúc đó, và ta có thể tự tạo ra một Framework hay Library theo kiến trúc Flux của riêng mình.

Hiện có khá nhiều thư viện Javascript giúp bạn viết app theo kiến trúc Flux một cách dễ dàng. Có thể kể ra như:

  • flux by Facebook: Facebook có public một cách Implementation của riêng mình, và đặt tên nó là ... Flux. Cấu trúc mà Flux của Facebook đưa ra cũng rất đơn giản, source code chỉ gồm duy nhất 2 files. Mục đích của nó cũng chỉ là để giải thích cho Kiến Trúc Flux là chính.
  • reflux by Mikael Brassman: Mang nhiều tư tưởng của Flux chính thống, nhưng Reflux mạnh dạn loại bỏ thành phần Dispatcher, và giúp Actions gọi thẳng đến Stores. Một library khá là thú vị, và độ phổ biến của nó chỉ đứng sau Flux chính chủ của Facebook mà thôi.
  • alt by Josh Perez: Một library hoàn toàn tuân thủ theo kiến trúc Flux, với hàng loại các tính năng hữu ích bổ sung. Alt được viết bằng ES6 (ECMAScript 6, phiên bản với hàng loạt các tính năng mới của Javascript), và bạn sẽ phải mất ít nhiều thời gian làm quen với ES6 trước khi hiểu về Alt
  • fluxxor by Brandon Tilley
  • fluxible by Yahoo: Như đã biết thì Yahoo là một trong những công ty lớn tích cực sử dụng React nhất. Và họ cũng có một cách Implement kiến trúc của riêng mình.
  • marty.js by James Hollingworth: Một library nữa hỗ trợ viết code React được đơn giản hơn, và giúp bạn tổ chức app của mình theo kiến trúc Flux.

Và còn nhiều libraries khác nữa.

Các libraries trên đều có một điểm chung đó là đều tận dụng React để xử lý phần Views. Như đã trình bày ở trên thì kiến trúc Flux rất phù hợp với các thức hoạt động của React, và Facebook vốn tạo ra Flux để tổ chức lại code React của họ một cách phù hợp, và dễ đoán hơn. Thế nên không có gì khó hiểu khi React là một phần quan trọng và thường xuyên được sử dụng trong các thư viện Flux với vai trò xử lý phần View.

Tuy nhiên, Flux dù gì cũng là một kiến trúc. Bạn hoàn toàn có thể viết một app theo kiến trúc Flux với một cách xử lý View của riêng mình, mà không cần dùng đến React.

Application theo Kiến Trúc Flux

Cùng với việc giới thiệu về Flux, Facebook cũng cung cấp 2 ví dụ về những những application sử dụng Kiến Trúc Flux trên Github. Các bạn có thể tham khảo tại Todo ApplicationChat Application

Còn trong nội dung bài viết này, mình xin được lấy một ví dụ demo đơn giản mà mình viết, sử dụng package flux mà Facebook cung cấp. Mục đích chỉ là để tìm hiểu về cách tổ chức của kiến trúc Flux, thế nên có nhiều chỗ có thể sẽ dài dòng và có thể được cải tiến trong thực tế khi đưa vào ứng dụng.

Đầu tiên ta có thể thấy cấu trúc tổ chức thư mục của một một application Flux sẽ như sau:

flux-app-structure.png

  • actions: Chứa các Actions của app, đây chính là một thành phần trong mô hình Flux.
  • components: Chứa các file views. Dĩ nhiên chúng được viết sử dụng React (viết bằng JSX)
  • dispatcher: Chứa thành phần trung tâm của Flux, Dispatcher.
  • stores: Chứa các file stores.

Nội dung của dispatcher rất đơn giản, nó chỉ gọi ra component Dispatcher đã được cung cấp bởi package flux của Facebook.

var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();

Trong khi đó hãy cùng xem qua nội dung của VoteActions

var AppDispatcher = require('../dispatcher/AppDispatcher.jsx');
var Constants = require('../constants/Constants.jsx');

var VoteActions = {
    voteUp: function() {
        AppDispatcher.dispatch({
            actionType: Constants.ACTION_VOTE_UP
        });
    },
    voteDown: function() {
        AppDispatcher.dispatch({
            actionType: Constants.ACTION_VOTE_DOWN
        });
    },
    voteReset: function() {
        AppDispatcher.dispatch({
            actionType: Constants.ACTION_VOTE_RESET
        });
    }
};

module.exports = VoteActions;

Ta có thể thấy một action trong mô hình Flux chỉ đơn giản là một object của javascript, ở đó nó sẽ đăng kí các action đến Dispatcher. Nội dung gửi đến Dispatcher cũng chỉ đơn giản là một object có chứa Action Type (dữ liệu mà sau này phía Store sẽ dùng để nhận diện action và thực hiện một hành động tương ứng), và ngoài ra có thể thêm cả các thông tin khác mà bên Store cần có để thực hiện thay đổi.

Như vậy là ta đã thấy cách mà Action sẽ gọi đến Dispatcher như thế nào. Sau đó Dispatcher sẽ thực hiện nhiệm vụ broadcast lời gọi của Action đến các Store. Và ở Store, hãy cùng tìm hiểu xem những action được tiếp nhận như thế nào nhé.

var AppDispatcher = require('../dispatcher/AppDispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
var Constants = require('../constants/Constants.jsx');
var assign = require('object-assign');

var CHANGE_EVENT = 'change';

var _votes = {
    up: 0,
    down: 0
};

function _voteUp() {
    _votes.up++;
}

function _voteDown() {
    _votes.down++;
}

function _voteReset() {
    _votes = {
        up: 0,
        down: 0
    };
}

var VoteStore = assign({}, EventEmitter.prototype, {
    getVotes: function() {
        var up_rate = (_votes.up || _votes.down) ? _votes.up / (_votes.up + _votes.down) * 100 : 50;
        var down_rate = 100 - up_rate;
        _votes.up_rate = up_rate.toFixed(2);
        _votes.down_rate = down_rate.toFixed(2);
        return _votes;
    },

    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    }
});

AppDispatcher.register(function(action) {
    switch(action.actionType) {
        case Constants.ACTION_VOTE_UP:
            _voteUp();
            VoteStore.emitChange();
            break;
        case Constants.ACTION_VOTE_DOWN:
            _voteDown();
            VoteStore.emitChange();
            break;
        case Constants.ACTION_VOTE_RESET:
            _voteReset();
            VoteStore.emitChange();
            break;
        default:
        // no op
    }
});

module.exports = VoteStore;

Ta có thể thấy bên trong biến VoteStore chỉ gồm các method getVotes, emitChange, addChangeListenerremoveChangeListener.

Bản thân biến lưu trữ thông tin về danh sách vote_votes cũng nhưng các hàm thay đổi dữ liệu trong _votes như _voteUp(), _voteDown()_voteReset() đều được đặt bên ngoài. Đây chính là cách chúng ta thể hiện concept của Store trong mô hình Flux. Store chỉ cho phép gọi getter, và không cho phép gọi setter từ bên ngoài. Tức ở bên ngoài ta sẽ không thể thay đổi được trạng thái của Store, mà muốn thay đổi, ta cần phải thông qua những callback được register với Dispatcher ở phần dưới.

Còn trong components React, ta sẽ có các đoạn xử lý sau:

    getInitialState: function() {
        return VoteStore.getVotes();
    },

    componentDidMount: function() {
        VoteStore.addChangeListener(this._onChange);
    },

    _onChange: function() {
        this.setState(VoteStore.getVotes());
    },

    _onVoteUpClick: function() {
        VoteActions.voteUp();
    },

    _onVoteDownClick: function() {
        VoteActions.voteDown();
    },

    _onVoteResetClick: function() {
        VoteActions.voteReset();
    },

Ta có thể thấy, ở view component, ta sẽ lấy ra dữ liệu trong Store thông qua hàm Getter của StoregetVotes(). Ngoài ra ta cũng sẽ truyền vào Store hàm callback sẽ được gọi khi mà dữ liệu trong Store bị thay đổi. Trong hàm callback này ta chỉ đơn giản là set lại state cho component. Và khi state bị thay đổi, view sẽ được render lại, đó là công việc của React.

Hãy cùng nhìn lại Data Flow của Flux một cách rõ ràng hơn, khi giả sử như nút Vote Up được click.

// Đầu tiên là sau khi component được mount, sẽ có một callback được đăng ký với VoteStore. Nội dung của callback đó đơn giản là đặt lại giá trị state của component bằng giá trị của votes được lưu giữ trong VoteStore.
componentDidMount: function() {
    VoteStore.addChangeListener(this._onChange);
},

_onChange: function() {
    this.setState(VoteStore.getVotes());
},

// Khi vote up button được click, hàm _onVoteUpClick sẽ được gọi. Nó sẽ trigger hàm voteUp() trong VoteActions
_onVoteUpClick: function() {
    VoteActions.voteUp();
},

// Tiếp theo, ở voteUp() trong VoteActions, ta sẽ gửi một message đến TẤT CẢ các Store thông qua Dispatcher. Ta đồng thời gửi actionType để có thể bên Store có thể nhận biết được.
voteUp: function() {
    AppDispatcher.dispatch({
        actionType: Constants.ACTION_VOTE_UP
    });
},

// Tiếp đó, ở trong VoteStore, ta nhận về message được truyền từ VoteActions, và check xem nó thuộc loại nào. Có đúng là loại mà VoteStore cần xử lý hay không. Ở đây ta sẽ gọi đến hàm _voteUp() để thay đổi dữ liệu, hàm này vốn chỉ có thể gọi bên trong VoteStore mà thôi.
case Constants.ACTION_VOTE_UP:
    _voteUp();
    VoteStore.emitChange();
    break;

// Sau khi xử lý thay đổi data xong, VoteStore tạo ra một event thông báo rằng nó đã thay đổi. Sẽ có một hàm lắng nghe sự thay đổi của VoteStore, bên trong đó ta sẽ gọi đến hàm callback mà ta đã register ở View Component.
emitChange: function() {
    this.emit(CHANGE_EVENT);
},

addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
},

// Hàm callback ở đây chính là hàm set lại state của View Component. Khi state bị đặt lại giá trị, view sẽ thay đổi. Thứ giúp ta thực hiện công việc này chính là React.

// Vậy là ta đã kết thúc một chu trình đơn giản, đi từ action của người dùng, đến lúc update view.

Nếu bạn đã thành thục và hiểu rõ về Flux, hãy tiếp tục xem qua source code của những ví dụ của Facebook nhé, đặc biệt là Chat Application, sẽ rất là thú vị đấy.

Ngoài ra, bạn còn có thể tham khảo một ví dụ khác về cách viết Flux Application sử dụng Library Reflux tại bài viết Flux vs Reflux - The differences

References

  • React and Flux: Building Applications with a Unidirectional Data Flow

  • React.js Conf 2015 - Flux Panel