Tự tạo một Single Page Application với React JS và kiến trúc Flux
Bài viết được dịch từ một tutorial của trang web codementor.io . Lời nói đầu Bài viết React.js này sẽ hướng dẫn bạn cách tạo một ứng dụng Todo đơn giản sử dụng React JS và kiến trúc Flux. Trong thời gian gần đây, React JS đã tạo tiếng vang lớn trong cộng đồng lập trình viên bởi hiệu năng tuyệt ...
Bài viết được dịch từ một tutorial của trang web codementor.io .
Lời nói đầu
Bài viết React.js này sẽ hướng dẫn bạn cách tạo một ứng dụng Todo đơn giản sử dụng React JS và kiến trúc Flux. Trong thời gian gần đây, React JS đã tạo tiếng vang lớn trong cộng đồng lập trình viên bởi hiệu năng tuyệt vời khi so sánh với các thư viện khác (ví dụ như Angular JS), đặc biệt khi sử dụng để liệt kê các danh sách. Vì vậy, tôi cảm thấy rất hứng thú với việc tạo ra một ứng dụng đơn giản và dễ hiểu đối với người dùng.
Một trong những điểm mạnh của React JS là virtual DOM - thứ nằm ẩn bên trong mỗi view và là lí do khiến cho React đạt được hiệu năng tốt. Khi một view yêu cầu gọi, tất cả mọi thứ sẽ được đưa vào trong một bản sao ảo của DOM. Sau khi việc gọi hoàn thành, React tiến hành một phép so sánh giữa DOM ảo và DOM thật, và thực hiện những thay đổi được chỉ ra trong phép so sánh trên.
Bạn hi vọng sẽ học được gì sau khi đọc xong bài viết này ? Bạn sẽ hiểu được những kiến thức cơ bản của việc xây dựng nhiều view trong React, bạn sẽ thấy được một ví dụ cụ thể của việc kiến trúc Flux làm thế nào để cho phép truy nhập dữ liệu, và cuối cùng, bạn sẽ thấy cách mà tất cả các cấu trúc trên kết hợp với nhau ra sao để có thể tạo thành một ứng dụng Todo đơn giản.
Flux là một phương pháp lấy dữ liệu từ server API của bạn trong khi cung cấp một giao thức một chiều để duy trì một cách nghiêm ngặt sự tách rời (decoupling) giữa các component ở phía client của bạn. Ứng dụng Todo sẽ sử dụng Flux để lấy dữ liệu từ một server API giả. React JS và Flux hỗ trợ nhau tương đối tốt, cũng bởi vì chúng được tạo ra để làm việc đó, nhưng không có nghĩa là chúng phụ thuộc vào nhau. Vì thế, tôi sẽ chia bài viết này làm một phần liên quan tới mảng React JS, nơi tôi sẽ chỉ ra làm thế nào để tạo ra các views và các phương pháp tốt nhất, tiếp theo đó, chúng ta sẽ tìm hiểu cách tôi lấy dữ liệu từ server API giả của tôi thông qua Flux.
Nếu bạn muốn lấy ngay source code của Todo application, thì tôi đã đưa chúng lên Github. Hãy xem ở đây.
React JS
Đầu tiên, nếu bạn chưa bao giờ nghe tới React JS, đó là một View Renderer tập trung vào hiệu năng, và được viết ra bởi những anh chàng Facebook. MVVM frameworks phải mất nhiều công sức để gọi ra lượng lớn dữ liệu dành cho các contender chẳng hạn như là các danh sách, nhưng đó không phải là vấn đề đối với React bởi nó chỉ gọi ra những gì đã thay đổi. Ví dụ như, nếu một user đang xem một danh sách của 100 item được gọi ra bằng React, và bằng cách nào đó anh ta thay đổi nội dung của item thứ ba, thì chỉ có item đó được gọi lại, 99 item còn lại vẫn nằm nguyên đó mà không có gì thay đổi. Nó cũng sử dụng một thứ mà Facebook gọi là "DOM ảo" (virtual DOM) để tăng tốc hiệu năng, gọi toàn bộ nội dung vào DOM ảo rồi so sánh với DOM thật để tạo ra một patch (có thể dịch là một "miếng vá"). React sử dụng các file JSX (tuỳ chọn) để viết các view, điều đó có nghĩa là JavaScript và HTML có thể được viết trên cùng một file. Đó là một sự thay đổi nhỏ và bạn cần mất chút thời gian để làm quen với nó. Vì sử dụng JSX là tuỳ chọn nên bạn không nhất thiết phải động đến nó, nhưng sẽ dễ dàng hơn cho bạn khi gọi ra các component bằng JSX, vì thế tôi nghĩ bạn nên sử dụng nó.
React hoạt động thông qua các Class. Để gọi một vài HTML, đầu tiên bạn cần tạo một React class biểu diễn những gì cần gọi ra. Đây là một ví dụ của React class.
var HelloWorld = React.createClass({ render: function() { return <div>Hello, world!</div>; } }); React.render(new HelloWorld(), document.body);
Đầu tiên chúng ta tạo ra React class có tên là HelloWorld. Bên trong đó, ta chỉ ra một function: render. Đó là những thứ sẽ được gọi đến khi ta lấy ra HTML bên trong nó - hãy xem ở dòng cuối cùng. Bạn sẽ nhận ra rằng render function ở bên trong class của chúng ta bao gồm cả HTML; đây là nơi JSX hoạt động. Nó cho phép ta viết HTML bên trong file JavaScript thay vì đặt nó ở một file riêng biệt. Dòng cuối cùng chỉ ra rằng tôi muốn class HelloWorld sẽ được gọi ra trong phần body của document.
Props
Vậy điều gì xảy ra khi ta muốn đưa một vài dữ liệu vào trong class React. Ví dụ phía trên không cung cấp cho ta điều gì về vấn đề này, vậy hãy đọc đoạn code dưới đây.
var HelloWorld = React.createClass({ render: function() { return <div>Hello, {this.props.name}!</div>; } }); React.render(new HelloWorld({ name: "Chris Harrington" }), document.body);
Tôi giới thiệu tới các bạn các tham số props trong React class. Chúng được sử dụng để chuyển dữ liệu vào trong các view của React. Mọi sự thay đổi của một biến props sẽ khởi động cho một lời gọi từ phần tương ứng tới view. Những sự thay đổi đó có thể đến từ bên trong chính view đó, hoặc từ view cha, hay có thể từ một view bất kỳ mà view của chúng ta có. Điều này khá tiện lợi khi nó cho phép chúng ta đưa dữ liệu tới các view khác nhau mà vẫn giữ được sự đồng bộ của chúng. Trong đoạn code phía trên, tôi đã chuyển tên của mình vào trong class HelloWorld - class đã được gọi bên trong một div sử dụng biến props.
State
var HelloWorld = React.createClass({ getInitialState: function() { return { counter: 0 }; }, increment: function() { this.setState({ counter: this.state.counter+1 }); }, render: function() { return <div> <div>{this.state.counter}</div> <button onClick={this.increment}>Increment!</button> </div>; } }); React.render(new HelloWorld(), document.body);
Đoạn code này giới thiệu về một nội dung khác của React class: state. State của một class React cho phép chúng ta theo dõi được nhưng sự thay đổi bên trong view. Cũng giống như props, mọi sự thay đổi của state kéo theo việc khởi động một lời gọi tới element tương ứng trên view, với một điều kiện: bạn phải gọi phương thức setState, giống như được viết trong function increment của class. Hãy luôn sử dụng setState! Nếu không có nó, những sự thay đổi của bạn sẽ không ảnh hưởng tới view. Function getInitialState bắt buộc phải có khi sử dụng internal state. Nó chỉ ra state ban đầu của view như thế nào. Hãy luôn chắc chắn rằng bạn đã đưa function này vào trong class kể cả khi state ban đầu của bạn không có gì, một state rỗng vẫn được coi là một state!
Vậy khi nào bạn sử dụng props hoặc state, hay chỉ là các biến private ? Props được sử dụng để gửi dữ liệu giữa các class React cha và con, mọi sự thay đổi của prop dẫn tới một lời gọi tự động của view, cả cha và con. Đối với các dữ liệu chỉ dành cho view, hãy dùng state. Mọi sự thay đổi ở đây cũng sẽ gọi tới view. Đối với các dữ liệu chỉ dành cho class mà không liên quan tới view, bạn có thể sử dụng một biến private, nhưng có lẽ tôi vẫn dùng state; vì nó cũng dành cho những trường hợp này mà!
Nested Views
Một trong những điều khiến cho React trở nên dễ sử dụng là khái niệm các nesting view. Chúng ta có thể gọi các React class từ bên trong của các React class khác, chẳng hạn như:
var FancyButton = React.createClass({ render: function() { return <button onClick={this.props.onClick}> <i className={"fa " + this.props.icon}></i> <span>{this.props.text}</span> </button> } }); var HelloWorld = React.createClass({ getInitialState: function() { return { counter: 0 }; }, increment: function() { this.setState({ counter: this.state.counter++ }); }, render: function() { return <div> <div>{this.state.counter}</div> <FancyButton text="Increment!" icon="fa-arrow-circle-o-up" onClick={this.increment} /> </div>; } });
Ở đây ta đã trừu tượng hoá một button để tăng giá trị của một biến đếm bằng cách định nghĩa nó trong một React class riêng biệt. Phần này cũng biểu diễn cho ta thấy làm các nào props có thể được gán thông qua các nested class.
Chú ý: Vì "class" là một key word bị giới hạn trong JavaScript, viết các class CSS trong HTML của React class được thực hiện bằng cách sử dụng dòng chữ "className". Bạn có thể thấy điều này khi tôi set CSS class cho icon ở trong FancyButtion.
User-built View Function
Có một vài function khác mà bạn cần biết khi viết các React view.
- componentWillMount: Function này được gọi khi một view được thêm vào trong view cha. Đây là một ứng viên tốt để bạn tạo ra một vài thiết lập ban đầu cho view của mình, hoặc để móc nối vào các event handler. Nó cũng rất tiện lợi khi chúng ta xây dựng theo kiến trúc Flux ở phần tới.
- componentWillUnmount: Ngược lại với componentWillMount, nó được sử dụng khi view không cần thiết phải gọi ra ở view cha nữa. Cũng rất tiện lợi để móc nối với các event handler.
Bạn có thể xem danh sách đầy đủ của các phương thức bạn có thể sử dụng tại React JS documentation.
Predefined View Functions
- setState: Như đã nhắc tới ở trên, đây là phương thức bạn gọi để thiết lập state bên trong của React view. Nếu bạn thiết lập state một cách trực tiếp (ví dụ, this.state.foo = "bar") view của bạn sẽ không được gọi. Hãy luôn sử dụng setState (ví dụ, this.setState({foo: "bar"}).
- forceUpdate: Đây là một phương thức tiện cho việc gọi cưỡng bức một view (force render). Nó tiện trong các trường hợp khi bạn đang cập nhật một vài biến bên trong view nhưng nó lại không phải là props hay state.
Tôi đã giới thiệu cho bạn những điểm cơ bản về React JS. Bây giờ chúng ta hãy tìm hiểu kiến trúc Flux hoạt động như thế nào và sau đó ta sẽ chuyển tới ứng dụng Todo.
Flux
Flux là một kiến trúc được Facebook khuyến khích sử dụng như là một cách để lấy dữ liệu cho phía client từ một store hoặc remote server. Nó là một luồng dữ liệu vô hướng. Ở level cao, một user khởi tạo một action được view xử lý bằng cách gửi một request dữ liệu tới store. Sau đó, store đó thực thi request và khi dữ liệu được nhận, nó phát ra một event thông báo với tất cả rằng nó đang nghe (listening). Các listener cập nhật view của chúng sao cho phù hợp. Sau đây là các component chính:
- View: View có trách nhiệm xử lý một action của user, như là lấy một danh sách các Todo item. Nó làm việc này bằng cách gửi một request lấy data thông qua dispatcher.
- Dispatcher: Dispatcher có hai nhiệm vụ chính:
- Đăng ký một callback sau một dispatch. Một store sẽ đăng ký một callback cùng với dispatcher để mỗi khi có mỗi action được gửi đi, store sẽ được thông báo và có thể kiểm tra xem nó có cần thiết phải thực hiện action nào không. Ví dụ, một TodoStore class đăng ký với dispatcher rằng khi nào có một action lấy các Todo được gửi tới, nó sẽ hiểu rằng cần phải bắt đầu thực hiện việc lấy dữ liệu.
- Gửi các action cần được thực hiện. Một view gửi một action để lấy dữ liệu thông qua method dispatch ở dispatcher. Mõi callback được đăng ký sử dụng key giống nhau sẽ được thông báo trên một dispatch. Phương thức dispatch cũng thường bao gồm mọi thông tin cần thiết về dung lượng, ví dụ nó cũng giống như một ID khi lấy một đoạn dữ liệu cụ thể nào đó.
- Store: Store cũng có hai nhiệm vụ:
- Hoạt động khi có một lời gọi được gửi đến để lấy dữ liệu. Điều này được thực hiện thông qua việc đăng ký với dispatcher, thường là khi bắt đầu được tạo ra.
- Báo với các listener về sự thay đổi ở dữ liệu của store sau khi các thao tác lấy, cập nhật hoặc tạo dữ liệu. Điều này được thực hiện bằng event emitter.
- Event Emitter : Event emitter có trách nhiệm thông báo cho các subcriber sau khi một store hoàn thành một action đối với dữ liệu.
Dispatcher
Dispatcher có nhiệm vụ nhận request từ view cho action và gửi nó tới các store tương ứng. Mỗi store sẽ đăng ký với dispatcher để nhận được cập nhận khi một action được gửi. Constructor của một store đăng ký một callback với dispatcher. Điều này có nghĩa là cứ mỗi khi một action được gửi đi, callback này sẽ được thực thi, nhưng chỉ những action nào được thiết lập tương ứng với dispatcher mới được thực thi. Rất may, React bower package cung cấp một class dispatcher cho chúng ta sử dụng, do đó ta không cần phải xây dựng gì khó khăn ở đây. Dispatcher là bước đầu tiên trong quá trình truy nhập dữ liệu ở phía client.
Store
Store được sử dụng để lấy, cập nhật hoặc tạo dữ liệu mỗi khi action được gửi tới nó. Constructor của store sẽ móc nối với một function callback thông qua phương thức đăng ký của dispatcher, cung cấp một lối vào một chiều tới store. Store sau đó sẽ kiểm tra để xem action loại nào đã được gửi tới và nếu nó được chấp nhận ở store, phương thức tương ứng sẽ được thực thi.
Ví dụ
Đây là một ví dụ nhanh về việc Flux hoạt động như thế nào ở một ứng dụng React sample.
var Count = React.createClass({ getInitialState: function() { return { items: [] }; }, componentWillMount: function() { emitter.on("store-changed", function(items) { this.setState({ items: items }); }.bind(this)); }, componentDidMount: function() { dispatcher.dispatch({ type: "get-all-items" }); }, render: function() { var items = this.state.items; return <div>{items.length}</div>; } }); var Store = function() { dispatcher.register(function(payload) { switch (payload.type) { case: "get-all-items": this._all(); break; } }.bind(this)); this._all = function() { $.get("/some/url", function(items) { this._notify(items); }.bind(this)); } this._notify = function(items) { emitter.emit("store-changed", items); }); }; var ItemStore = new Store();
View đơn giản của chúng ta sẽ gọi ra số lượng của các item trong một danh sách. Khi được gọi lên, nó móc nối vào event emitter để xem xem store thay đổi lúc nào, và sẽ gửi một request để lấy các Todo item. Khi request được gửi tới, nó thực hiện một ajax request nhanh và khi nó quay lại, nó sẽ báo cho tất cả các subcribers thông qua event emitter. Quay trở lại view, trang thái được cập nhật với danh sách item mới và gọi ra view, hiển thị số đếm đã được cập nhật.
Ứng dụng Todo
Chúng ta đã nắm được một số kiến thức cơ bản về cách viết React view và kiến trúc Flux là gì, bây giờ ta sẽ tìm hiểu về ứng dụng Todo mà tôi đã tạo ra dành riêng cho bài viết này.
Views
Chúng ta hãy bắt đầu với main view. Ứng dụng này chỉ có một page duy nhất, đó chính là main view.
Todo
"use strict"; var Todo = React.createClass({ getInitialState: function() { return { todos: [] } }, componentWillMount: function() { emitter.on(constants.changed, function(todos) { this.setState({ todos: todos }); }.bind(this)); }, componentDidMount: function() { dispatcher.dispatch({ type: constants.all }); }, componentsWillUnmount: function() { emitter.off(constants.all); }, create: function() { this.refs.create.show(); }, renderList: function(complete) { return <List todos={_.filter(this.state.todos, function(x) { return x.isComplete === complete; })} />; }, render: function() { return <div className="container"> <div className="row"> <div className="col-md-8"> <h1>Todo List</h1> </div> <div className="col-md-4"> <button type="button" className="btn btn-primary pull-right spacing-top" onClick={this.create}>New Task</button> </div> </div> <div className="row"> <div className="col-md-6"> <h2 className="spacing-bottom">Incomplete</h2> {this.renderList(false)} </div> <div className="col-md-6"> <h2 className="spacing-bottom">Complete</h2> {this.renderList(true)} </div> </div> <Modal ref="create" /> </div>; } });
Đây là main view Todo. Khi gọi ra, nó sẽ gửi một request để lấy tất cả các item Todo. Nó cũng nối tới một change event dành cho Todo store để nhận được thông báo mỗi khi store item được cập nhật sau khi request được gửi thành công. Tôi sử dụng một grid của Bootstrap để hiển thị những item ở trạng thái complete và incomplete. Có hai class khác vẫn chưa hiện ra ở đây là List và Modal. `Class trước gọi ra danh sách thực sự của các item và class sau sử dụng để thêm một item mới.
List
var List = React.createClass({ renderItems: function() { return _.map(this.props.todos, function(todo) { return <Item todo={todo} />; }); }, render: function() { return <ul className="list-group"> {this.renderItems()} </ul>; } });
List view có nhiệm vụ gọi ra danh sách các item, cả loại complete và incomplete. Nó sử dụng class Item để gọi các item.
Item
var Item = React.createClass({ toggle: function() { this.props.todo.isComplete = !this.props.todo.isComplete; dispatcher.dispatch({ type: constants.update, content: this.props.todo }); }, render: function() { return <li className="list-group-item pointer" onClick={this.toggle}>{this.props.todo.name}</li>; } });
Item class có trách nhiệm làm hai việc: gọi item trong danh sách và cập nhật trạng thái của một item khi được bấm vào. Tag li có một handler onClick sử dụng phương thức toggle dùng để gửi một request cập nhật item thông qua dispatcher. Item được gửi có 2 đặc tính: type - sử dụng để chỉ tới tên event, và content - sử dụng để chỉ tới event payload, trong trường hợp này là chính Todo item.
Modal
var Modal = React.createClass({ getInitialState: function() { return { value: "" }; }, componentDidMount: function () { this.$el = $(this.getDOMNode()); this.$el.on("hidden.bs.modal", this.reset); emitter.on(constants.changed, function() { this.$el.modal("hide"); }.bind(this)); }, componentWillUnmount: function() { emitter.off(constants.changed); }, show: function () { this.$el.modal("show"); }, reset: function() { this.setState({ value: "" }); }, save: function() { dispatcher.dispatch({ type: constants.create, content: { name: this.state.value, isComplete: false }}); }, onChange: function(e) { this.setState({ value: e.target.value }); }, render: function() { return <div className="modal fade" tabIndex="-1" role="dialog" aria-hidden="true"> <div className="modal-dialog modal-sm"> <div className="modal-content"> <div className="modal-header"> < button type="button" className="close" data-dismiss="modal"> <span aria-hidden="true">×</span> <span className="sr-only">Close</span> </button> <h2 className="modal-title">New Task</h2> </div> <div className="modal-body"> < input placeholder="Task name..." type="text" value={this.state.value}<