Chương 3-Bài 2. Vuex là gì? Cài đặt và làm quen với Vuex
Tác giả: Dương Nguyễn Phú Cường
Ngày đăng: Hồi xưa đó
Lượt xem: 170
Vuex là gì?
Theo như định nghĩa của trang chủ thì nguyên văn nó như thế này :
Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion. It also integrates with Vue’s official devtools extension to provide advanced features such as zero-config time-travel debugging and state snapshot export / import
Định nghĩ thì có vẻ khó hiểu như vậy nhưng ta có thể hiểu một cách nôm na Vuex là một pattern + library của Vuejs, nó có chức năng như một cái kho chứa tập trung các state của các component trong ứng dụng. Khi chúng ta cần thay đổi gì chỉ cần tương tác trực tiếp với thằng state trên store của Vuex, mà không cần phải thông qua quan hệ giữa các component.
Để hiểu hơn là tại sao phải cần đến Vuex ta sẽ qua một ví dụ sau:
Chúng ta có 2 component là Counter chứa 2 chức năng là increment – decrement và component Result có chức năng in ra kết quả
Với trường hợp không sử dụng Vuex thì chúng ta sẽ cần truyền sự kiện increment hoặc decrement từ Counter lên cho App và sau đó App sẽ cập nhật và truyền kết quả xuống cho thằng Result
Đây là trường hợp mới chỉ có một cấp. Vậy nếu cây phân cấp components của ứng dụng là rất nhiều thì điều gì sẽ xảy ra. Trông nó sẽ như thế này :
Sẽ rất là rối và khó quản lý thì ý tưởng của thằng này cũng tương tự như Redux nếu anh em nào đã từng học qua Redux. Thì Vuex cũng vậy, nó sẽ tạo ra một strore chung cho các state để dễ dàng quản lý và thao tác khi có thay đổi:
Cài Đặt
CDN
Nếu bạn đang dùng Vuejs dạng CDN như Jquery :
https://unpkg.com/vuex
nhớ tải cả Vuejs nhá Vuejs
1
2
3
4
<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>
NPM
1
2
3
npm install vuex--save
Yarn
1
2
3
yarn add vuex
Sau khi kiểm tra trong package.json đã cài đặt thành công Vuex ta tạo 1 folder store và tạo file store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from'vue';
import Vuex from'vuex';
Vue.use(Vuex);
export conststore=newVuex.Store({
state:{
result:0
},
mutations:{
},
getters:{
},
actions:{
},
modules:{
}
});
Vậy là đã có 1 cái store tập trung của Vuex rồi.
Các thành phần và cách sử dụng của chúng
1. State
Giống như ở mỗi component chúng ta thường có 1 đối tượng data chứa các biến của componet thì state ở đây cũng có thể hiểu chính là data của cả ứng dụng. Sử dụng một state duy nhất như thế này sẽ giúp ta đồng bộ được dữ liệu giữa các componet một cách nhanh chóng và chính xác.
1
2
3
4
5
state:{
result:0
}
Lấy ra giá trị của một biến trong state, thì cũng giống như cách lấy ta giá trị của một attribute trong đối tượng vậy.
1
2
3
4
5
6
7
8
9
exportdefault{
computed:{
result(){
returnthis.$store.state.result;
}
}
};
Nếu trong state của chúng ta có nhiều biến và ta chỉ muốn lấy ra một số các biến nhưng lại không muốn gọi từng thứ một như thế kia, thì đừng lo đã có cách đó là sử dụng một helper tên là mapState. Nó sẽ sử dụng toán thử Spread (...Array) cú pháp này chỉ áp dụng được trong các phiên bản javascript ES6 trở lên thôi nhe.
state
1
2
3
4
5
6
state:{
result:0,
value:'aaa'
}
1
2
3
4
5
6
7
8
9
10
11
import{mapState}from"vuex";
exportdefault{
computed:{
localComputed(){/* ... */},
// mix this into the outer object with the object spread operator
...mapState(["result","value"]),
c
}
};
1
2
3
4
5
6
7
8
<template>
<div>
<p>thisisResult:{{result}}</p>
<p>value:{{value}}</p>
</div>
</template>
Vậy là giờ ta có các giá trị result và value đã có thể lấy ra sử dụng mà không cần phải lấy từng giá trị một nữa. Đừng quên import mapState không lại bảo sao không chạy.
Sử dụng mapState thì có thể lấy ra giá trị nhưng không thể update được đâu, Docs thì không thấy nói update bằng cách này, nhưng mình thấy từ map mình cứ nghĩ là nó binding 2 chiều nên mình đã thử update state bằng cách này và không thấy được nên chắc nó chỉ để get state thôi.
2. Getters
Đôi khi chúng ta có một hàm cần tính toán dựa trên biến trong state mà cái hàm này lại xuất hiện ở nhiều component. Bây giờ chả nhẽ ở mỗi component ta lại lôi cái biến đó ra và tạo hàm tính toán lại ví dụ hàm lọc các công việc phải làm và đếm chúng:
Ta có thể sử dụng các hàm trong cùnggetters với nhau:
1
2
3
4
5
6
7
8
9
10
11
getters:{
doneTodos:state=>{
returnstate.todos.filter(todo=>todo.done)
},
doneTodosCount:(state,getters)=>{
returngetters.doneTodos.length
}
}
1
2
3
store.getters.doneTodosCount// -> 1
Còn ở trong các component khác thì cũng đơn giản không kém
1
2
3
4
5
6
7
computed:{
doneTodosCount(){
returnthis.$store.getters.doneTodosCount
}
}
Đã có mapSate thì cũng có mapGetters và cách dùng cũng tương tự:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import{mapGetters}from'vuex'
exportdefault{
// ...
computed:{
// mix the getters into computed with object spread operator
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
Hoặc là lúc định nghĩ thì một tên nhưng lúc sử dụng ta muốn dùng tên khác cũng chẳng sao đổi được ý mà:
1
2
3
4
5
6
...mapGetters({
// map `this.doneCount` to `this.$store.getters.doneTodosCount`
doneCount:'doneTodosCount'
})
3. Mutations
Theo như Docs thì mutations là cách duy nhất mà ta có thể thay đổi thực sự state trong store. Và cách để kích hoạt một mutations đó là ta sẽ commit một chuỗi String chính là tên của hàm mà ta muốn gọi trong mutations, nó sẽ nhận state của store làm tham số đầu tiên:
1
2
3
4
5
6
7
8
9
10
11
12
13
conststore=newVuex.Store({
state:{
count:1
},
mutations:{
increment(state){
// mutate state
state.count++
}
}
})
1
2
3
store.commit('increment')
Thấy nhà phát triển nói là cách duy nhất để thay đổi thực sự nhưng mình đã thử một cách và vẫn thấy nó thay đổi được đó là:
1
2
3
4
5
6
7
8
9
10
exportdefault{
name:"counter",
methods:{
increment(){
this.$store.state.count++;
}
}
}
Mình vẫn thấy nó hoạt động nhưng không thấy Docs nói đến kiểu này hay là nó không chuẩn chỉ và có thể gây lỗi hay như thế nào. Nhưng hoi Docs đã viết cách này dùng để thay đổi, nên mình sẽ dùng cách này cho chắc không lại đến lúc có lỗi thì vỡ mồm.
Ngoài commit mỗi tên của hàm thì bạn cũng có thể truyền thêm một tham số bổ sung, nếu như hàm của bạn có định nghĩa nhiều hơn 1 tham số đầu vào là state
1
2
3
4
5
6
7
8
// ...
mutations:{
increment(state,n){
state.count+=n
}
}
1
2
3
store.commit('increment',10)
thường thì người ta sẽ gom các đối số thành một Object để có thể chứa được nhiều biến cần truyền vào hơn
1
2
3
4
5
6
7
8
// ...
mutations:{
increment(state,payload){
state.count+=payload.amount
}
}
1
2
3
store.commit('increment',{amount:10,total:50})
Còn một cách nữa đó là gom cả tên hàm cần gọi và biến cần truyền vào 1 Object với tên hàm để là type vậy là mutations sẽ hiểu và thực hiện mà không cần thay đổi số tham số của hàm
1
2
3
4
5
6
store.commit({
type:'increment',
amount:10
})
1
2
3
4
5
6
7
mutations:{
increment(state,payload){
state.count+=payload.amount
}
}
Muntations thì cũng tuân theo Reactivity Rules của Vue. Nên nếu sau khi state đã được khởi tạo ta muốn thêm một biến mới vào trong state, thì ta cần khai báo cho Vue biết rằng ta có một mới muốn thêm vào hoặc là thay thế toàn bộ
1
2
3
4
5
6
7
Vue.set(obj,'newProp',123)
OR
state.obj={...state.obj,newProp:123}
Ta có thể dùng các mutations với dạng các hằng số. Điều này sẽ rất giúp ích cho việc đồng bộ tên hàm cũng như phù hợp cho các dự án lớn với nhiều bên tham gia
1
2
3
4
// mutation-types.js
export constSOME_MUTATION='SOME_MUTATION'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// store.js
import Vuex from'vuex'
import{SOME_MUTATION }from'./mutation-types'
conststore=newVuex.Store({
state:{...},
mutations:{
// we can use the ES2015 computed property name feature
// to use a constant as the function name
[SOME_MUTATION](state){
// mutate state
}
}
})
nhưng nó chỉ là một tùy chọn thôi nha bạn không nhất thiết cứ phải sử dụng nó đâu
=> Một điều cần nhớ đó là thằng mutations này sẽ chạy đồng bộ nên bạn cần cẩn thận khi sử dụng nó, không lại dối tung lên khi kết hợp nó với các hàm bất đồng bộ và không hiểu sao nó lại không chạy.
Giống như 2 thằng trên thì mutations cũng có helper đó là mapMutations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import{mapMutations}from'vuex'
exportdefault{
// ...
methods:{
...mapMutations([
'increment',// map `this.increment()` to `this.$store.commit('increment')`
// `mapMutations` also supports payloads:
'incrementBy'// map `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add:'increment'// map `this.add()` to `this.$store.commit('increment')`
})
}
}
4. Actions
Actions cũng giống như mutations nhưng nó khác ở hai điểm:
Actions không trực tiếp thay đổi state trong store mà nó sẽ thông qua mutations để thay đổi
Nó có thể chứa các hàm bất đồng bộ
Ví dụ đơn giản
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
conststore=newVuex.Store({
state:{
count:0
},
mutations:{
increment(state){
state.count++
}
},
actions:{
increment(context){
context.commit('increment')
}
}
})
Có thể sử dụng hàm argument destructuring để tạo
1
2
3
4
5
6
7
actions:{
increment({commit}){
commit('increment')
}
}
Cách kích hoạt 1 actions khi ở component khác
1
2
3
store.dispatch('increment')
chúng ta cũng có thể truyền thêm một tham số
1
2
3
4
5
6
7
8
9
10
11
12
// dispatch with a payload
store.dispatch('incrementAsync',{
amount:10
})
// dispatch with an object
store.dispatch({
type:'incrementAsync',
amount:10
})
Và lại thêm một cái helper nữa đó là mapActions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import{mapActions}from'vuex'
exportdefault{
// ...
methods:{
...mapActions([
'increment',// map `this.increment()` to `this.$store.dispatch('increment')`
// mapActions` also supports payloads:
'incrementBy'// map `this.incrementBy(amount)` to `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add:'increment'// map `this.add()` to `this.$store.dispatch('increment')`
})
}
}
5. Modules
Bây giờ mới có vài hàm thì nhét hết vào file store.js được chứ về sau mỗi biến trong state lại có hàng tá hàm thì rất rối. Thì Vuex đã hỗ trợ một tùy chỉnh đó là modules, ta có thể tách các hàm có chung mục đích ra một file như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
constmoduleA={
state:{...},
mutations:{...},
actions:{...},
getters:{...}
}
constmoduleB={
state:{...},
mutations:{...},
actions:{...}
}
conststore=newVuex.Store({
modules:{
a:moduleA,
b:moduleB
}
})
store.state.a// -> `moduleA`'s state
store.state.b// -> `moduleB`'s state
Nếu bạn muốn nhóm các kiểu mutation/action có chung mục đích sử dụng lại với nhau ta có thể sử dụng đinh nghĩa Namespacing với thuộc tính namespaced: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
conststore=newVuex.Store({
modules:{
account:{
namespaced:true,
// module assets
state:{...},// module state is already nested and not affected by namespace option
Hoặc là đưa phần string namespace vào làm đối số đầu tiên
1
2
3
4
5
6
7
8
9
10
11
12
13
14
computed:{
...mapState('some/nested/module',{
a:state=>state.a,
b:state=>state.b
})
},
methods:{
...mapActions('some/nested/module',[
'foo',// -> this.foo()
'bar'// -> this.bar()
])
}
Còn nếu không muốn dùng lại những string namespace đó nhiều lần bạn có thể sử dụng createNamespacedHelpers. Nó sẽ trả về một đối tượng liên kết với các helper mà bạn muốn.
Ngoài ra bạn có thể đăng ký các module sau khi store đã được tạo với method store.registerModule
1
2
3
4
5
6
7
8
9
10
11
// register a module `myModule`
store.registerModule('myModule',{
// ...
})
// register a nested module `nested/myModule`
store.registerModule(['nested','myModule'],{
// ...
})
Cấu trúc ứng dụng
Theo hướng dẫn thì cấu trúc của ứng dụng Vuex nên như này
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── index.html
├── main.js
├── api
│└── ...# abstractions for making API requests
├── components
│├── App.vue
│└── ...
└── store
├── index.js# where we assemble modules and export the store
├── actions.js# root actions
├── mutations.js# root mutations
└── modules
├── cart.js# cart module
└── products.js# products module
Để quản lý chặt chẽ hơn thì chúng ta nên kết hợp vừa tách thành các modules cho những đối tượng chứa nhiều hàm trong mutations, actions, getters và vừa tách ra các file actions, mutations dùng cho các đối tượng ít hàm hơn.
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.
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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<style>
#app {
background-color: yellow;
padding: 10px;
}
#parent {
background-color: green;
width: 400px;
height: 300px;
position: relative;
padding-left: 5px;
}
h1 {
margin-top: 0;
}
.child {
width: 150px;
height: 150px;
position:absolute;
top: 60px;
padding: 0 5px 5px;
}
.childA {
background-color: red;
left: 20px;
}
.childB {
background-color: blue;
left: 190px;
}
</style>
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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Vue.component('ChildB',{
template:`
<div class="child childB">
<h1> Score: </h1>
</div>`
})
Vue.component('ChildA',{
template:`
<div class="child childA">
<h1> Score: </h1>
</div>`
})
Vue.component('Parent',{
template:`
<div id="parent">
<childA/>
<childB/>
<h1> Score: </h1>
</div>`
})
newVue ({
el: '#app'
})
Ở đâ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:
1
2
3
<divid="app">
<parent/>
</div>
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:
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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
const store = newVuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {
}
})
Để 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:
Đố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:
1
2
3
state: {
score: 0
}
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:
1
2
3
4
5
computed: {
score () {
returnthis.$store.state.score
}
}
Trong template của compent cha, đặt {{ score }}:
1
<h1> Score: {{ score }} </h1>
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:
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ó.
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():
1
2
3
4
5
getters: {
score (state){
returnstate.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.
1
2
3
4
5
computed: {
score () {
returnthis.$store.getters.score
}
}
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():
1
2
3
4
5
mutations: {
increment (state, step) {
state.score += step
}
}
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():
1
2
3
4
5
methods: {
changeScore (){
this.$store.commit('increment', 3);
}
}
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ó.
1
<button@click="changeScore">Change Score</button>
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:
1
2
3
4
5
6
7
actions: {
incrementScore: ({ commit }, delay) => {
setTimeout(() => {
commit('increment', 3)
}, delay)
}
}
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():
1
2
3
4
5
methods: {
changeScore (){
this.$store.dispatch('incrementScore', 3000);
}
}
Để 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:
1
2
3
4
5
computed: {
score () {
returnthis.$store.state.score
}
}
Ta chỉ cần dùng hàm hỗ trợ mapState() như thế này:
1
2
3
computed: {
...Vuex.mapState(['score'])
}
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():
1
2
3
computed: {
...Vuex.mapGetters(['score'])
}
Để tạo các phương thức changeScore(), ta dùng hàm hỗ trợ mapMutations() như sau:
1
2
3
methods: {
...Vuex.mapMutations({changeScore: 'increment'})
}
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:
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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const childB = {
state: {
result: 3
},
getters: {
result (state) {
returnstate.result
}
},
mutations: {
increase (state, step) {
state.result += step
}
},
actions: {
increaseResult: ({ commit }, delay) => {
setTimeout(() => {
commit('increase', 6)
}, delay)
}
}
}
const childA = {
state: {
score: 0
},
getters: {
score (state) {
returnstate.score
}
},
mutations: {
increment (state, step) {
state.score += step
}
},
actions: {
incrementScore: ({ commit }, delay) => {
setTimeout(() => {
commit('increment', 3)
}, delay)
}
}
}
const store = newVuex.Store({
modules: {
scoreBoard: childA,
resultBoard: childB
}
})
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ư scoreBoard và resultBoard 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.
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.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const childB = {
namespaced: true,
state: {
score: 3
},
getters: {
score (state) {
returnstate.score
}
},
mutations: {
increment (state, step) {
state.score += step
}
},
actions: {
incrementScore: ({ commit }, delay) => {
setTimeout(() => {
commit('increment', 6)
}, delay)
}
}
}
const childA = {
namespaced: true,
state: {
score: 0
},
getters: {
score (state) {
returnstate.score
}
},
mutations: {
increment (state, step) {
state.score += step
}
},
actions: {
incrementScore: ({ commit }, delay) => {
setTimeout(() => {
commit('increment', 3)
}, delay)
}
}
}
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.vue và ChildB.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:
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ó.