Students Save 30%! Learn & create with unlimited courses & creative assets Students Save 30%! Save Now
Advertisement
  1. Code
  2. Vue
Code

Làm sao để xây dựng ứng dụng quy mô lớn, phức tạp với Vuex

by
Difficulty:AdvancedLength:LongLanguages:

Vietnamese (Tiếng Việt) translation by Thai An (you can also view the original English article)

Học và sử dụng Vue.js rất dễ nên ai cũng có thể xây dựng một ứng dụng đơn giản với framework đó. Ngay cả những người nghiệp dư, với tài liệu hỗ trợ của Vue, cũng có thể làm điều đó. Tuy nhiên mọi thứ trở nên trầm trọng khi sự phức tạp xuất hiện. Thực sư là nhiều component được lồng ghép sâu nhiều cấp với các trạng thái được chia sẻ có thể nhanh chóng khiến ứng dụng của bạn trở thành không thể bảo trì.

Vấn đề lớn nhất trong một ứng dụng phức tạp là làm sao để quản lý trạng thái giữa các component mà không viết code hỗn tạp hoặc gây ra các hiệu ứng phụ. Trong hướng dẫn này, bạn sẽ học cách giải quyết vấn đề đó với Vuex: một thư viện quản lý trạng thái để xây dựng ứng dụng Vue.js phức tạp.

Vuex là gì?

Vuex là một thư viện quản lý trạng thái đặc biệt dùng để xây dựng những ứng dụng Vue.js quy mô lớn và phức tạp. Nó sử dụng store tập trung hoá, và toàn cục cho tất cả component trong một ứng dụng, tận dụng hệ thống các phản ứng cho những cập nhanh tức thời.

Vuex store được xây dựng theo cách để không thể thay đổi trạng thái của nó từ bất kỳ component nào. Bảo đảm rằng trạng thái chỉ có thể biến đổi theo cách có thể dự đoán được. Do đó store của bạn trở thành một nguồn đáng tin: mỗi yếu tố dữ liệu chỉ được lưu một lần và chỉ cho phép đọc để tránh các component của ứng dụng không làm hỏng trạng thái được truy xuất từ các component khác.

Sao bạn cần phải biết Vuex?

Bạn sẽ hỏi: Sao tôi lại cần Vuex ngay từ đầu? Tôi không thể chỉ đưa trạng thái đã được chia sẻ vào một file JavaScript và import nó vào ứng dụng Vue.js của tôi phải không?

Dĩ nhiên bạn có thể nhưng so với một đối tượng toàn cục đơn giản thì Vuex store có những điểm mạnh và ích lợi đáng kể.

  • Vuex store có tính phản ứng. Khi các component thu một trạng thái từ nó, chúng sẽ cập nhật view mỗi khi trạng thái thay đổi.
  • Component không thể trực tiếp biến đổi trạng thái của store. Cách duy nhất để thay đổi trạng thái này thông qua cam kết những thay đổi này một cách rõ ràng. Điều này bảo đảm mỗi thay đổi trạng thái tạo ra một kỷ lục có thể theo dõi làm cho ứng dụng dể sửa lỗi và thử nghiệm hơn.
  • Bạn có thể dễ dàng gỡ lỗi cho ứng dụng qua việc tích hợp Vuex với phần mở rộng DevTools của Vue.
  • Vuex store cho bạn cái nhìn tổng quan cách mọi thứ kết nối và ảnh hướng trong ứng dụng của bạn.
  • Sẽ dễ dàng hơn để bảo trì và đồng bộ trạng thái giữa nhiều component, thậm chí nếu cấu trúc component thay đổi.
  • Vuex giúp các component có thể giao tiếp với nhau trực tiếp.
  • Nếu một component bị xoá bỏ, trạng thái trong Vuex store sẽ được duy trì.

Bắt đầu với Vuex

Trước khi bắt đầu, tôi sẽ làm rõ một số điều.

Trước tiên để theo dõi hướng dẫn này, bạn cần hiểu rõ về Vue.js, và hệ thống component của nó, hoặc có trải nghiệm tối thiểu với framework này.

Đồng thời, đính hướng của hướng dẫn này không phải cho bạn xem cách xây dựng một ứng dụng phức tạp thực sự ra sao; chủ yếu muốn tập trung nhiều hơn vào các khái niệm của Vuex và cách bạn có thể dùng chúng để tạo ra các ứng dụng phức tạp. Vì lý do đó, tôi sẽ sử dụng những ví dụ thuần và đơn giản, không chứa code không cần thiết. Khi bạn hoàn toàn nắm bắt khái niệm của Vuex, bạn sẽ có thể áp dụng chúng cho bất kỳ độ phức tạp nào.

Sau cùng, tôi dùng cú pháp ES2015. Nếu chưa quen thuộc với nó, bạn có thể học nó ở đây.

Và giờ thì bắt đầu thôi!

Thiết lập một dự án Vuex

Bước đầu tiên của bắt đầu với Vuex là cần có Vue.js và Vuex được cài đặt trong máy của bạn. Có vài cách để thực hiện điều này, nhưng chúng ta sẽ chọn cách dễ nhất. Chỉ cần tạo một file HTML và bổ sung những liên kết CDN cần thiết:

Tôi dùng một vài CSS để làm cho các component đẹp mắt hơn, nhưng bạn không cần quá lo về CSS. Nó chỉ giúp tăng gía trị thị giác cho bạn biết điều gì đang diễn ra. Chỉ cần copy và paste vào thẻ <head> bên dưới:

Giờ hãy tạo vài component để làm việc. Trong thẻ <script>, bên phải trên thẻ đóng </body>, đưa code Vue sau đầy vào:, bên phải trên thẻ đóng . đưa code của Vue bên dưới vào:

Ở đây ta có giá trị Vue, một component cha, và hai component con. Mỗi component có một Score: ở đây ta dùng để xuất ra trạng thái của ứng dụng.

Điều sau cùng cần làm là đặt thẻ <div> bao bọc bên ngoài với id="app" ngay sau thẻ mở <body> , và đặt component cha vào bên trong:

Công tác chuẩn bị hoàn tất, giờ chúng ta sẵn sàng tiếp tục.

Khám phá Vuex

Quản lý trạng thái

Trong thực tiễn, chúng ta đương đầu với sự phức tạp bằng cách dùng các chiến lược để tổ chức và cấu trúc nội dung cần sử dụng. Ta tập hợp các thứ liên quan với nhau thành nhiều phần, thể loại khác nhau. Như một thư viện sách, trong đó các cuốn sách được phân loại và xếp vào những phần khác nhau để chúng ta dễ dàng tìm kiếm. Vuex tổ chức dữ liệu và logic có liên quan để định trạng thái trong 4 nhóm: state (trạng thái), getters, mutation (thay đổi), và actions (hành động).

Trạng thái và các thay đổi là cơ sở cho Vuex store bất kỳ.

  • state là một đối tượng lưu giữ trạng thái của dữ liệu.
  • mulatations cũng là một đối tượng chưa những phương thức tác động đến state.

Getter và actions giống như những dự đoán có tính logic của state và mutation:

  • getters có các phương thức được dùng để giả lập việc truy xuất trạng thái, và thực hiện một vài công việc tiền xử lý, nếu cần thiết (tính toán dữ liệu, lọc dữ liệu.v.v).
  • actions là các phương thức để kích hoạt mutations và xử lý code không đồng bộ.

Hãy khám khá sơ đồ bên dưới để hiểu rõ hơn:

Vuex State Management Workflow Diagram

Phía bên trái chúng ta có một ví dụ Vuex store, sẽ được chúng ta tạo ra sau trong hướng dẫn này. Ở bên phải, ta có một sơ đồ quy trình của Vuex, cho thấy cách những phần tử Vuex khác biệt cùng hoạt đông và giao tiếp với nhau.

Để đổi trạng thái, một đối tượng Vue cụ thể phải cam kết các thay đổi (ví dụ this.$store.commit('increment', 3) và sau đó, những thay đổi này làm thay đổi trạng thái (score trở thành 3). Sau đó, getter tự động cập nhật và chúng xuất các thay đổi trong view của component (với this.$store.getters.score).

Mutation không thể xử lý code không đồng bộ, vì việc này sẽ không thể tạo bản ghi và theo dấu các thay đổi trong những công cụ gỡ lỗi như Vue DevTools. Để sử dụng logic không đồng bộ, bạn cần đưa nó vào actions. Trong trường hợp này, một component trước tiên sẽ gửi action (this.$store.dispatch('incrementScore', 3000) ở đây code không đồng bộ được xử lý, và sau đó những action này sẽ cam kết mutation, điều này sẽ thay đổi trạng thái.

Tạo một cấu trúc Vuex Store

Giờ ta đã khám phá cách Vuex hoạt động, hãy tạo một cấu trúc cơ bản của Vuex Store. Đưa code sau đây vào phía trên phần đăng ký component ChildB:

Để cung cấp truy xuất toàn cục đến Vuex store cho mỗi component, ta cần bổ sung thuộc tính store vào đối tượng Vue:

Giờ ta có thể truy xuất vào store từ bất kỳ component nào với biến this.$store.

Tới giờ nếu bạn mở dự án bằng CodePen trong trình duyệt, bạn sẽ thấy kết quả như sau:

App Skeleton

Thuộc tính của state

Đối tượng state chứa tất cả dữ liệu được chia sẻ trong ứng dụng. Dĩ nhiên, nếu cần, mỗi component có thể có trạng thái của riêng nó.

Hình dung xem bạn muốn tạo một ứng dụng game, và bạn cần một biến để lưu điểm trong game. Vậy bạn đưa nó vào đối tượng state:

Giờ bạn có thể truy xuất điểm của state trực tiếp. Hãy quay lại các component và sử dụng dữ liệu từ store. Để có thể sử dụng lại dữ liệu tương tác từ state của store, bạn nên dùng các thuộc tính đã được tính toán. Vậy hãy tạo ra một thuộc tính score() trong component cha:

Trong template của compent cha, đặt {{ score }}:

Giờ làm tương tự cho 2 component con.

Vuex thông minh đến mức sẽ thực hiện tất cả công việc cho chúng ta để cập nhật thuộc tính score bất kể khi nào state thay đổi. Hãy thử thay đổi gía trị điểm và xem kết quả trong 3 component cập nhật ra sao.

Tạo getter

Dĩ nhiên thật tốt khi bạn có thể tái sử dung từ khoá this.$store.state trong những component, như bạn đã thấy bên trên. Nhưng hình dung những kịch bản sau đây:

  1. Trong một ứng dụng lớn, nơi có nhiều component truy xuất trạng thái của store thông qua this.$store.state.score, bạn quyết định thay đổi tên của score. Có nghĩa là bạn phải thay đổi tên của biến trong mỗi component sử dụng nó.
  2. Bạn muốn dùng một giá trị của state đã được tính toán. Ví dụ, hãy nói rằng bạn thưởng 10 điểm cho người chơi khi họ đạt 100 điểm. Vậy, khi điểm số là 100, 10 điểm sẽ được thêm vào. Có nghĩa là mỗi component cần phải chứa một hàm để dùng lại điểm số và tăng điểm đó thêm 10. Bạn sẽ có những code lặp lại trong mỗi component, điều này hoàn toàn không hay.

Thật may khi Vuex đề xuất một giải pháp để xử lý những tình huống này. Tưởng tượng môt getter trung tâm có thể truy xuất state của store và cung cấp một hàm getter cho mỗi thành phần của state. Nếu cần, getter này có thể áp dụng tính toán cho thành phần của state. Và nếu bạn cần thay đổi tên của vài thuộc tính của state, bạn chỉ cần thay đổi nó ở một nơi, trong getter này.

Hãy tạo ra một getter score():

Một Getter nhần state làm đối số đầu tiên, và sau đó dùng nó để truy xuất các thuộc tính của state.

Chú ý: Getter cũng nhận getters khác làm đối số thứ hai. Bạn có thể dùng nó để truy xuất những getter khác trong store.

Trong tất cả component, điều chỉnh thuộc tính đã được tính toán score() để dùng getter score() thay vì trực tiếp dùng score của state.

Giờ nếu bạn quyết định thay đổi score thành result, bạn chỉ cần cập nhật ở một nơi duy nhất: trong getter score(). Hãy thử trong CodePen!

Tạo các mutation

Mutation là cách duy nhất cho phép thay đổi state. Kích phát thay đổi đơn giản nghĩa là cam kết các thay đổi trong các phương thức của component.

Một mutation gần như một hàm xử lý sự kiện được định nghĩa bằng tên. Các hàm xử lý mutation nhận một state làm đối số đầu tiên. Bạn cũng có thể truyền một đối số bổ sung, đây gọi là payload của sự thay đổi.

Hãy tạo ra một thay đổi increment():

Các thay đổi không thể được gọi trực tiếp. Để thực hiện thay đổi, bạn nên gọi phương thức commit() với tên gọi của thay đổi tương ứng và những đối số bổ sung khả dĩ. Đó có lẽ chỉ cần một, như step trong trường hợp của chúng ta, hoặc có thể nhiều đối số thuộc về một đối tượng.

Hãy sử dụng mutation increment() trong hai component con bằng một phương thức được gọi là changeScore():

Chúng ta đang thực hiện một mutation thay vì thay đổi trực tiếp this.$score.state.score, bởi vì chúng ta muốn tường minh theo dấu thay đổi được mutation thực hiện. Với cách này, chúng ta khiến logic ứng dụng rõ ràng hơn, có thể theo dõi và hiểu lý do. Ngoài ra, điều này giúp triển khai các công cụ nhưng Vue DevTools hoặc Vuetron, chúng có thể ghi lại tất cả thay đổi, tạo bản snapshot cho state, và thực hiện gỡ lỗi time-travel.

Giờ hãy sử dụng phương thước changeScore(). Trong mỗi template của hai component con, hãy tạo một button và một event listener khi click vào nó.

Khi bạn click vào button, state sẽ tăng thêm 3, và thay đổi này sẽ được phản ánh trong tất cả component. Giờ chúng ta đã đạt thành giao tiếp trực tiếp giữa các component, điều này không xảy ra với Vue.js tích hợp sẵn theo cơ chế "props down, events up". Hãy xem ví dụ trên CodePen.

Tạo các action

Action chỉ là một hàm để thực hiện một mutation. Nó sẽ gián tiếp thay đổi state, điều này cho phép xử lý của các hoạt động bất đồng bộ.

Cùng tạo một action incrementScore() nào:

Action nhận context làm đối số đầu tiên. Đối số này có tất cả phương thức và thuộc tính từ store. Thông thường, chúng ta chỉ trích xuất những phần chúng ta cần bằng phương pháp ES2015 argument destructing. Phương thức commit là điều ta sẽ thường xuyên cần. Action cũng có một đối số payload thứ hai, giống như các mutation.

Trong component ChildB, thay đổi phương thức changeScore():

Để gọi một action, ta dùng phương thức dispatch() với tên gọi của action tương ứng và những đối số bổ sung, giống như của mutations.

Giờ button Change Score từ component ChildA sẽ gia tăng điểm số thêm 3. Button y hệt từ component ChildB sẽ làm việc tương tự, nhưng chậm hơn 3 giây. Trong trường hợp đầu, ta đang xử lý code đồng bộ và dùng một mutation, nhưng trường hợp thứ hai ta xử xý code bất đồng bộ, và chúng ta cần dùng một action thay vào đó. Xem tất cả cùng hoạt động thế nào trong ví dụ CodePen của chúng tôi.

Helper cho Vuex Mapping

Vuex đề xuất một số helper để sắp xếp phù hợp quá trình tạo state, getter, mutation và action. Thay vì tự viết những hàm này, chúng ta có thể nhờ Vuex làm việc này. Hãy xem cách nó hoạt động.

Thay vì viết thuộc tính score() như vầy:

Ta chỉ cần dùng hàm hỗ trợ mapState() như thế này:

Và thuộc tính score() được tạo ra tự động cho chúng ta.

Tương tự cho getter, mutation và action.

Để tạo getter score(), chúng ta dùng hàm hỗ trợ mapGetters():

Để tạo các phương thức changeScore(), ta dùng hàm hỗ trợ mapMutations() như sau:

Khi dùng cho mutation và action với đối số payload, ta phải truyền đối số trong template để định nghĩa phần xử lý sự kiện:

Nếu tao muốn changeScore() dùng một action thay vì mutation, ta dùng mapActions() như sau:

Lần nữa ta phải định nghĩa độ trì hoãn trong phần xử lý sự kiện.

Chú ý: Tất các hàm hỗ trợ mapping trả về một đối tượng. Vì thế nếu chúng ta muốn dùng chúng kết hợp với những thuộc tính được tính toán cục bộ hoặc với các phương thức, thì chúng ta cần hợp nhất chúng vào một đối tượng. Thật may chúng ta có thể làm điều đó với toán tử (...) mà không cần bất kỳ tiện ích nào.

Trong CodePen của chúng ta, bạn có thể thấy một ví dụ về cách tất cả hàm hỗ trợ mapping được dùng thế nào trong thực hành.

Làm cho store có tính mô-đun hơn

Dường như vấn đề phức tạp liên tục cản trở chúng ta. Chúng tôi đã giải quyết nó trước khi tạo ra Vuex store, ở đây chúng tôi làm cho việc quản lý state và giao tiếp giữa các component trở nên dễ dàng. Trong store đó, chúng ta có tất cả mọi thứ ở cùng một nơi, dễ vận dùng và dễ hiểu.

Tuy nhiên khi ứng dụng của ta phát triển, file store dễ quản lý này trở nên càng lúc càng lớn, và kết quả là khó bảo trì hơn. Thêm lần nữa, ta cần những chiến lược và kỹ thuật để cải tiến cấu trúc ứng dụng bằng việc trả nó về hình thái dễ dàng bảo trì. Trong phần này, chúng ta sẽ khám phá vài kỹ thuật giúp chúng ta làm việc này.

Sử dụng các mô-đun Vuex

Vuex cho phép chúng ta chia nhỏ những đối tượng store thành các mô-đun riêng biệt. Mỗi mô-đun có thể có state, mutation, action, getter và những mô-đun cấp dưới cúa riêng nó. Sau khi ta tạo ra những mô-đun cần thiết, chúng ta đăng ký chúng trong store.

Hãy xem nó hoạt động thế nào:

Trong ví dụ trên, chúng ta tạo ra 2 mô-đun, một cho mỗi component con. Mô-đun chỉ là những object thuần, ta đăng ký chúng như scoreBoardresultBoard trong đối tượng modules bên trong store. Code cho childA giống với code trong store trong ví dụ trước đó. Trong code của childB, chúng ta bổ sung vài thay đổi trong giá trị và tên gọi.

Hãy điều chỉnh component ChildB để phản ánh các thay đổi trong mô-đun resultBoard.

Trong component ChildA, điều duy nhất ta cần thay đổi là phương thức changeScore():

Như bạn cũng thấy, chia nhỏ store thành các mô-đun khiến nó dễ bảo trì và gọn nhẹ hơn, trong khi vẫn giữ chức năng tuyệt vời của nó. Hãy xem qua CodePen mới cập nhật để thấy kết quả.

Namespaced Modules

Nếu bạn muốn hoặc cần sử dụng một hoặc cùng tên cho một thuộc tính hoặc phương thức trong các mô đun của bạn, sau đó bạn nên cân nhắc namespace chúng. Nếu không bạn nên quan sát những hiệu ứng phụ kỳ lạ, như việc xử lý những action có cùng tên, hoặc lấy sai giá trị của state.

Để namespace một mô đun Vuex, chỉ cần bạn xét thuộc tính namespaced thành true.

Trong ví dụ trên, chúng ta đã tạo thuộc tính và phương thức có trùng tên với 2 mô đun. Và giờ chúng ta có thể dùng thuộc tính và phương thức với tiếp đầu ngữ là tên của mô đun. Ví dụ nếu ta muốn dùng getter score() từ mô đun resultBoard, chúng ta gõ vào như vầy: resultBoard/score. Nếu ta muốn getter score() từ mô đun scoreBoard, thì chúng ta gõ như sau: scoreBoard/score.

Giờ hãy thay đổi component để phản ánh thay đổi chúng ta đã tạo ra.

Như bạn thấy trong ví dụ CodePen, chúng ta có thể dùng phương thức hoặc thuộc tính ta muốn và nhận kết quả ta mong đợi.

Phân chia Vuex Store thành những file riêng biệt

Trong phần trước, chúng ta đã cải tiến cấu trúc ứng dụng đến vài mức độ bằng cách phân tách store thành những mô đun. Chúng ta đã làm store rõ ràng hơn và có tổ chức hơn, nhưng tất cả code cho store và mô đun của nó vẫn là một file lớn.

Vậy bước đi hợp lý kế tiếp là chia Vuex store thành những file tách biệt. Ý tưởng là cần có một file riêng biệt cho chính store và một file khách cho các đối tượng của nó, bao gồm cả mô đun của nó. Có nghĩa là phân tách các file cho state, getter, mutation, action và cho mỗi mô đun (store.js, state.js, getters.js, v.v) Bạn có thể xem ví dụ cấu trúc này khi kết thúc phần kế tiếp.

Sử dụng các component của Vue trong file riêng lẻ

Chúng ta vừa mô đun hoá Vue store như ta muốn. Tiếp theo là áp dụng cùng chiến lược này cho các component của Vue.js. Chúng ta có thể đưa mỗi component vào từng file với tên mở rộng là .vue. Để hiểu cách này hoạt động ra sao, bạn có thể xem ở Vue Single File Components documentation page.

Vậy trong trường hợp của ta, chúng ta sẽ có 3 file: Parent.vue, ChildA.vueChildB.vue.

Cuối cùng, nếu ta kết hợp 3 kỹ thuật này lại, chúng ta sẽ có kết quả là cấu trúc tương tư dưới đây:

Trong repo Github của hướng dẫn, bạn có thể xem dự án hoàn tất với cấu trúc như trên.

Tóm tắt

Hãy tóm tắt một số điểm chính bạn cần nhớ về Vuex:

Vuex là thu viện quản lý state giúp ta tạo ra những ứng dụng lớn và phức tạp. Nó dùng một store trung tâm hoá và toàn cục cho tất cả component trong một ứng dụng. Để giả lập state, ta dùng getter. Getter giống các thuộc tính đã tính toán và là giải pháp lý tưởng khi chúng ta cần lọc hoặc tính toán vài điều trong runtime.

Vuex store có tính phản ứng, và component không thể trực tiếp biến đổi state của store. Cách duy nhất để biến đổi state là thực hiện mutation, nó là những giao dịch đồng bộ. Mỗi mutation chỉ nên thực hiện một action, phải đơn giản nhất có thể, và chỉ đảm trách cho việc cập nhật một phần của state.

Logic bất đồng bộ nên được áp dụng trong action. Mỗi action có thể thực hiện một hoặc nhiều mutation và một mutation có thể được thực hiện bởi nhiều action. Action có thể phức tạp nhưng chúng không bao giờ thay đổi state một cách trực tiếp.

Sau cùng, tính mô-đun là mấu chốt của việc bảo trì. Để đương đầu với sự phức tạp và làm code có tính mô-đun, ta cần dùng quy tắc "divide and conquer" (chia để trị) và kỹ thuật chia nhỏ code.

Tổng kết

Thế đấy! Bạn đã biết các khái niệm chính đằng sau Vuex, và bạn sẵn sàng bắt đầu áp dụng vào thực hành.

Nhàm mục đich ngắn gọn và đơn giản, tôi cố ý lược bỏ một số chi tiết và tính năng của Vuex, nên bạn cần đọc tài liệu Vuex đầy đủ để tìm hiểu mọi thứ về Vuex và bộ tính năng của nó.

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.