Vuex應用篇
參考vuex官網範例 + redux 設計模式
索引
資料夾結構: 採用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中的每個都分別歸類在各個資料夾
接下來以範例的專案,來實際介紹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皆為reducerconst state = createStore(reducers, initState)
state(vuex)則是設計了"modules”以便store拆分
const store = new Vuex.Store({
modules: {
a, // 含state, actions, mutations...
b,
},
});
總結
此範例僅為個人習慣的寫法,一般使用上其實照著官方的範例(如下面的連結)即可。
此方法是將action, mutations…各別分開管理,如果專案更龐大,或是module有更深的嵌套結構,那麼資料夾和檔案可以分得更細,以方便維護和閱讀。
唯一建議的是,以ActionTypes和MutationTypes統一管理,除了防止重複命名,維護者也可以很好的理解現在專案有哪些商業邏輯 。