Hướng dẫn xây dựng ứng dụng ToDo với React

Tuan Anh Le
andy.le
Published in
17 min readJan 2, 2019
Feature Image

Một trong những phương pháp hiệu quả khi muốn tìm hiểu một công nghệ mới là qua việc thực hành.

Trong bài viết này cũng giới thiệu về React theo hướng tiếp cận đó, cụ thể hơn chúng ta sẽ xây dựng một ứng dụng ToDo đơn giản dựa trên thư viên React. Ứng dụng ToDo cho phép người sử dụng tạo hoặc chỉnh sửa (xoá) một danh sách các task (todo item).

Source code của ứng dụng, cũng như kết quả chạy ứng dụng trên trình duyệt có thể tham khảo theo đường dẫn: Github Link

Spoiler

  • Bài viết này dành cho những người mới bắt đầu làm quen với React và có kiến thức cơ bản về JavaScript. Quá trình hướng dẫn có thể kèm theo các hình ảnh và đường dẫn tham khảo trong trường hợp người đọc cần tìm hiểu thêm chi tiết các kiến thức liên quan.
  • Quá trình tìm hiểu & xây dựng một ứng dụng React (cho dù đơn giản), liên quan đến nhiều kiến thức khác nhau trong JavaScript. Mặc dù cố gắng, bài viết sẽ không tránh khỏi sự dài “lê thê”. Vì vậy, hãy chuẩn bị một cốc cafe, sắp xếp khoảng thời gian hợp lý và nghiền ngẫm. Cố gắng tránh việc copy & paste code mà không suy nghĩ.
  • Todo là một ứng dụng thường được sử dụng khi giới thiệu về một ngôn ngữ hay framework lập trình mới. Tuy nhiên, Todo không phải là một ứng dụng đơn giản. Có thể kết hợp nhiều idea trong Todo, ví dụ như Social, Recommendation System, Eco-app, Multi-platform view, etc…Các bài viết trong blog này sẽ cố gắng thể hiện các kiến thức quan trong về JavaScript, cũng như React thông qua các ví dụ liên quan đến ứng dụng Todo.
Ứng dụng todo “thực sự"

It is not about the destination, it is about the journey.

This is ..not our journey. It is way far than that mountain.

Chuẩn bị môi trường phát triển

Hãy bắt đầu ví dụ với việc cài đặt môi trường phát triển:

Bước 1: Cài đặt node package manager (npm).

Đây là một chương trình thư viện quan trọng cho phép chúng ta download và cài đặt các thư viện liên quan đến JavaScript. Việc cài đặt có thể khác nhau tuỳ theo hệ điều hành đang sử dụng. Có thể tham khảo chi tiết về npm và hướng dẫn cài đặt theo đường dẫn sau: [NPM].

Sau khi cài đặt, chúng ta có thể sử dụng terminal để xác nhận version qua hai câu lệnh

node -v
npm -v

Output của câu lệnh (có thể thay đổi tuỳ theo từng trường hợp cụ thể)

Kết quả cài đặt node và npm

Bước 2: Cài đặt công cụ (toolchain) Create-React-App (CRA).

Theo hướng dẫn từ reactjs.org, đây là một công cụ hữu ích cho việc học tập React cũng như cách tốt nhất để tạo ra một ứng dụng Sing-page application dựa trên React.

Việc cài đặt đơn giản thông qua một câu lệnh duy nhất:

npm install create-react-app --global
Kết quả cài đặt toolchain CRA

Ứng dụng web theo mô hình SPA khác với các trang web truyền thống. Giải thích một cách đơn giản, theo mô hình này, SPA chỉ bao gồm một page duy nhất, dựa trên các tương tác của người dùng, SPA sẽ tự động tải (load) các thông tin, hoặc các thành phần khác nhau từ phía server — và — không yêu cầu trình duyệt phải reload lại toàn bộ trang web. Cách làm này giúp cho website SPA hoạt động liền mạch như một ứng dụng thông thường chạy trên desktop

Bước 3: (Không bắt buộc) Cài đặt JavaScript IDE

Trong trường hợp là người mới bắt đầu học JavaScript, bạn có thể sử dụng bất cứ một trình soạn thảo — editor nào để viết chương trình. Bên cạnh các editor, chúng ta cũng có thể sử dụng các IDE (integrated development environment) với các tích năng bổ sung cho việc phát triển ứng dụng. Với JavaScript, có thể liệt kê một số IDE phổ biến:

Tạo một ứng dụng React

Ok, tạm đủ. Sau khi kết thúc bước chuẩn bị môi trường phát triển, chúng ta có thể sử dụng toolchain “Create-React-App” để tạo ra một khung ứng dụng dựa trên React với lệnh:

create-react-app react-todo

Kết quả in ra từ dòng lệnh (khá dài nên tôi chỉ copy phần cuối cùng :) )

Kết quả từ lệnh tao ra một khung ứng dụng với React

Cấu trúc file và folder trong ứng dụng React

Trước khi tiến hành việc xây dựng ứng dụng ToDo, chúng ta cần hiểu qua ý nghĩa một số file và folder trong ứng dụng được tạo ra bởi câu lệnh phía trên.

Danh sách các file & folder cơ bản trong một ứng dụng React
  • package.json: danh sách các module (thư viện) mà ứng dụng sử dụng. Mỗi module thực hiện những chức năng nhất định (react, react-dom & react-scripts).
  • node_modules: lưu trữ source code của các module được liệt kê trong file package.json
  • src: bao gồm các file source code mà chúng ta cần phát triển cho ứng dụng TODO
  • public: bao gồm các static files, ví dụ như stylesheet (css file), images

Để chạy ứng dụng React, bên trong folder react-todo, thực hiện câu lệnh:

npm start

Chú ý: Chúng ta cũng có thể sử dụng câu lệnh yarn start với kết quả tương tự. Yarn cũng là một ứng dụng có chức năng quản lý thư viện như npm, tuy nhiên yarn có khả năng cache lại các thư viện đã được download, đem lại khả năng lưu trữ tốt hơn.

Kết quả trên terminal từ câu lệnh trên:

Kết quả terminal từ lệnh npm start

Kết quả trên cho biết ứng dụng React đã chạy thành công trên localhost với cổng 3000. Thông thường, trình duyệt mặc định của bạn sẽ tự động bật lên:

Thông báo trên kết quả trình duyệt cho biết chúng ta sửa file src/App.js để thay đổi kết quả hiển thị của ứng dụng. File App.js được render từ file index.js. App.js được xem như một container (lưu chứa) cho các component của một ứng dụng React. Thực tế, file index.js có thể sử dụng để render các React component khác, không bắt buộc đi kèm với App.js

Một số chú ý về thuật ngữ:

  • Render: được sử dụng phổ biến trong React. Render là quá trình chuyển đổi một React component vào cấu trúc DOM (Document Object Model) từ đó trình duyệt có thể hiểu và hiển thị kết quả từ component đó.
  • Component: một ứng dụng React được tạo ra từ các thành phần (component). Từ các thành phần nhỏ, chúng ta có thể kết hợp lại để tạo nên các thành phần phức tạp hơn. Đây là một phương pháp thông dụng trong xây dựng phần mềm (component-based approach). Hướng tiếp cận này giúp chúng ta chia nhỏ một ứng dụng thành các phần nhỏ hơn, dễ bảo trì và tái sử dụng cao.
Cách xây dựng trang web hiện đại dựa trên component ;)

And Hello… with React

Để hiểu ứng dụng React một cách đơn giản hơn, chúng ta sẽ thực hiện một số thay đổi trong cấu trúc chương trình và in ra thông điệp “Hello React” trên trình duyệt.

Thay đổi file index.js, App.js

A simple index.js
A simple App.js

Chúng ta đã xoá đi một số thông tin dư thừa bên trong hai file index.jsApp.js. Do đó, cũng có thể xoá thêm một số file khác không cần thiết trong folder react-todo:

  • logo.svg
  • App.css
  • serviceWorker.js

Kết quả in ra trên trình duyệt:

Kết quả hiển thị với thông điệp Hello React trên trình duyệt

Hot Module Replacement (HRM) hay, Hot Reloading: Trong quá trình sửa đổi code trong ứng dụng React, chúng ta không cần stop hay restart lại ứng dụng. Thay vào đó React có khả năng tự động đọc latest file từ việc chỉnh sửa và cập nhật tức thì kết quả trên trình duyệt. Tính năng này gọi là hot reloading, giúp cho việc phát triển nhanh chóng hơn (khi developer có thể thấy ngay được kết quả trong suốt quá trình phát triển ứng dụng).

Với tính năng Hot Reloading, trình duyệt sẽ tự động cập nhật kết quả ngay khi chúng ta thay đổi file index.jsApp.js.

Quay trở lại source code, App.js là một React component (chính xác hơn, là một container component nếu chúng ta sử dụng App.js để lưu chứa các component khác bên trong). Một React component có thể khai báo dưới dạng một JavaScript class hoặc JavaScript function

Class cũng chính là một dạng function object. Việc sử dụng class là một cách thức đơn giản (syntactic sugar) để phát triển object theo tính kế thừa dễ dàng hơn so với prototype. Trong JavaScript, khái niệm class không có ý nghĩa giống như class trong các ngôn ngữ OOP như Java :)

Tham khảo: (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes).

Cũng như mọi React component, App.js “kế thừa” từ Component trong package “react”. App.js bao gồm duy nhất một phương thức render, trả về một phần từ HTML đơn giản (<div>{message}</div>). Khi phương thức render này được gọi, giá trị này được biên dịch, biến {message} được gán bằng giá trị “Hello React”, sau đó đưa kết quả cuối cùng vào cấu trúc DOM, nơi phần tử App được khai báo:

// index.jsReactDOM.render(<App />, /document/.getElementById('root'));// public/index.html....
<div id="root"></div>
....

Chú ý hơn vào biểu thức trả về của phương thức render: <div>{message}</div>. Về bản chất, đây không phải là một giá trị HTML hay một chuỗi string đơn thuần. Biểu thức này dựa trên ngôn ngữ JSX của Facebook, một mở rộng của ngôn ngữ JavaScript. Nó tương đối giống với các template language khác, tuy nhiên có thể tận dụng được toàn bộ các đặc điểm của ngôn ngữ JavaScript.

Bài tập

Trong file index.js, output render của component App được đưa vào vị trí phần tử root trong cấu trúc DOM. Trong file index.html, phần tử root gằn liền với item HTML div với id="root". Tuy nhiên, index.html không chứa script gọi đến file index.js. Hãy giải thích (tìm hiểu) cách thức file index.html liên kết được với file index.js?

Phân tích ứng dụng ToDo

Như giới thiệu, phương pháp phát triển ứng dụng React dựa trên việc xây dựng các thành phần - component. Vì vậy, với ứng dụng ToDo, chúng ta cần phân tích và chia nhỏ các component trước khi tiến hành coding.

Hãy xem xét giao diện ứng dụng ToDo:

Phân tích các component xây dựng nên ứng dụng ToDo

Ứng dụng ToDo có thể chia thành hai component chính:

  • Form component (tạm gọi:TodoList): Sử dụng để add các item todo vào danh sách
  • List component (tạm gọi:TodoItems): Chứa danh sách các item todo tạo nên từ Form component

Form component, chứa hai thành phần, form input và button. List component, sử dụng list item HTML (ul, li). Mỗi task item tương ứng với một phần tử li.

Xây dựng UI prototype

Với việc phân tích cấu trúc giao diện (User Interface — UI) từ bước trước, chúng ta có thể xây dựng một mock up cho ứng dụng với các component đơn giản.

Mock up là quá trình xây dựng một prototype của ứng dụng với những tính năng đơn giản, phù hợp cho việc trình bày ý tưởng thiết kế, chức năng: [Mockup — Wikipedia](https://en.wikipedia.org/wiki/Mockup)

Trong file src\index.css, chúng ta định nghĩa stylesheet cho các thành phần HTML trong ứng dụng.

index.css file — stylesheet

Trong file src\Todolist.js, chúng ta tạo ra một HTML form input và button submit

Trong file src\TodoItems.js, chúng ta tạo ra một danh sách todo item -todoEntries dưới dạng hard-coding (giá trị dữ liệu không thay đổi). Danh sách Todo item là một mảng các JavaScript object, mỗi object đại diện cho một ToDo item với hai thuộc tính: keyindex.

Thông qua function JavaScript map, chúng ta áp dụng function createTasks cho từng phần tử trong danh sách todoEntries. Kết quả trả về là một danh sách các phần tử <li key={keyValue}>{value}</li>, trước khi đưa vào bên trong phần tử ul.

Cuối cùng, hai component TodolistTodoItems được đưa vào trong container component App trong file App.js

Kết quả hiển thị trên trình duyệt với hard-coding data:

Giao diện của prototype ứng dụng

Phân tích mô hình dữ liệu (Data model)

Trong các bước trước, chúng ta đã xây dựng một bản prototype với dữ liệu cố định. Để xây dựng ứng dụng có khả năng tương tác, chúng ta cần tiếp tục phân tích khía cạnh dữ liệu (mô hình dữ liệu trong mỗi component, cách chia sẻ dữ liệu giữa các component), hành vi tương tác của người dùng với các component.

Trong ứng dụng ToDo, các tương tác giữa người dùng với component có thể chia thành các Use Case (kịch bản sử dụng):

  • Use Case 1: Người dùng có khả năng add một item vào ToDo list
  • Use Case 2: Người dùng click vào một item trong danh sách ToDo list để xoá item

Hãy bắt đầu với Use Case 1. Để add một item vào ToDo list, người dùng nhập nội dung item vào form input và click vào submit button trong component TodoList. Để nội dung item này được gửi sang danh sách ToDo list (tương ứng component TodoItems).

Vì vậy, chúng ta cần một nơi (hoặc một phương pháp) để chia sẻ dữ liệu giữa giữa các component.

Có nhiều phương pháp để thực hiện điều này với React, ví dụ: sử dụng Redux store. Trong ví dụ này. chúng ta sử dụng cách làm đơn giản hơn, sử dụng container component App (nơi lưu chứa cho hai component ToDoListToDoItem)

Với phân tích trên, source code của App.js có thể được update lại như sau:

Sử dụng container component App.js làm nơi chia sẻ data giữa các sub-component

Với cách làm này, danh sách các ToDo item được lưu trữ trong mảng state.items của container App.

Trước khi đi tiếp với việc xây dựng ứng dụng, chúng ta cần dừng lại một chút để tìm hiểu về khái niệm state trong React component.

Có hai cách lưu trữ data trong một component: props (viết tắt của properties) và state. Đây đều là hai JavaScript object, đều có khả năng lưu trữ data và ảnh hưởng đến đầu ra của việc render component.

Điểm khác biệt giữa props và state:

  • props được gửi/chia sẻ giữa các component thông qua giá trị các tham số (giống như việc truyền vào giá trị tham số khi gọi function). Giá trị của props không thay đổi (immutable) trong vòng đời của một component. Nếu container thay đổi giá trị đầu vào props của component con, hàm render của component sẽ được gọi lại — re-render, và (may be) cấu trúc DOM nodes sẽ được cập nhật.
  • state: được quản lý trong “nội tại” của từng component (giống như định nghĩa một biến local, tồn tại duy nhất bên trong function). State có thể thay đổi giá trị (mutable) (thông thường do tác động bởi tương tác từ người dùng).

Giải thích và so sánh ý nghĩa giữa props và state, có thể tham khảo chi tiết hơn: Props v.s State

[Cập nhật]: Thực ra nói rằng state chỉ tồn tại bên trong component không thực sự chính xác. Trong proposal của React 16.7-alpha, khái niệm React hook được giới thiệu để tách state khỏi một React component (chính xác hơn, React functional component).

Ok, quay lại với ứng dụng ToDo, danh sách dữ liệu ToDo được chuyển vào mảng state.item của App, sau đó gửi xuống component TodoItems thông qua thuộc tính entries:

Dữ liệu gửi từ container state xuống component's props

Component TodoItems nhận được giá trị entries từ container component App, và tạo ra danh sách các ToDo item:

ToDoItems nhận giá trị cho thuộc tính props từ container App

Một chút lưu ý trong thay đổi source code của ToDoItems: sử dụng destruction assignment để gán giá trị cho biến entries. Destruction assignment là một đặc điểm mới trong ES6, đem lại tính tiện dụng khi cần bóc tách các thuộc tính từ một JavaScript object.

Tạm nghỉ một chút, chúng ta đã đi một nửa chặng đường cho câu hỏi làm sao để chia sẻ data giữa hai component ToDoItemsTodoList. Bằng việc sử dụng container App.js, chúng ta đã có thể gửi dữ liệu từ App.js đến TodoItems thông qua thuộc tính props.

Câu hỏi tiếp theo, làm sao để component ToDoItems gửi dữ liệu form khi submit đến App.js ?

Câu trả lời: Sử dụng callback function.

Cụ thể hơn App.js sẽ cung cấp một function cho component ToDoItems. Khi người dùng click vào button submit, sự kiện (event) này sẽ kích hoạt function này để gửi data cho App.js.

Callback function là một đặc điểm rất thú vị trong ngôn ngữ lập trình. Khái niệm callback kết hợp cùng closure (bounding environment’s variables), giúp cho function callback có thể sử dụng các variable trong môi trường nó được khai báo. Nhờ đặc điểm này, hàm callback trong App.js có thể kết hợp dữ liệu từ form của ToDoItems và danh sách ToDo item hiện tại state.items để tạo ra một danh sách item mới!

Giải thích về callback và closure Closures vượt ra ngoài phạm vi của bài viết này, nhưng rất đang để tìm hiểu chi tiết hơn.

Khai báo hàm callback addItem trong container App:

Tạo hàm callback trong container App

Các thay đổi chính trong container App:

  • Line 9: Xoá bỏ việc khởi tạo dữ liệu trong biến state.items
  • Line 13: Tạo function callback addItem với input là nội dung task được gửi từ form trong component ToDoList
  • Line 30: Truyền function callback addItem vào component ToDoList thông qua thuộc tính addItem (tên thuộc tính và tên callback function được chọn trùng lặp, cách này không bắt buộc nhưng được khuyến khích cho tính dễ hiểu của source code)

Ngoài các điểm thay đổi kể trên, có thể chú ý kĩ hơn logic trong function addItem:

  • Line 14: Xây dựng một JavaScript object chứa ToDo item. Object có hai thuộc tính, valuekey. Giá trị value tương ứng với nội dung item, giá trị của key được gán ngẫu nhiên theo thời gian hiện tại Date.now()(trả về giá trị tính bằng millisecond tính từ thời điểm 1/1/1970 đến hiện tại). Cách tạo key này hơi ngu trong việc sử dụng bộ nhớ ;)
  • Line 18: Trong trường hợp ToDo item có nội dung (tránh trường hợp form gửi đi một chuỗi kí tự rỗng), xây dựng một danh sách các thuộc tính mới items và gán danh sách này cho biến state.items của container App.

Cách xây dựng danh sách items:

const items = [...this.state.items, newTask]

Cú pháp này sử dụng spread operator. Đây cũng là một đặc điểm mới của ES6 được sử dụng khi xây dựng khi muốn bóc tách các phần tử của một mảng để xây dựng một mảng mới, hoặc cung cấp giá trị tham số khi gọi một function.

Đến đây, có thể bạn sẽ đặt câu hỏi, tại sao không chọn một cách làm đơn giản hơn. Nếu state.items là một mảng, sao không sử dụng build-in function push, ví dụ:

this.state.items.push(newTask

NO, Đây không phải là cách làm với React ! Trong một ứng dụng React, chúng ta không nên thay đổi giá trị của các biến số (immutable data). Lập trình trong React đi theo hướng functional programming style, hạn chế tối thiểu các hoạt động tạo ra side effect (ảnh hưởng ngoại biên) đối với các dữ liệu. Thay vì việc sửa đổi giá trị một biến, hay một mảng (hàm push thay đổi giá trị của mảng state.items), chúng ta xây dựng một mảng mới, dựa trên giá trị cũ cùng các thông tin đầu vào khác.

Ở dòng lệnh tiếp theo, chúng ta cập nhật lại state cho component App:

this.setState({
items,
})

Đây là một cách viết tắt trong quá trình khởi tạo object theo ES6 (Shorthand property trong Object initializer). Cách làm này không có gì đặc biệt ngoại trừ giúp source code khởi tạo object trông đơn giản hơn. Nếu viết đầy đủ, có thể hiểu đoạn code trên tương đương với:

this.setState({
items: items,
})

Wow, quá nhiều thông tin trong một đoạn code ngắn. Hãy kiên nhẫn vì câu chuyện của chúng ta còn chưa dừng lại. Hãy nhớ về slogan của chúng ta khi mới bắt đầu bài viết này.

Sometimes, you just need a break. In a beautiful place. Alone. To figure everything out.

Break and relax

Trong ví dụ trước, chúng ta sử dụng function setState để thiết lập state mới cho container App. Tuy nhiên cách làm này không thực sự chính xác.

Khi cập nhật state cho một component, có hai trường hợp:

  • Giá trị state mới không phụ thuộc vào state cũ
  • Giá trị state mới được xây dựng một phần dựa trên giá trị state cũ

Theo reactjs.org, Function setState là một hàm bất đồng bộ (asynchronous function). Nói cách khác, khi gọi hàm setState, chúng ta không chắc chắn state sẽ được cập nhật ngay tại thời điểm gọi. Lý do của điều này vì React muốn tối ưu hoá việc render trong cấu trúc các component, và hàm setState của các component sẽ thực hiện theo batch style (thực hiện hàng loạt một lúc thay vì thực thi đơn lẻ từng setState).

Có thể tham khảo chi tiết thông tin này từ comment của bạn Dan Abramov trên Github. Đây là một thành viên trong core team của ReactJS, đồng thời là tác giả của Redux, và Create React App toolchain !

Trong ví dụ ToDo, state mới của container App (danh sách các ToDo item) được xây dựng dựa trên state trước đó. Nếu sử dụng setState với giá trị tham số là một JavaScript object (items), trong trường hợp ứng dụng phức tạp với nhiều lời gọi setState tại cùng thời điểm, điều này sẽ làm giá trị bị xung đột và trả về kết quả không chính xác (tham khảo Link)

Thay vì cách làm trước, chúng ta cần đưa vào tham số cho setState là một anonymous function như sau:

this.setState(state =>  {
return {
items: [...state.items, newTask]
}
})

Bằng cách này, khi React gọi hàm setState theo batch style, các hàm anonymous function có thể được lồng với nhau (chain functions) và trả về kết quả chính xác.

Với hàm callback handleInput, component TodoList được update như sau:

Sử dụng hàm callback trong component TodoList

Các thay đổi trên component này khá đơn giản:

  • Line 4: xây dựng function handleInput được gọi khi submit button trong form được click
  • Line 5: function preventDefault loại bỏ hoạt động mặc định khi button submit được click (trong trường hợp này, hoạt động mặc định là submit HTML form)
  • Line 21: sử dụng thuộc tính ref để xây dựng một tham chiếu đến phần tử input với tên taskInput
  • Line 10–11: Dựa vào tham chiếu ref, hàm handleInput thiết lập lại giá trị và focus vào form input, sau khi việc cập nhật danh sách ToDo item hoàn thành

Bài tập

  • Hãy thử xoá bỏ Line 5 khỏi function handleInput và chạy lại chương trình. Tìm hiểu sao trong trường hợp này kết quả cập nhật ToDo list không còn chính xác ?
  • Hãy phát triển Use Case 2, khi người dùng click vào một item trong ToDo list để xoá item đó
  • Thay đổi mô tả trong Use Case 2, khi người dùng click vào một item trong ToDo list, form input sẽ hiển thị nội dung Todo item. Người dùng sửa nội dung item, submit và danh sách ToDo sẽ được cập nhật lại. Cần phát triển cơ chế để phân biệt giữa việc Tạo mới / hoặc Cập nhật ToDo item
  • Thay vì hiển thị theo dạng list (ul/li), hãy sử dụng HTML select để hiển thị các phần tử trong ToDo list.

Bổ sung

Source code cuối cùng của chương trình có thể tìm theo địa chỉ trên Github tại đây: Link

Source code trên Github
  • Các thành phần file trong hướng dẫn có thể thấy trong branch master.
  • Ngoài ra, branch enhancement, bao gồm các cải tiến từ master (naming, refactor, new features).

Kết luận

Đây là một bài hướng dẫn khá dài, tuy nhiên thực sự vẫn chưa thể mô tả tất cả các khía cạnh khác trong việc phát triển một ứng dụng React (ví dụ type checking, lifecycle hook của các React component, các thư viện third-party thông dụng trong React, deployment…). Tuy nhiên, bạn có thể xem đây là các kiến thức cơ bản & đầu tiên, để tìm hiểu thêm về React. Hi vọng các bài viết về sau có thể cung cấp nhiều chi tiết hơn cho các bạn.

Chúc các bạn một ngày mới vui vẻ và hiệu quả. Happy coding. ~ Andy.

--

--