Vuex

整理 Vuex 筆記

流程與說明

用下列二張圖,簡易表示 Vuex 的概念。

單向數據流

與 Vue 相似之處


State

存放所有要共用的變數

store.js
1
2
3
4
5
6
7
8
9
// store.js
const state = {
isLoading: false,
formData: {
name: 'Kanboo',
sex: 'male',
age: 18
}
};
vue檔
1
2
3
4
5
6
7
8
9
10
// .vue
import { mapState } from 'vuex'

// 方法1(不推)
this.$store.state.isLoading

// 方法2(建議)
computed: {
...mapState(['isLoading','formData'])
}

Getter

如同 computed,用來取得 state 裡的變數值,或者經過計算、篩選的 state 值

store.js
1
2
3
4
5
6
7
8
9
// store.js
const getters = {
isLoading: state => state.isLoading,
formData: state => state.formData,
doneTodos: state => {
// 經過計算篩選的值
return state.todos.filter(todo => todo.done);
}
};
Vue檔
1
2
3
4
5
6
7
8
9
10
// .vue
import { mapGetters } from 'vuex'

// 方法1(不推)
this.$store.state.doneTodos

// 方法2(建議)
computed: {
...mapGetters(['isLoading', 'formData', 'doneTodos'])
}

範例

分別敘述下列三種用法

  • 用法 1:計算 尚未 Done 的項目
  • 用法 2:計算 已 Done 的項目
    • 使用 getters 參數,取得原先已有的itemsNotDone函式
  • 用法 3:透過外層「傳遞傳數」,再加以運算出結果
store.js
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
// store.js
const state = {
todos: [
{ id: 1, done: true },
{ id: 2, done: true },
{ id: 3, done: false },
{ id: 4, done: false },
{ id: 4, done: true }
]
};

const getters = {
// 用法1
itemsNotDone(state) {
return state.todos.filter(todo => !todo.done).length; // 過濾
},
// 用法2
itemsDone(state, getters) {
return state.todos.length - getters.itemsNotDone; // 搭配「用法1」運用
},
// 用法3
itemsWithID(state) {
return id => {
return state.todos.filter(item => item.id === id); // 接收「參數」
};
}
};

達到傳遞傳數的方式,需配合上述的「用法 3」的設定

Vue檔
1
2
3
4
5
6
7
8
9
10
11
12
13
import { mapState, mapGetters } from 'vuex';

export default {
computed: {
...mapState(['todos']),
...mapGetters({'itemsWithID'})
},
methods: {
getID() {
this.itemWithID(16); // 傳遞參數給getters的函式
}
}
}

延伸上例,
有可能會遇到不同Modules,卻命名到一樣名稱的話,
這時我們可以利用「更名」方式,避免實際運用時,產生衝突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { mapState, mapGetters } from 'vuex';

export default {
computed: {
...mapState(['todos']),
...mapGetters({
todoItem: 'itemsWithID' // 將 itemsWithID 改用 todoItem 名稱
})
},
methods: {
getID() {
this.todoItem(16); // 使用新名稱呼叫
}
}
};

Mutations

用於更改 state 裡的變數。

注意: 請記住所有要更改 state 的變數值,都只能透過 mutations 進行更改。

store.js
1
2
3
4
5
6
7
8
9
// store.js
const mutations = {
LOADING(state, value) {
state.isLoading = value;
},
FORMDATA(state, value) {
state.formData = value;
}
};
Vue檔
1
2
3
4
5
6
7
8
9
10
// .vue
import { mapMutations } from 'vuex';

// 方法1(不推)
this.$store.commit('LOADING' , true)

// 方法2(建議,不過還是透過actions觸發比較好)
methods: {
...mapMutations(['LOADING','FORMDATA'])
}

觸發 Mutations

承上例,
若要觸發 Vuex 的 mutations,因為我們有解構賦值 mapMutations 裡的 methods,
所以就直接使用即可,不建議使用this.$store.commit 的方式,
如下所示

Vue檔
1
2
3
4
5
6
7
8
9
10
11
// .vue

// 基本型別 (Primitives)
this.LOADING(false);

// 物件
this.FORMDATA({
name: 'Lucas',
sex: 'male',
age: 4
});

Actions

如同 methods
在執行過程中,可透過 AJAX 取的資料後,再計算分析後,將結果回塞給 state 裡的變數值,
But…剛剛上面說過,所有要更改 state 的變數值,都只能透過 mutations 進行更改。

store.js
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
// store.js
const actions = {
loading({ commit }, value) {
commit('LOADING', value)
},
getProducts({ commit }, page = 1) {
const api = `/api/${process.env.DBPATH}/products?page=${page}`

commit('LOADING', true)

axios.get(api).then(res => {
if (res.data.success)
// action 不應該直接修改 state 的值,
// 要使用 commit 的方式呼叫 mutations 去改值
// 以下寫法在嚴格模式會發生錯誤
// state.isLogin = true;
commit('PRODUCTS', res.data.products)
commit('PAGINATION', res.data.pagination)
} else {
this.$bus.$emit('messsage:push', res.data.message, 'danger')
}

commit('LOADING', false)
})
}
}
Vue檔
1
2
3
4
5
6
7
8
9
10
// .vue
import { mapActions } from 'vuex';

// 方法1(不推)
this.$store.dispatch('loading', true)

// 方法2(建議)
methods: {
...mapActions(['loading','getProducts']),
}

Mutations vs Actions

說明二者的差異點,
另外需注意的是「Mutations 和 Actions」都是解構賦值至 vuemethods 位置,
所以要注意二者的 Function Name 是否有衝突。

Mutations

Mutations 一定要是「同步式」更改 state 的變數值,不可以是「非同步」更改變數值。

錯誤範例

store.js
1
2
3
4
5
6
7
8
9
// store.js
const mutations = {
LOADING(state, value) {
setTimeout(() => {
// 非同步, 不行
state.isLogin = value;
}, 1000);
}
};

正確範例

store.js
1
2
3
4
5
6
// store.js
const mutations = {
LOADING(state, value) {
state.isLoading = value;
}
};

Actions

Actions 可以是非同步更改 state 的值,
但在 strict(嚴格) 模式下,Actions 不應該直接更改 state 的值,
而且是要 Actions 觸發 Mutations 去改 state 的值。

非嚴格模式(不推)

store.js
1
2
3
4
5
6
7
8
// store.js
const actions = {
login({ commit, state }, value) {
setTimeout(() => {
state.isLoading = value; // 在strict下,這樣不行
}, 1000);
}
};

嚴格模式(建議)

store.js
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
// store.js
const actions = {
addToCart({ commit, dispatch }, { id, qty = 1 }) {
const api = `/api/${process.env.DBPATH}/cart`;
const cart = {
product_id: id,
qty
};

commit('LOADING', true); // 觸發 mutation 變更值

axios.post(api, { data: cart }).then(res => {
if (res.data.success) {
dispatch('getCart'); // 呼叫另一個 action
}
commit('LOADING', false); // 觸發 mutation 變更值
});
},
getCart({ commit }) {
const api = `/api/${process.env.DBPATH}/cart`;

axios.get(api).then(res => {
if (res.data.success) {
commit('CART', res.data.data); // 觸發 mutation 變更值
}
});
}
};

Action 傳送多個值

因為 Action 的接收參數只有二個,而第一個參數固定是 context
所以只剩第二個參數可以傳送「資料」,若只有一個值的話,沒什麼問題,
但如果同時間,要傳送多筆資料,又卡在只剩一個參數可塞值的話,
該如何解決呢?

參數

  • 第一個參數:context
  • 第二個參數(可選):payload

context 對象包含以下屬性:

1
2
3
4
5
6
7
8
{
state, // 等同于 `store.state`,若在模組中则为「局部」状态
rootState, // 等同于 `store.state`,只存在于模組Module中
commit, // 等同于 `store.commit`
dispatch, // 等同于 `store.dispatch`
getters, // 等同于 `store.getters`
rootGetters; // 等同于 `store.getters`,只存在于模組Module中
}

官方 API:Action

範例(單個值)

Vue檔
1
2
3
4
5
6
7
8
9
methods: {
...mapActions(['loading']),
createOrder() {
// ...略
this.loading(true)
// do something...
this.loading(false)
}
}

範例(多個值)

籍由傳送 物件 並透過 解構賦值 的配合,達到傳遞多筆資料。

store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// store.js
const actions = {
// 第二參數 payload 透過解構賦值的方式,達到傳遞多筆資料
addToCart({ commit, dispatch }, { id, qty = 1 }) {
// ...略

const cart = {
product_id: id,
qty
};

// ...略
}
};
Vue檔
1
2
3
4
5
6
7
8
9
10
11
// .vue
methods: {
...mapActions(['addToCart', 'getCart']),
addItem() {
// 傳送 物件
this.addToCart({
id:product.id,
qty:product.num
})
}
}

Action 非同步處理多事件

有時我們可能要完成一個事的話,需等待前一個 AJAX 回傳結果,才能繼續執行下一步驟,
這樣的話,我可以利用 promise 完成此事。

範例

下列為要完成 actionB 的動作時,事前需先完成 actionA 的事情,
所以 actionA 多包一層 promise 來回應,
actionB 則用 .then 來等待上一事件完成,
才會繼續執行後續動作 commit('someOtherMutation')

Vuex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation') // do something
resolve() // 回應「成功完成」
}, 1000)
})
},
actionB ({ commit, dispatch }) {
return dispatch('actionA').then(() => { // 1. 等待 actionA 完成
commit('someOtherMutation') // 2. 執行後續動作
})
}
}

Module 模組化

簡單來說,就是原本全部都寫在 store.js 裡的程式,
全部拆分一支支的模組 js,各別存放有相互關係的功能,
最終再透過 Vuex 的 modules 引入各個模組。

事前提醒

當我們將 store.js 裡,部份程式拆分為模組 js 時,
此時模組 js 的程式作用域如下:

  • state 屬於模組「區域」變數
  • actions、mutations、getters 仍是屬於「全域」變數

所以當不同模組裡,有命名到「相同名稱」的話,就會出現錯誤訊息,
只要我們將名稱不要重覆即可,並不是一件很嚴重的事,
我們也可以從命名的方式,用來分辦是屬於哪個模組內的 methods。

官方:命名空间

高封裝度和復用性

如果希望你的模塊具有更高的封裝度和復用性,
你可以通過添加 namespaced: true 的方式使其成為帶命名空間的模塊,
這時「actions、mutations、getters」就變成「區域」變數。

範例

將原本在 store.js 拆解出 「產品模組、購物車模組」,
剩下共用的部份,依舊放在 store.js 裡即可,
如:畫面 loading 效果

Product.js 模組
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
// 產品模組
const state = {
products: [],
product: {},
pagination: {}
};

const getters = {};

const actions = {
getProducts({ commit }, page = 1) {
// do something
},
getProduct({ commit }, id) {
// do something
},
updateProduct({ commit }, payload) {
// do something
}
};

const mutations = {
PRODUCTS(state, value) {
state.products = value;
},
PRODUCT(state, value) {
state.product = value;
},
PAGINATION(state, value) {
state.pagination = value;
}
};

export default {
namespaced: true, // 將 actions, mutations, getters 變更為區域變數
state,
getters,
actions,
mutations
};
Cart 模組
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
// 購物車模組
const state = {
cart: {}
};

const getters = {};

const actions = {
addToCart({ commit, dispatch }, { id, qty = 1 }) {
// do something
},
removeCartItem({ commit, dispatch }, id) {
// do something
},
getCart({ commit }) {
// do something
}
};

const mutations = {
CART(state, value) {
state.cart = value;
}
};

export default {
namespaced: true, // 將 actions, mutations, getters 變更為區域變數
state,
getters,
actions,
mutations
};
store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// store.js
import Vuex from 'vuex';
import Vue from 'vue';

import Products from './modules/Products'; // 產品模組
import Carts from './modules/Carts'; // 購物車模組

Vue.use(Vuex);

// ..略

const modules = {
Products,
Carts
};

export default new Vuex.Store({
// ...
modules // 載入模組
});
Vue檔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// .vue
methods: {
// 主要 Vuex store
...mapActions(['loading']),
// 產品模組
...mapMutations('Products', ['PRODUCT']),
...mapActions('Products', ['getProducts', 'getProduct', 'updateProduct']),
// 購物車模組
...mapActions('Carts', [
'removeCartItem',
'addToCart',
'getCart'
]),
addCouponCode() {
// do something
},
createOrder() {
// do something
}
},

細節可參考下列連結:

Vuex 範例:modules
使用範例:vue 引用方式

不同模組函式名稱重覆

當我們開始拆分模組時,很有可能在不同模組下,命名到同樣名稱的函式,
這樣當我們在 Vue 引用時,很可以就會產生衝突。

情況

假設 A 模組B 模組 裡,同時有相同 Actions 的名稱叫「setFilter」的話,
這時又要同時引入的話,該怎麼處理呢?

1
2
3
4
5
6
7
8
9
10
11
export default () {
methods: {
...mapActions('module_A', {set_A:'setFilter'}), // 將 setFilter 改用 set_A 名稱
...mapActions('module_B', {set_B:'setFilter'}), // 將 setFilter 改用 set_B 名稱

changeFilter () {
this.set_A() // A模組 的 setFilter
this.set_B() // B模組 的 setFilter
}
}
}

官方:在组件中分发 Action
Vuex: Actions with same name in different modules


參考文件

官方:Vuex
2018 iT 邦幫忙鐵人賽:Vuex 學習筆記
GitHub:2017 年,線上讀書會,vue vuex 分享