ReactJS từ góc nhìn của người lập trình Frontend
Mở đầu Tôi là một lập trình viên thích làm Frontend (chắc là ai cũng biết nó là các thứ như HTML, CSS và Javascipt). Tất nhiên cũng chỉ ở dạng xoàng xĩnh thôi. Ngày tôi bắt đầu học làm Frontend, người ta cũng bảo tôi là nên học Javascipt trước rồi hãy học Jquery. Tuy nhiên, sự đơn giản và ...
Mở đầu
Tôi là một lập trình viên thích làm Frontend (chắc là ai cũng biết nó là các thứ như HTML, CSS và Javascipt). Tất nhiên cũng chỉ ở dạng xoàng xĩnh thôi.
Ngày tôi bắt đầu học làm Frontend, người ta cũng bảo tôi là nên học Javascipt trước rồi hãy học Jquery. Tuy nhiên, sự đơn giản và tiện dụng của Jquery đã khiến tôi làm điều ngược lại, học Jquery và chỉ quay lại tìm hiểu thêm về Javascipt (không có Jquery) khi cần thiết.
Càng học và làm việc với Jquery và các thư viện của nó nhiều tôi càng thấy Jquery toàn năng. Đúng như tiêu chí "Write Less, Do More", Jquery (và các thư viện đi kèm) rất dễ đọc, dễ viết, dễ sửa, cộng đồng sử dụng lớn... Và tôi dùng Jquery để giải quyết hầu hết các vấn đề gặp phải trên Frontend.
Gần đây, người ta hay nhắc đến và tán dương React - một thư viện Javascript được phát triển bởi Facebook. Sau khi tham dự một buổi seminar ngắn của một bạn cùng công ty về React, tôi bị ấn tượng khá mạnh về VIRTUAL DOM và DATA FLOW của thư viện này. Tôi hình dung ra những bài toán Frontend mình gặp từ trước đến nay, và có một số vấn đề có vẻ khá sáng sủa nếu làm bằng React. Vì vậy tôi quyết định đặt ra một bài toán nhỏ và thử giải quyết bằng React - một người bạn mới gặp lần đầu.
Bài toán
Viết một trang HTML mô tả màn hình Cart (giỏ hàng) của một trang bán hàng trực tuyến. Người dùng có thể thêm /xoá mặt hàng, thay đổi số lượng, giá tiền của từng loại mặt hàng và số tiền tổng sẽ được cập nhật tự động khi thêm/xoá hay thay đổi số lượng.
Hình vẽ dưới là hình dung về trang HTML đơn giản này, người dùng có thể thêm sản phẩm bằng cách chọn trong selection box, xoá bằng nút [X], thay đổi số lượng bằng các nút [+] và [-]. Giá ở cột Price và hai vị trị Total sẽ được cập nhật ngay khi có thay đổi trong giỏ hàng.
Để có một hình dung tốt nhất về bài toán, đây là link của sản phẩm đã hoàn thành.
https://jsfiddle.net/bs90/189txdh2/29/embedded/result/
Bài viết này tôi chỉ đi sâu vào chia sẻ luồng tư duy của cá nhân tôi trong việc giải quyết bài toán này với React, chứ không đi sâu vào việc phân tích viết code thế nào và tại sao nó chạy.
Giải quyết bài toán
Nếu như bình thường, việc đầu tiên tôi sẽ làm là viết HTML và CSS, sau đó bắt đầu viết JavaScript( Jquery). Tuy nhiên, việc đầu tiên cần làm với React là thiết kế các Vitual DOM.
Ai biết về HTML chắc hẳn cũng sẽ biết khái niệm DOM (Document Object Model), ví dụ đơn giản nhất là một trang HTML cũng là một DOM đã được trình duyệt dịch ra và hiển thị, một cấu trúc cây gồm gốc là thẻ html, hai con là thẻ head và thẻ body, head thì có con là thẻ title, meta, body thì có con là thẻ div, a... Thông thường chúng ta sẽ viết Javascipt tác động trực tiếp lên các thành phần của DOM trên view để thay đổi chúng.
React cung cấp một khái niệm tên là Vitual DOM. Vitual DOM - DOM ảo đúng như tên gọi của nó, chứa toàn bộ các thông tin cần thiết để tạo nên một DOM, tuy nhiên lại nằm hoàn toàn trong code Javascript cho phép chúng ta tạo, xoá, sửa, thực hiện đủ loại thao tác trên nó trước khi được render thành (real)DOM trên view.
Tại sao nên dùng Vitual DOM? Bạn có thể search trên VIBLO. Đã có sẵn một số bài viết về vấn đề này.
Chính vì dùng Vitual DOM nên file HTML của tôi lần này sẽ chẳng có nội dung gì trong thẻ body cả, điều kỳ lạ đầu tiên!
<html> <head> <title>ReactJS - VibloCart</title> <link rel="stylesheet" type="text/css" href="style.css"> <script src="../js/react-0.13.0.js"></script> <script src="../js/JSXTransformer-0.13.0.js"></script> <script type="text/jsx" src="script.jsx"></script> </head> <body> </body> </html>
Thiết kế cấu trúc Vitual DOM
Quay lại việc thiết kế các Vitual DOM đã nói ở trên. Việc thiết kế này, hay là việc tách các thành phần trong nội dung trang ra thành các React Class riêng theo cấu trúc cây, giúp cho ta có một hình dung mạch lạc về luồng dữ liệu (DATA FLOW) của bài toán.
Lần này tôi thiết kế như hình vẽ dưới đây.
Root là CartDiv, nó chính là cái sẽ được render vào trong thẻ body của trang. CartDiv có hai con là Banner và ItemTable. Banner là phần banner của trang, có phần title và phần tổng giá tiền TOTAL, phần này đơn giản nhất là cho vào trong CartDiv luôn, nhưng tôi muốn tách ra để có cơ hội nắm rõ về Data Flow của React. ItemTable là phần bảng còn lại, mỗi thẻ tr chứa thông tin của một sản phẩm tôi lại cho thành 1 con của ItemTable, tên là OneItem.
Giải thích về Data Flow thì không có gì dễ bằng bắt tay vào code và thuyết mình về nó. Đầu tiên là phần CartDiv
var CartDiv = React.createClass({ render: function() { return ( <div className={"viblo-cart"}> <Banner /> <ItemTable /> </div> ); } }); React.render(<CartDiv />, document.body);
Đó là trạng thái sơ khai nhất, cho chúng ta nhìn thấy là CartDiv chứa hai React Class con là Banner và ItemTable. Bây giờ chúng ta xem xét về data, one-way binding của React yêu cầu data phải truyền từ cha xuống con, data ở đây không chỉ là các biến mà còn là cả các method nữa. Chúng ta phân tích từ từ xem cần truyền gì từ CartDiv xuống cho Banner và ItemTable?
Banner thì đơn giản, truyền cho nó cái tổng tiền là xong. ItemTable thì phức tạp hơn một chút, ngoài tổng tiền thì còn phải truyền thông tin về sản phẩm đang ở trong giỏ hàng kèm theo số lượng và thông tin sản phẩm chưa được mua nữa.
Thêm những phần đó thì class CartDiv sẽ thành như sau.
... <Banner total_cost={this.state.total_cost} /> <ItemTable total_cost={this.state.total_cost} left_items={this.state.left_items} items={this.state.items} /> ...
total_cost, items và left_items sẽ được khởi tạo trong hàm getInitialState
var CartDiv = React.createClass({ getInitialState: function() { var items = []; // Ban đầu chưa có hàng nào được chọn var total_cost = caculate_total_cost(items); // Hàm caculate_total_cost là hàm để tính tổng số tiền hiện tại var left_items = [{id:1, name:"Framgia Super Server", price:5000}, {id:2, name:"Framgia Super Laptop", price:4000}, {id:3, name:"Framgia Super Watch", price:3000}, {id:4, name:"Framgia Super Keyboard", price:2000}, {id:5, name:"Framgia Super Mouse", price:1000}]; // Set sẵn 5 sản phẩm ví dụ trong kho hàng return {items: items, left_items: left_items, total_cost: total_cost}; // Khi trang vừa load state của CartDiv sẽ thành thế này }, ...
Tạm ổn như thế, chúng ta sẽ đi xây dựng hai class con Banner và ItemTable. Banner thì quá đơn giản như sau.
var Banner = React.createClass({ render: function() { return ( <div className={"banner"}> <span className={"page-title"}>YOUR VIBLO CART</span> <span className={"total-cost"}>TOTAL: {this.props.total_cost}$F</span> </div> ); // Sau mỗi lần được render lại, tổng số tiền sẽ được cập nhật bằng total_cost mới được truyền lên. Nhân tiện để thiết lập class (để viết cSS chẳng hạn) ta dùng attribute className } });
Về ItemTable thì tôi thiết kế nó là một HTML Table, nhận thấy khi có sản phẩm thì cấu trúc của các thẻ tr hiển thị một sản phẩm là như nhau. Vì vậy tôi quyết định viết một class khác tên là OneItem. Phần ItemTable tôi viết như dưới đây.
var ItemTable = React.createClass({ render: function() { // Hàm rederItem để render 1 dòng, tương ứng với một sản phẩm có trong giỏ hàng hiện tại. Truyền cho nó thông tin cần thiết từ cha của nó là ItemTable var renderItem = function(itemData, index) { return ( <OneItem key={itemData.id} itemIndex={index} itemData={itemData} /> ); }.bind(this); var left_items_html; // Phần HTML thể hiện những sản phẩm còn lại trong kho, khi không có gì nó chỉ là dòng text "You're awsome!", còn không nó là 1 form với selection box và nút submit. if (this.props.left_items.length == 0) { left_items_html = "You're awsome!"; } else { left_items_html = ( <form> <select> {this.props.left_items.map(function(item){return <option key={item.id} value={item.id}>{item.name}</option>;})} </select> <button>ADD ITEM</button> </form> ); }; return ( <table> <thead> <tr key={0}> <th>No</th><th>Item name</th><th>Unit Price</th><th>Quantity</th><th>Price</th><th>Action</th> </tr> </thead> <tbody> {this.props.items.map(renderItem)} </tbody> <tfoot> <tr key={1000}> <td></td><td>{left_items_html}</td><td></td><td>TOTAL</td><td>{this.props.total_cost}$F</td><td></td> </tr> </tfoot> </table> ); } });
OneItem thì khá đơn giản, nó chỉ là 1 dòng tr với số thứ tự, thông tin sản phẩm và các nút [+], [-] để tăng giảm số lượng, nút [X] xoá sản phẩm mà thôi.
var OneItem = React.createClass({ render: function() { return ( <tr> <td>{this.props.itemIndex + 1}</td> <td>{this.props.itemData.name}</td> <td>{this.props.itemData.price}$F</td> <td><button>-</button> {this.props.itemData.quantity} <button>+</button></td> <td>{this.props.itemData.price*this.props.itemData.quantity}$F</td> <td><button>X</button></td> </tr> ); // Như các bạn thấy, hàm render dùng những giá trị được gửi xuống từ cha để render ra nội dung trả về của OneItem. Khi có thay đổi về các giá trị này, OneItem cũng sẽ đượcc cập nhật theo ngay lập tức. } });
Như vậy là chúng ta thiết kế xong cấu trúc theo hình vẽ bên trên. Chạy thử bây giờ nó sẽ ra trạng thái đầu tiên của link https://jsfiddle.net/bs90/189txdh2/29/embedded/result/, tuy nhiên chả button nào hoạt động cả. Đó chính là việc tiếp theo chúng ta cần làm, làm cho các button hoạt động.
Thiết kế sự kiện (event) - làm cho các button hoạt động
Nếu mà cứ giữ như trên thì viết luôn 1 cái trang HTML tĩnh là xong, chả phải mất công chia chác làm gì cả. Giờ là lúc làm cho mọi thứ hoạt động.
Chúng ta cùng nhau phân tích các sự kiện có thể diễn ra trong trang hiện tại. Cũng không nhiều.
- Sự kiện thêm hàng vào giỏ
- Sự kiện xoá hàng khỏi giỏ
- Sự kiện thay đổi số lượng của từng mặt hàng trong giỏ
Như đã nói ở trên, các method cũng sẽ được truyền từ cha xuống con, vì vậy chúng ta sẽ viết các method ứng với mỗi sự kiện ở class CartDiv.
... // Thêm sản phẩm được chọn vào giỏ, xoá sản phẩm đó ra khỏi kho hàng còn lại, render lại CartDiv AddItemHandle: function(id) { var left_items = this.state.left_items; var item = left_items.filter(function(item){return item.id==id})[0]; left_items.splice(left_items.indexOf(item), 1); item.quantity = 1; var items = this.state.items.concat(item); this.setState({left_items: left_items, items: items, total_cost: caculate_total_cost(items)}); }, // Xoá sản phẩm được chọn ra khỏi giỏ, thêm lại sản phẩm đó vào kho hàng còn lại, render lại CartDiv RemoveItemHandle: function(id) { var items = this.state.items; var item = items.filter(function(item){return item.id==id})[0]; items.splice(items.indexOf(item), 1); delete item.quantity; var left_items = this.state.left_items.concat(item); this.setState({left_items: left_items, items: items, total_cost: caculate_total_cost(items)}); }, // Tăng/Giảm số lượng sản phẩm tương ứng, render lại CartDiv QuantityHandle: function(item_index, method) { var items = this.state.items; if (method == "plus") { items[item_index].quantity++; } else if(this.state.items[item_index].quantity > 0) { items[item_index].quantity--; } this.setState({items: items, total_cost: caculate_total_cost(items)}); } ...
Và 3 methods này sẽ được truyền xuống cho các con cần dùng đến nó.
... <ItemTable quantityHandle={this.QuantityHandle} addItemHandle={this.AddItemHandle} removeItemHandle={this.RemoveItemHandle} total_cost={this.state.total_cost} left_items={this.state.left_items} items={this.state.items} selected_id={this.state.left_items[0] && this.state.left_items[0].id} /> ...
... <OneItem key={itemData.id} removeItemHandle={this.props.removeItemHandle} quantityHandle={this.props.quantityHandle} itemIndex={index} itemData={itemData} /> ...
Chuẩn bị xong phần methods, bây giờ chúng ta làm nốt phần cuối cùng, bắt các sự kiện vào các methods đó. Cùng xem lại các sự kiện có thể xảy ra.
- Sự kiện thêm hàng vào giỏ → Chọn ở selection box (1) và ấn nút submit form (2)
- Sự kiện xoá hàng khỏi giỏ → Ấn vào nút [X] (3)
- Sự kiện thay đổi số lượng của từng mặt hàng trong giỏ → Ấn vào nút [+] hoặc [-] (4)
Vậy là có 4 events tất cả. Chúng ra bổ sung code cho 4 event này.
// (1) ... onChange: function(e) { this.props.selected_id = e.target.value; } ... <select onChange={this.onChange}> ...
// (2) ... handleSubmit: function(e) { e.preventDefault(); this.props.addItemHandle(this.props.selected_id); } ... <form onSubmit={this.handleSubmit}> ...
// (3) ... <td><button onClick={this.props.removeItemHandle.bind(null, this.props.itemData.id)}>X</button></td> ...
// (4) ... <td> <button onClick={this.props.quantityHandle.bind(null, this.props.itemIndex, "minus")}>-</button> {this.props.itemData.quantity} <button onClick={this.props.quantityHandle.bind(null, this.props.itemIndex, "plus")}>+</button></td> <td> ...
SAVE! And you done! https://jsfiddle.net/bs90/189txdh2/29/embedded/result/
Bạn có thể xem và nghịch source code tôi đã viết tại đây http://jsfiddle.net/bs90/189txdh2/29/
Mọi thứ diễn ra khá dễ dàng phải không các bạn!
Nếu giải quyết bằng JQuery?
Như tôi có nói ban đầu, với tôi JQuery là toàn năng, sau khi biết về React điều này vẫn không có thay đổi. Tuy nhiên lượng vấn đề trên FrontEnd tôi giải quyết bằng JQuery có lẽ sẽ giảm đi, thay vào đó tôi sẽ dùng React.
Ví dụ như bài toán bên trên, nếu làm bằng Jquery, việc xử lý các hành động diễn ra sẽ khá là mệt mỏi. Hành động xoá sản phẩm đi chẳng hạn, ngoài việc tính toán lại hai mảng chứa sản phẩm như làm bằng React, chúng ta sẽ phải cập nhật hai giá trị Total, tìm đúng hàng chứa sản phẩm để xoá nó đi khỏi bảng, sau đó còn phải tính toán lại để hiển thị số thứ tự cho đúng. Ngoài ra cũng còn tương đối những rắc rối khác nữa. Thực ra ban đầu rồi định giải quyết bài toán này bằng cả JQuery nữa để có một phép so sánh chính xác nhất. Tuy nhiên nghĩ đến nhức rắc rối trên, tôi quyết định từ bỏ. Trong bài toán này, việc React hoàn toàn vượt trội là điều dễ nhìn thấy.
Tất nhiên bạn có thể thắc mắc rằng, nếu không dùng React vẫn có thể xây dựng được một hàm render tương tự như React, mỗi khi có thay đổi cũng chỉ cần chạy hàm render đó là xong? Không sai, nhưng các kỹ sư của Facebook đã kỳ công nghiên cứu và cải tiến các thuật toán để Vitual DOM của React trở nên siêu nhanh thì tại sao chúng ta không dùng! React nhanh ra sao các bạn có thể tìm trong Viblo và tham khảo các bài viết khác.
Kết luận
Sau một quá trình ngắn tìm hiểu và làm thử React, tôi thấy nó khá phù hợp với một lập trình viên thích làm FrontEnd như tôi. Cấu trúc và cách viết React khá sáng sủa, tuy nhiên lại đòi hỏi người viết có một tư duy lập trình ở một mức nhất định.
Những bài toán nhỏ, những hệ thống chỉ có một chút UI cần dùng đến Javascipt tôi nghĩ không nên dùng React. React thích hợp hơn với những hệ thống vừa và lớn, yêu cầu khả năng duy trì và mở rộng cao. Việc phân tích cấu trúc, kết nối các thành phần stateless cũng khá đau đầu và mất thời gian, tuy nhiên nếu làm được chúng ta sẽ có một hệ thống được cấu trúc tốt, đẹp và dễ dàng duy trì và mở rộng.
TL,DR? Cảm ơn các bạn đã đọc bài viết của tôi. Chúc các bạn thành công với React!