Vuex應用篇

參考vuex官網範例 + redux 設計模式

Juo Penguin
12 min readSep 16, 2020

索引

資料夾結構: 採用redux pattern

types寫法: ActionTypes 和 MutationTypes

getters

actions

mutations

應用在組件

與redux的簡單比較

Redux Pattern的vuex

vuex store基本寫法

vuex的基本寫法是將所有的actions, mutations, getters統一寫在store中,例如以下程式碼

const store = {
actions: {},
mutations: {},
getters: {},
};

專案逐漸複雜和龐大之後…

然而我們今天專案比較複雜時,甚至會分成modules,每個modules的actions, mutations可能也會有數十個,類似以下程式碼,我們如果只依靠單個module的object(像是以下的userInfo拆成userInfo.js),光是該檔案可能就有數百行,不管是閱讀還是維護都比較不容易。

const saleProduct = {
state: () => ({}),
actions: {}, // 很多actions
mutations: {}, // 很多mutations
getters: {},
};
const userInfo = {
state: () => ({}),
actions: {}, // 這裡也是很多actions
mutations: {}, // 很多mutations
getters: {},
};
// 接著還有很多的module...
const store = {
modules: {
saleProduct,
userInfo,
},
};

這時候會建議採用redux的設計模式: 將每一個actions, mutations各別拆開做管理。

採用Redux Pattern

像是以下的資料夾結構,將store, modules中的每個都分別歸類在各個資料夾

vuex redux pattern

接下來以範例的專案,來實際介紹redux的常用的設計模式,資料夾該怎麼區分,及檔案結構該如何撰寫

Types寫法: 統一管理和命名actions, mutations

利用ActionTypes和MutationTypes的統一管理,將所有actions, mutations的方法名抽出來,我們不只更好管理,而且在使用actions或mutations更不怕寫錯(因為你只要引用該object)。

actions配合ActionTypes, MutationTypes的簡單範例

mutations/index.js

//...
const MutationTypes = {
ADD_TODO: 'ADD_TODO',
SET_TODOS: 'SET_TODOS',
// ...
};

actions/index.js

//...
const ActionTypes = {
ADD_TODO: "ADD_TODO",
DELETE_TODO: "DELETE_TODO",
}
export default ActionTypes;

actions/todo-actions.js

import ActionTypes from '.'function addTodo() {}
function removeTodo() {}

function deleteAllTodos() {
commit(MutationTypes.SET_TODOS, [])
//使用MutationTypes來commit,不怕寫錯function name
}
const todoActions = {
[ActionTypes.ADD_TODO]: addTodo,
[ActionTypes.DELETE_TODO]: removeTodo,
}
export default todoActions;

接著以實際的範例來一個個介紹getters, actions, mutations(module的namespaced: true)

getters

getters/cart-getters.js

const cartProducts = (state, getters, rootState) => state.cartItems.map(({ id, quantity }) => {
const product = rootState.products.allProducts.find((p) => p.id === id);
return {
id,
title: product.title,
price: product.price,
quantity,
};
});
const cartTotalPrice = (state, getters) => (
//...
);
const cartGetters = {
cartProducts,
cartTotalPrice,
};
export default cartGetters;

actions

負責commit任務給mutations修改state

index.js

主要放置ActionTypes

const CART_ACTION_TYPES = {
CHECKOUT_ORDERS: 'CHECKOUT_ORDERS',
ADD_PRODUCT_TO_CART: 'ADD_PRODUCT_TO_CART',
DECREMENT_PRODUCT_IN_CART: 'DECREMENT_PRODUCT_IN_CART',
};
const PRODUCTS_ACTION_TYPES = {
GET_ALL_PRODUCTS: 'GET_ALL_PRODUCTS',
};
const ActionTypes = {
...CART_ACTION_TYPES,
...PRODUCTS_ACTION_TYPES,
};
export default ActionTypes;

actions/cart-actions.js

這邊有action中呼叫async的使用範例

actions/product-actions.js

mutations

index.js

const CART_MUTATION_TYPES = {
PUSH_PRODUCT_TO_CART: 'PUSH_PRODUCT_TO_CART',
REMOVE_PRODUCT_FROM_CART: 'REMOVE_PRODUCT_FROM_CART',
ADD_ITEM_QUANTITY: 'ADD_ITEM_QUANTITY',
DECREMENT_ITEM_QUANTITY: 'DECREMENT_ITEM_QUANTITY',
SET_CART_ITEMS: 'SET_CART_ITEMS',
SET_CHECKOUT_STATUS: 'SET_CHECKOUT_STATUS',
};
const PRODUCTS_MUTATION_TYPES = {
SET_PRODUCTS: 'SET_PRODUCTS',
DECREMENT_PRODUCTS_INVENTORY: 'DECREMENT_PRODUCTS_INVENTORY',
ADD_PRODUCTS_INVENTORY: 'ADD_PRODUCTS_INVENTORY',
};
const MutationTypes = {
...CART_MUTATION_TYPES,
...PRODUCTS_MUTATION_TYPES,
};
export default MutationTypes;

mutations/cart-mutations.js

mutations/product-mutations.js

應用在組件

省略組件部分細節的寫法,以script為主,主要都以map輔助函數來映射actions, mutations…到組件中

store/store.js

components/ProductList.vue

//...<script>
import { mapState, mapActions } from 'vuex';
import ActionTypes from '../actions';
export default {
computed: mapState({
products: (state) => {
return state.products.allProducts;
},
}),
methods: mapActions('cart', {
addProductToCart: ActionTypes.ADD_PRODUCT_TO_CART,
}),
created() {
this.$store.dispatch(`products/${ActionTypes.GET_ALL_PRODUCTS}`);
},
};
</script>

components/ShoppingCart.vue

// ...
<script>
import { mapGetters, mapState } from 'vuex';
import ActionTypes from '../actions/index';
export default {
computed: {
...mapState({
checkoutStatus: (state) => state.cart.checkoutStatus,
}),
...mapGetters('cart', {
products: 'cartProducts',
total: 'cartTotalPrice',
}),
},
methods: {
checkout(products) {
this.$store.dispatch(`cart/${ActionTypes.CHECKOUT_ORDERS}`, { products });
},
decrementCartItem(product) {
this.$store.dispatch(`cart/${ActionTypes.DECREMENT_PRODUCT_IN_CART}`, { product });
},
},
};
</script>

完成!

與Redux的簡單比較

redux和vuex皆為解決全域狀態問題的library,但這兩個有著根本上的差異,除了redux的actions和reducers皆是Pure Function,且不一定要用在react;而vuex是為vue量身打造的。

共同之處

  • 主要流程皆遵循單向觸發資料更新的流程:
組件觸發dipatch >> actions >> reducers(mutations) >>state更新 >> 組件更新
  • action的object寫法幾乎一樣(畢竟皆參考自flux)

redux: action的return object
vuex: dispatch, commit接受的參數形式之一

{ 
type: ACTION_TYPE,
payload
}

相異之處

  • 任務: actions(redux) v.s. actions(vuex)

redux的任務為return一個純object,將此object丟給reducer

const reduxAction = (payload) => ({ 
type: ACTION_TYPE,
payload
})

vuex的任務則負責觸發commit,不必要return

const vuexAction = ({ commit }, payload) => { 
commit(type: 'MUTATION_TYPE', payload)
}

備註: vuex如果使用async functions,可以直接在action中組合與套用;redux如果要async處理action,會將async的任務放在”Thunk”,見官網說明
https://redux.js.org/advanced/async-actions#async-action-creators

  • 異動: reducers(redux) v.s. mutations(vuex)

reducers為pure function不會在此直接更改state,將最新的整個state丟給redux store去負責觸發更新
(這也是react和vue核心之間的最大差異)

const reducer = (state, action) => state //更新過的state

mutations: 在此直接更改state

const mutations = { 
addOne(state, payload) {
state+=payload.number
}
}
// 不return state
  • Store狀態: state(redux) v.s. state(vuex)

state(redux)通常只需要用到一個store,並搭配combineReducers,進行不同的reducers合併(其實類似modules的概念)

詳見官網的combineReducers說明
但是如果必要時也能使用多個store,詳見此討論 stackOverflow

const reducers = combineReducers({ a, b })
// a, b皆為reducer
const state = createStore(reducers, initState)

state(vuex)則是設計了"modules”以便store拆分

const store = new Vuex.Store({
modules: {
a, // 含state, actions, mutations...
b,
},
});

總結

此範例僅為個人習慣的寫法,一般使用上其實照著官方的範例(如下面的連結)即可。

此方法是將action, mutations…各別分開管理,如果專案更龐大,或是module有更深的嵌套結構,那麼資料夾和檔案可以分得更細,以方便維護和閱讀。

唯一建議的是,以ActionTypes和MutationTypes統一管理,除了防止重複命名,維護者也可以很好的理解現在專案有哪些商業邏輯 。

--

--

Juo Penguin
Juo Penguin

Written by Juo Penguin

不挑食的雜食者,近期的目標是瘦10公斤。

No responses yet