Vuex là gì? Hướng dẫn Vuex cho người mới bắt đầu
Trong các ứng dụng một trang (single-page application), khái niệm trạng thái (state) liên quan đến dữ liệu thay đổi được bất kỳ. Một ví dụ về state có thể là chi tiết của người dùng đã đăng nhập hoặc dữ liệu được nạp từ API.
Xử lý state trong các ứng dụng một trang không dễ dàng. Khi một ứng dụng trở nên lớn hơn và phức tạp hơn, bạn bắt đầu gặp phải tình huống trong đó một phần state nhất định cần được sử dụng trong nhiều thành phần, hay bạn vô tình pass state qua các thành phần không cần đến nó, chỉ để đưa những state đến nơi cần thiết. Điều này còn được gọi là “prop drilling”, và có thể biến code của bạn thành một mớ lùng bùng.
Vuex là gì?
Vuex là giải pháp quản lý state chính thức cho Vue. Nó hoạt động thông qua một kho trung tâm chứa các state chung, và cung cấp các phương thức để cho phép bất kỳ thành phần nào trong ứng dụng của bạn truy cập vào state đó. Về bản chất, Vuex đảm bảo view thật nhất quán với dữ liệu của ứng dụng, bất kể chức năng nào gây ra thay đổi dữ liệu trong ứng dụng.
Trong bài viết này, chúng tôi sẽ cung cấp cho bạn một cái nhìn tổng quan và thật kỹ lưỡng về Vuex và trình bày cách triển khai nó trong một ứng dụng đơn giản.
Ví dụ về giỏ hàng
Hãy cùng xem xét một ví dụ thực tế để minh họa cho vấn đề mà Vuex có thể giải quyết.
Khi bạn vào một trang web mua sắm, bạn sẽ có một danh sách các sản phẩm. Mỗi sản phẩm có nút Thêm vào giỏ hàng và đôi khi là Số hàng còn lại cho biết số lượng tồn kho hiện tại hoặc số lượng mặt hàng tối đa bạn có thể đặt hàng cho sản phẩm được chỉ định. Mỗi khi một sản phẩm được mua, số hàng hiện tại của sản phẩm đó sẽ giảm. Khi điều này xảy ra, Số hàng còn lại sẽ cập nhật với con số chính xác. Khi mức hàng của sản phẩm đạt 0, Số hàng còn lại sẽ đổi thành Hết hàng. Ngoài ra, nút Thêm vào giỏ hàng nên được tắt hoặc ẩn để đảm bảo khách hàng không thể đặt hàng các sản phẩm hiện không có trong kho.
Bây giờ hãy tự hỏi làm thế nào để lập trình tính năng này! Nó có thể phức tạp hơn bạn nghĩ. Gợi ý nhé! Bạn sẽ cần một function khác để cập nhật số hàng khi có hàng mới. Khi sản phẩm đã hết được cập nhật, cả Số hàng còn lại và nút Thêm vào giỏ hàng sẽ được cập nhật ngay lập tức để phản ánh trạng thái mới của số hàng.
Tùy thuộc vào năng lực lập trình của bạn, giải pháp của bạn có thể bắt đầu trông hơi giống spaghetti (rối tung và kém hiệu quả). Bây giờ, hãy tưởng tượng tự nhiên sếp bắt bạn phát triển API cho phép các trang web của bên thứ ba bán sản phẩm trực tiếp từ kho. API cần đảm bảo rằng trang web mua sắm chính phải đồng bộ với số hàng của sản phẩm. Lúc này, đảm bảo bạn sẽ tức điên lên và muốn lật bàn la lên tại sao ông ý không yêu cầu từ ban đầu đi? Bạn cảm thấy như tất cả công sức trước giờ bị đổ xuống sông xuống biển, vì bạn sẽ cần phải viết lại hoàn toàn bộ code của mình để đáp ứng với yêu cầu mới này.
Đây là lúc một thư viện mẫu quản lý state có thể cứu bạn khỏi những cơn đau đầu như vậy. Nó sẽ giúp bạn quả code front-end hiệu quả để đáp ứng bất kỳ yêu cầu mới nào.
Tiền đề
Trước khi ta bắt đầu, tôi sẽ giả định rằng bạn:
- có kiến thức cơ bản về Vue.js
- quen thuộc với các tính năng ngôn ngữ ES6 và ES7
Bạn cũng cần phải có một phiên bản Node.js mới hơn 6.0. Tại thời điểm viết bài, Node.js v10.13.0 (LTS) và npm phiên bản 6.4.1 là phiên bản mới nhất. Nếu bạn chưa cài đặt phiên bản Node phù hợp trên hệ thống của mình, tôi khuyên bạn nên sử dụng version manager .
Cuối cùng, bạn nên cài đặt phiên bản Vue CLI mới nhất:
npm install -g @vue/cli
Xây dựng bộ đếm bằng Local State
Trong phần này, chúng ta sẽ xây dựng một bộ đếm đơn giản để theo dõi state ở cấp độ local. Trước khi bắt đầu, ta hãy tìm hiểu các khái niệm cơ bản của Vuex.
Chuẩn bị
Hãy tạo một dự án mới bằng CLI:
vue create vuex-counter
Sẽ xuất hiện Wizard hướng dẫn bạn cách tạo project. Chọn Manually select features chọn install Vuex.
Tiếp theo, truy cập vào thư mục mới và vào thư mục src/components, đổi tên HelloWorld.vue thành Counter.vue :
cd vuex-counter
mv src/components/HelloWorld.vue src/components/Counter.vue
Cuối cùng hãy mở, src/App.vue và thay thế đoạn code hiện có thành như dưới đây:
<template>
<div id="app">
<h1>Vuex Counter</h1>
<Counter/>
</div>
</template>
<script>
import Counter from './components/Counter.vue'
export default {
name: 'app',
components: {
Counter
}
}</script>
Bạn có thể giữ nguyên styles.Tạo bộ đếm
Đầu tiên, hãy khởi động một lần đếm và đẩy kết quả lên trang. Chúng ta cũng sẽ thông báo cho người dùng biết kết quả là số chẵn hay số lẻ. Mở src/components/Counter.vue và thay thế đoạn code thành dưới đây.
<template>
<div>
<p>Clicked {{ count }} times! Count is {{ parity }}.</p>
</div>
</template>
<script>
export default {
name: 'Counter',
data: function() {
return {
count: 0
};
},
computed: {
parity: function() {
return this.count % 2 === 0 ? 'even' : 'odd';
}
}
}</script>
Như bạn thấy đấy, chúng ta có một state variable là count và một hàm tính toán là parity, với kết quả là một string even hay odd dựa vào số đầu ra của count .
Để xem những gì ta đã có được nãy giờ, hãy khởi động ứng dụng từ thư mục gốc bằng cách chạy npm run serve và điều hướng đến http: // localhost: 8080.
Nếu muốn, bạn có thể thoải mái thay đổi giá trị của bộ đếm để hiển thị kết quả chính xác cho cả counter và parity. Khi đã hài lòng, hãy đảm bảo đặt lại về 0 trước khi ta tiến tới bước tiếp theo.
Incrementing và Decrementing
Ngay sau property
computed
trong phần <script>
của Counter.vue
, thêm đoạn code sau:methods: {
increment: function () {
this.count++;
},
decrement: function () {
this.count--;
},
incrementIfOdd: function () {
if (this.parity === 'odd') {
this.increment();
}
},
incrementAsync: function () {
setTimeout(() => {
this.increment()
}, 1000)
}
}
Việc sử dụng hai hàm
increment
vàdecrement
chắc không cần giải thích thêm nữa. Hàm incrementIfOdd
chỉ thực thi nếu giá trị của count
là một số lẻ, trong khi đó incrementAsync
là một hàm bất đồng bộ thực hiện increment chỉ sau một giây.
Để truy cập những phương thức (method) mới này từ template, chúng ta sẽ cần phải định nghĩa nút bấm. Thêm đoạn code dưới đây vào sau đoạn template code để cho ra kết quả bộ đếm và chẵn lẻ:
<button @click="increment" variant="success">Increment</button>
<button @click="decrement" variant="danger">Decrement</button>
<button @click="incrementIfOdd" variant="info">Increment if Odd</button>
<button @click="incrementAsync" variant="warning">Increment Async</button>
Sau khi lưu, trình duyệt sẽ tự động làm mới. Click vào tất cả các nút để check xem mọi thứ đã làm việc ổn định hay chưa. Nếu không có lỗi xảy ra, bạn sẽ thấy:
Ví dụ về bộ đếm đã xong. Trước khi tiến đến phần viết lại và thực thi bộ đếm, hãy cùng tìm hiểu các nguyên tắc cơ bản của Vuex.
Nguyên lý làm việc của Vuex
Trước khi đi vào thực hành, hãy cùng xem thử code trong Vuex được tổ chức như thế nào nhé. Nếu đã quen thuộc với các framework như Redux, bạn sẽ làm quen rất dễ dàng. Nếu chưa từng làm việc với các framework quản lý nền Flux trước đây, bạn cần để ý kỹ một chút.
Vuex Store
Kho lưu trữ là nơi tập trung tất cả các state dùng chung cho ứng dụng Vue. Trạng thái cơ bản nhất sẽ giống như sau:
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// put variables and collections here
},
mutations: {
// put sychronous functions for changing state e.g. add, edit, delete
},
actions: {
// put asynchronous functions that can call one or more mutation functions
}
})
Sau khi đã xác định store, bạn cần inject store vào ứng dụng Vue.js, như sau:
// src/main.js
import store from './store'
new Vue({
store,
render: h => h(App)
}).$mount('#app')
Như vậy, store đã inject đã available cho mỗi component trong ứng dụng dưới dạng
this.$store
.Làm việc với State
Còn có tên single state tree, đây đơn giản chỉ là một object chứa tấp cả dữ liệu ứng dụng front-end. Vuex, giống như Redux, vận hành chỉ với một store duy nhất. Dữ liệu của ứng dụng được tổ chức theo cấu trúc dạng cây khá đơn giản. Ví dụ như:
state: {
products: [],
count: 5,
loggedInUser: {
name: 'John',
role: 'Admin'
}
}
Tại đây chúng ta có
products
đã được khởi tạo với array (danh sách) rỗng, và count
, có giá trị khởi tạo là 5. Chúng ta cũng có loggedInUser
, một JavaScript Object Literal chứa nhiều trường. Tính chất của State (state property) có thể chứa bất cứ datatype hợp lệ nào từ Booleans, đến arrays, đến các object khác.
Có nhiều cách để hiển thị state trong view. Chúng ta có thể reference store trực tiếp trong template với
$store
:<template>
<p>{{ $store.state.count }}</p>
</template>
Hoặc ta có thể trả kết quả một số store state từ trong một computed property:
<template>
<p>{{ count }}</p>
</template>
<script>
export default {
computed: {
count() {
return this.$store.state.count;
}
}
}
</script>
Vì store trong Vuex luôn reactive, ngay khi giá trị của
$store.state.count
thay đổi, view cũng sẽ thay đổi theo. Tất cả tính toán này sẽ ngầm được thực hiện, giúp code đơn giản và dễ nhìn hơn.
The mapState
Helper
Đến đây, giả dụ bạn có nhiều state khác nhau cần hiển thị trong view. Việc khai báo một loạt computed properties sẽ rất mất thời gian, thật may Vuex đã có sẵn mapStatehelper. Công cụ này có thể được sử dụng để dễ dàng tạo ra nhiều computed properties khác nhau.
<template>
<div>
<p>Welcome, {{ loggedInUser.name }}.</p>
<p>Count is {{ count }}.</p>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: mapState({
count: state => state.count,
loggedInUser: state => state.loggedInUser
})
}
</script>
Dưới đây còn có cách khác đơn giản hơn nữa giúp bạn pass một loạt string đến hàm
mapState
helper:export default {
computed: mapState([
'count', 'loggedInUser'
])
}
Cả hai đoạn code trên đều thực hiện một chức năng. Nên nhớ,
mapState
sẽ trả kết quả là một objet. Nếu muốn dùng nó cho các computed properties khác, bạn có thể dùng thêm spread operator. Cách làm như sau:computed: {
...mapState([
'count', 'loggedInUser'
]),
parity: function() {
return this.count % 2 === 0 ? 'even' : 'odd'
}
}
Getters
Trong Vuex store, getters giống với computed properties trong Vue. Chúng cho phép bạn tạo derived state (trạng thái chuyển hóa), dùng chung được trên các thành phần khác nhau. Như ví dụ dưới đây:
getters: {
depletedProducts: state => {
return state.products.filter(product => product.stock <= 0)
}
}
Kết quả của handler
getter
(khi được truy xuất dưới dạng properties) được cache và có thể được call nhiều lần tùy thích. Những kết quả này cũng sẽ tùy biến theo thay đổi của state. Nói cách khác, Nếu state liên quan bị thay đổi, hàm getter
sẽ tự động được thực thi và cache kết quả mới. Bất kỳ thành phần nào đã từng truy xuất handler getter
sẽ cập nhật ngay lập tức. Bạn có thể truy xuất handler getter
như dưới đâycomputed: {
depletedProducts() {
return this.$store.getters.depletedProducts;
}
}
The mapGetters
Helper
Bạn có thể đơn giản hóa
getters
code bằng helper mapGetters
:import { mapGetters } from 'vuex'
export default {
//..
computed: {
...mapGetters([
'depletedProducts',
'anotherGetter'
])
}
}
Bạn còn có thể pass arguments sang handler
getter
thông qua kết quả hàm, sẽ rất hữu ích nếu bạn muốn thực hiện một query trong getter
:getters: {
getProductById: state => id => {
return state.products.find(product => product.id === id);
}
}
store.getters.getProductById(5)
Nên nhớ mỗi lần handler
getter
được truy xuất thông qua một method. Nó sẽ luôn chạy và kết quả sẽ không được cache.
So sánh:
// property notation, result cached
store.getters.depletedProducts
// method notation, result not cached
store.getters.getProductById(5)
Thay đổi State bằng Mutations
Một điểm quan trọng trong kiến trúc của Vuex là: Component không bao giờ trực tiếp thay đổi state. Việc này sẽ dẫn đến nhiều loại bất thường và gây thiếu đồng bộ trong state của app.
Thay vào đó, thay đổi state trong store Vuex được thực hiện thông qua commit mutation (biến thể), giống với reducer cho những ai đã quen thuộc với Redux.
Dưới đây là một ví dụ về mutation giúp tăng biến
count
được lưu trữ trong state:export default new Vuex.Store({
state:{
count: 1
},
mutations: {
increment(state) {
state.count++
}
}
})
Bạn không thể trực tiếp call handler mutation. Thay vào đó, “commit mutation” như sau:
methods: {
updateCount() {
this.$store.commit('increment');
}
}
Hoặc pass parameters đến mutation:
// store.js
mutations: {
incrementBy(state, n) {
state.count += n;
}
}
// component
updateCount() {
this.$store.commit('incrementBy', 25);
}
Ở ví dụ trên, chúng ta đã pass một số nguyên cần tăng trong bộ đếm cho mutation. Bạn cũng có thể pass một object dưới dạng parameter. Như vậy, bạn có thể dễ dàng gửi đi nhiều field khác nhau mà không gây overload mutation handler:
// store.js
mutations: {
incrementBy(state, payload) {
state.count += payload.amount;
}
}
// component
updateCount() {
this.$store.commit('incrementBy', { amount: 25 });
}
Bạn cũng có thể thực hiện object-style commit như dưới đây:
store.commit({
type: 'incrementBy',
amount: 25
})
Mutation handler sẽ giữ nguyên.
The mapMutations
Helper
Giống với
mapState
và mapGetters
, bạn cũng có thể dùng helper mapMutations
để giảm boilerplate cho mutation handlers:import { mapMutations } from 'vuex'
export default{
methods: {
...mapMutations([
'increment', // maps to this.increment()
'incrementBy' // maps to this.incrementBy(amount)
])
}
}
Bạn cần chú ý, mutation handler phải đồng bộ. Hiển nhiên, bạn có thể thoải mái viết hàm mutation không đồng bộ, nhưng khi ứng dụng scale lên sẽ nảy sinh khá nhiều rắc rối không cần thiết.
Actions
Action là hàm không thay đổi state. Thay vào đó, chúng sẽ commit mutation sau khi thực hiện một số logic (thường là không đồng bộ). Dưới đây là ví dụ đơn giản về một action:
//..
actions: {
increment(context) {
context.commit('increment');
}
}
Action handlers sẽ nhận một object
context
làm argument đầu tiên, từ đó giúp ta truy xuất được store properties và methods. Ví dụ:context.commit
: commit mutationcontext.state
: truy xuất statecontext.getters
: truy xuất getters
Bạn cũng có thể dùng argument destructing để trích xuất các thông số store cần có cho code của mình. Ví dụ:
actions: {
increment({ commit }) {
commit('increment');
}
}
Như đã đề cập bên trên, action có thể không đồng bộ. Xem thử ví dụ sau:
actions: {
incrementAsync: async({ commit }) => {
return await setTimeout(() => { commit('increment') }, 1000);
}
}
Trong ví dụ này, mutation được commit sau 1,000 mili-giây.
Giống mutation, action handles không được call trực tiếp, mà thông qua một method riêng là
dispatch
trên store, như thế này:store.dispatch('incrementAsync')
// dispatch with payload
store.dispatch('incrementBy', { amount: 25})
// dispatch with object
store.dispatch({
type: 'incrementBy',
amount: 25
})
Bạn có thể triển khai một action trong component như sau:
this.$store.dispatch('increment')
The mapActions
Helper
Một các thay thế khác là dùng helper
mapActions
để chỉ định action handler cho local method:import { mapActions } from 'vuex'
export default {
//..
methods: {
...mapActions([
'incrementBy', // maps this.increment(amount) to this.$store.dispatch(increment)
'incrementAsync', // maps this.incrementAsync() to this.$store.dispatch(incrementAsync)
add: 'increment' // maps this.add() to this.$store.dispatch(increment)
])
}
}
Build lại ứng dụng đếm với Vuex
Đến đây, ta đã biết khá nhiều khái niệm trong Vuex, đã đến lúc bắt tay thực hành các kiến thức này để viết lại bộ đếm để tận dụng giải pháp quản lý stage trong Vue.
Khi tạo project với
Vue CLI
, và có chọn tính năng Vuex
. Một số thứ sau xảy ra:Vuex
đã được cài đặt dưới dạng package dependency. Check thửpackage.json
để chắc chắn hơn.- File
store.js
đã được tạo và inject vào ứng dụng Vue.js của bạn thông quamain.js
.
Để convert ứng dụng đếm “local state” thành ứng dụng Vuex, hãy mở
src/store.js
và update code thành:import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
getters: {
parity: state => state.count % 2 === 0 ? 'even' : 'odd'
},
mutations: {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
},
actions: {
increment: ({ commit }) => commit('increment'),
decrement: ({ commit }) => commit('decrement'),
incrementIfOdd: ({ commit, getters }) => getters.parity === 'odd' ? commit('increment') : false,
incrementAsync: ({ commit }) => {
setTimeout(() => { commit('increment') }, 1000);
}
}
});
Ở đây ta có thể thấy cách một store hoành chỉnh trong Vuex được cấu trúc trong thực tế như thế nào. Vui lòng quay lại phần lý thuyết của bài viết này nếu bất cứ điều gì ở đây không rõ ràng với bạn.
Tiếp đến, cập nhật component
src/components/Counter.vue
bằng cách thay thế code đã có trong block <script>
. Ta sẽ tráo các state và function local sang thành phần mới tương ứng trong store Vuex:import { mapState mapGetters, mapActions } from 'vuex'
export default {
name: 'Counter',
computed: {
...mapState([
'count'
]),
...mapGetters([
'parity'
])
},
methods: mapActions([
'increment',
'decrement',
'incrementIfOdd',
'incrementAsync'
])
}
Phần code mẫu cần được giữ nguyên, vì chúng ta sẽ dùng lại tên biến và hàm trước đó. Code bây giờ đã sạch hơn nhiều rồi.
Nếu không muốn dùng state và getter map helper, bạn có thể truy xuất dữ liệu trong store trực tiếp từ template như sau:
<p>
Clicked {{ $store.state.count }} times! Count is {{ $store.getters.parity }}.
</p>
Sau khi đã lưu lại các thay đổi, hãy test thử ứng dụng nhé. Từ góc nhìn của người dùng, ứng dụng đếm cần hoạt động giống y như trước đó. Điểm khác biệt duy nhất là bộ điếm đang hoạt động từ store Vuex.
Lời kết
Trong bài viết này chúng ta đã được tìm hiểu về Vuex là gì, những vấn đề mà Vuex giải quyết, và cách cài đặt, cũng như các khái niệm cốt lõi. Cũng như cách áp dụng những khái niệm này vào ứng dụng đếm đơn giản để nó vận hành với Vuex. Hi vọng bài giới thiệu hôm nay sẽ giúp bạn ứng dun Vuex thật nhanh và nhuần nhuyễn vào project của mình in.
Nhắc nhỏ: Vuex còn có nhiều ứng dụng hay hơn nhiều, và rất hoàn hảo cho các project lớn. Chúng ta sẽ tìm hiểu sâu hơn với bài viết tiếp theo trong tương lai nhé.
Tham khảo: Sitepoint
Nhận xét
Đăng nhận xét