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ặcdecrement
từCounter
lên choApp
và sau đóApp
sẽ cập nhật và truyền kết quả xuống cho thằngResult
Cài Đặt
CDN
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
|
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 const store = new Vuex.Store({
state: {
result: 0
},
mutations: {
},
getters: {
},
actions: {
},
modules: {
}
});
|
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
|
export default {
computed: {
result() {
return this.$store.state.result;
}
}
};
|
...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";
export default {
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>this is Result: {{result}}</p>
<p>value: {{value}}</p>
</div>
</template>
|
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:
1
2
3
4
5
6
7
|
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
|
getters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
|
1
2
3
|
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
|
getters
với nhau:
1
2
3
4
5
6
7
8
9
10
11
|
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
},
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
|
1
2
3
|
store.getters.doneTodosCount // -> 1
|
1
2
3
4
5
6
7
|
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// mix the getters into computed with object spread operator
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
|
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
|
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// mutate state
state.count++
}
}
})
|
1
2
3
|
store.commit('increment')
|
1
2
3
4
5
6
7
8
9
10
|
export default {
name: "counter",
methods: {
increment() {
this.$store.state.count++;
}
}
}
|
- 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)
|
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 })
|
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
}
}
|
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 const SOME_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'
const store = new Vuex.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
}
}
})
|
- 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'
export default {
// ...
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ộ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
|
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')
|
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
})
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import { mapActions } from 'vuex'
export default {
// ...
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
|
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.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
|
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// module assets
state: { ... }, // module state is already nested and not affected by namespace option
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// nested modules
modules: {
// inherits the namespace from parent module
myPage: {
state: { ... },
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// further nest the namespace
posts: {
namespaced: true,
state: { ... },
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
|
- Và khi binding với các helpers mà có sử dụng namespace trông nó sẽ như thế này:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}
|
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()
])
}
|
createNamespacedHelpers
. Nó sẽ trả về một đối tượng liên kết với các helper mà bạn muốn.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// look up in `some/nested/module`
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// look up in `some/nested/module`
...mapActions([
'foo',
'bar'
])
}
}
|
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
|
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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18 |
<! DOCTYPE html> < html > < head > < meta charset = "UTF-8" /> <!-- Put the CSS code here --> </ head > < body > <!-- Put the HTML template code here --> < script > // Put the Vue code here </ script > </ body > </ html > |
<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 > |
<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>` }) new Vue ({ el: '#app' }) |
<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 |
< div id = "app" > < parent /> </ div > |
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.
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ộ.
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ý componentChildB
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14 |
const store = new Vuex.Store({ state: { }, getters: { }, mutations: { }, actions: { } }) |
store
vào đối tượng Vue:
1
2
3
4 |
new Vue ({ el: '#app' , store // register the Vuex store globally }) |
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:
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:
1
2
3 |
state: { score: 0 } |
score()
trong component cha:
1
2
3
4
5 |
computed: { score () { return this .$store.state.score } } |
{{ score }}
:
1 |
< h1 > Score: {{ score }} </ h1 > |
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ủascore
. 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.
score()
:
1
2
3
4
5 |
getters: { score (state){ return state.score } } |
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 () { return this .$store.getters.score } } |
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ộtstate
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 } } |
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); } } |
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 > |
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 actionincrementScore()
nào:
1
2
3
4
5
6
7 |
actions: { incrementScore: ({ commit }, delay) => { setTimeout(() => { commit( 'increment' , 3) }, delay) } } |
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); } } |
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ínhscore()
như vầy:
1
2
3
4
5 |
computed: { score () { return this .$store.state.score } } |
mapState()
như thế này:
1
2
3 |
computed: { ...Vuex.mapState([ 'score' ]) } |
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' ]) } |
changeScore()
, ta dùng hàm hỗ trợ mapMutations()
như sau:
1
2
3 |
methods: { ...Vuex.mapMutations({changeScore: 'increment' }) } |
1 |
< button @ click = "changeScore(3)" >Change Score</ button > |
changeScore()
dùng một action thay vì mutation, ta dùng mapActions()
như sau:
1
2
3 |
methods: { ...Vuex.mapActions({changeScore: 'incrementScore' }) } |
1 |
< button @ click = "changeScore(3000)" >Change Score</ button > |
...
) 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) { return state.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) { return state.score } }, mutations: { increment (state, step) { state.score += step } }, actions: { incrementScore: ({ commit }, delay) => { setTimeout(() => { commit( 'increment' , 3) }, delay) } } } const store = new Vuex.Store({ modules: { scoreBoard: childA, resultBoard: childB } }) |
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
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17 |
Vue.component( 'ChildB' ,{ template:` <div class = "child childB" > <h1> Result: {{ result }} </h1> <button @click= "changeResult()" >Change Result</button> </div>`, computed: { result () { return this .$store.getters.result } }, methods: { changeResult () { this .$store.dispatch( 'increaseResult' , 3000); } } }) |
ChildA
, điều duy nhất ta cần thay đổi là phương thức changeScore()
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17 |
Vue.component( 'ChildA' ,{ template:` <div class = "child childA" > <h1> Score: {{ score }} </h1> <button @click= "changeScore()" >Change Score</button> </div>`, computed: { score () { return this .$store.getters.score } }, methods: { changeScore () { this .$store.dispatch( 'incrementScore' , 3000); } } }) |
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ínhnamespaced
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) { return state.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) { return state.score } }, mutations: { increment (state, step) { state.score += step } }, actions: { incrementScore: ({ commit }, delay) => { setTimeout(() => { commit( 'increment' , 3) }, delay) } } } |
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.
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 |
Vue.component( 'ChildB' ,{ template:` <div class = "child childB" > <h1> Result: {{ result }} </h1> <button @click= "changeResult()" >Change Result</button> </div>`, computed: { result () { return this .$store.getters[ 'resultBoard/score' ] } }, methods: { changeResult () { this .$store.dispatch( 'resultBoard/incrementScore' , 3000); } } }) Vue.component( 'ChildA' ,{ template:` <div class = "child childA" > <h1> Score: {{ score }} </h1> <button @click= "changeScore()" >Change Score</button> </div>`, computed: { score () { return this .$store.getters[ 'scoreBoard/score' ] } }, methods: { changeScore () { this .$store.dispatch( 'scoreBoard/incrementScore' , 3000); } } }) |
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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17 |
├── index.html └── src ├── main.js ├── App.vue ├── components │ ├── Parent.vue │ ├── ChildA.vue │ ├── ChildB.vue └── store ├── store.js ├── state.js ├── getters.js ├── mutations.js ├── actions.js └── modules ├── childA.js └── childB.js |
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ó.Chương trình học
Các bài học
Bài học trước Bài học tiếp theo
Chương trình học
Bao gồm Module, Chương, Bài học, Bài tập, Kiểm tra...Chương trình học
Bài học trước Bài học tiếp theo