Vuex API 語法速查:安裝、state、mutations、actions、getters、modules
整理當時上課做的一系列 Vuex 練習筆記——安裝與注入 store、state 的兩種取用方式、mutations 的三種 commit 傳參方式、actions 裡串接多個非同步動作、getters 回傳值/參數/函式三種型態、modules 拆分,以及 v-model 搭配 store 狀態的 get/set 寫法。核心概念與 Pinia 比較另外整理在別篇,這篇純粹是語法速查。
W
這篇是上課期間陸續做的 Vuex 練習筆記合輯,涵蓋安裝到 state/mutations/actions/getters/modules 每一塊的具體語法。核心概念的說明、Vuex 遇到的瓶頸、跟 Pinia 的完整比較已經整理在《Pinia 與 Vuex:差在哪、怎麼選、怎麼寫》,這篇不重複講「為什麼」,純粹當語法查詢用;一個從頭到尾能動的練習範例見《Vuex 上課筆記:用一個 todos 範例串起 state、mutation、action》。
安裝與注入
npm install vuex --save
// main.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
store/index.js(彙整 Vuex 所有行為的地方):
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
});
main.js 裡注入:
import store from './store';
new Vue({
store,
});
一定要在最上層(main.js)注入 store,之後元件裡用 mapState/mapMutations/mapActions 這些輔助函式才會生效,沒注入的話這些輔助函式拿不到 store 實例。
Store 三大規則
- app 層級的狀態集中放進 store——例如會員登入狀態、跨元件都會用到的商品列表
- mutations 是改變 state 的唯一方法,且必須是同步的
- 非同步操作都放在 actions 裡
state:儲存狀態
state 裡可以放任何東西,但要事先定義好欄位,就算給空值也沒關係:
state: {
loading: false,
todos: [],
}
沒有事先定義、想動態加欄位的話要用 Vue.set(state, 'loading', false),但這不是建議的做法——事先定義好結構,可讀性跟可維護性都比較好。
取用 state 一定要透過 computed(不要在 methods 或直接在模板裡運算),這樣資料變動時畫面才會自動反應:
// 第一種:直接用 $store
computed: {
loading() {
return this.$store.state.loading;
},
}
// 第二種:用 mapState
import { mapState } from 'vuex';
computed: {
...mapState(['loading']),
}
mutations:唯一能改變 state 的地方
mutations: {
SET_LOADING(state, value) {
state.loading = value;
},
}
必須是同步操作,不能在 mutation 裡直接呼叫 API——因為 Vuex 的 devtools 是靠追蹤每一次 mutation 來記錄狀態變化的時間點,非同步操作會讓這個追蹤失去意義(沒辦法確定狀態是在哪個時間點被改變的)。命名習慣上,mutation 常用全大寫命名,跟一般方法區分開來。
在元件裡觸發 mutation 用 commit,有三種傳參方式:
// 傳入字串(不帶參數)
this.$store.commit('SET_LOADING');
// 傳入參數(payload)
this.$store.commit('SET_LOADING', true);
// 傳入物件(type 欄位指定要觸發哪個 mutation)
this.$store.commit({
type: 'SET_LOADING',
value: true,
});
用 mapMutations 映射到 methods:
import { mapMutations } from 'vuex';
methods: {
// 字串陣列方式
...mapMutations(['SET_LOADING']),
// 物件別名方式(本地方法名稱不同於 mutation 名稱時)
...mapMutations({
setLoading: 'SET_LOADING',
}),
}
actions:非同步操作
actions: {
async loadTodos(context) {
const res = await axios.get('/api/todos');
context.commit('SET_TODOS', res.data); // 非同步完成後才 commit
},
}
action 不能直接修改 state,一定要透過 commit 觸發 mutation——這是 Vuex 架構上的硬性規則,就算 action 裡已經非同步取得資料,也不能跳過 mutation 直接 state.todos = res.data。
在元件裡觸發 action 用 dispatch:
this.$store.dispatch('loadTodos');
// 帶參數
this.$store.dispatch('loadTodos', { page: 2 });
// 物件別名寫法
this.$store.dispatch({ type: 'loadTodos', page: 2 });
action 裡面做多次 commit 是同步依序執行的(一行一行往下跑,不會因為前一個 commit 是同步操作而互相打斷)。
mapActions 用法跟 mapMutations 對稱:
import { mapActions } from 'vuex';
methods: {
...mapActions(['loadTodos']),
...mapActions({ loadMyTodos: 'loadTodos' }), // 物件別名
}
action 完成後呼叫另一個 action、或需要等待非同步結果,靠回傳 Promise 串接:
actions: {
async loadTodos(context) {
const res = await axios.get('/api/todos');
context.commit('SET_TODOS', res.data);
return res.data; // 回傳值,讓外部可以用 .then() 接
},
}
this.$store.dispatch('loadTodos').then((todos) => {
console.log('載入完成', todos);
});
getters:store 層級的 computed
getters: {
// 直接依 state 回傳值
todoCount(state) {
return state.todos.length;
},
// 回傳參數(依賴其他 getter)
doneTodoCount(state, getters) {
return getters.todoCount - state.todos.filter((t) => !t.done).length;
},
// 回傳一個函式,讓呼叫端自己帶參數
getTodoById(state) {
return (id) => state.todos.find((t) => t.id === id);
},
}
getters 一定是吃 state 當第一個參數,第二個參數可以拿到其他 getters。回傳函式這個型態很實用——一般的 getter 結果會被快取(依賴不變就不重新計算),但回傳函式的寫法每次呼叫都會重新執行,適合需要外部傳參數查詢的情境(例如「依 ID 查某一筆」)。
在元件裡使用:
// 直接用 $store.getters
computed: {
todoCount() {
return this.$store.getters.todoCount;
},
}
// mapGetters,也支援物件別名
import { mapGetters } from 'vuex';
computed: {
...mapGetters(['todoCount']),
...mapGetters({ count: 'todoCount' }),
}
getTodoById 這種回傳函式的 getter,在元件裡當函式呼叫:
this.$store.getters.getTodoById(3);
modules:拆分大型 store
// store/modules/todos.js
export default {
state: { todos: [] },
mutations: {
SET_TODOS(state, value) {
state.todos = value;
},
},
actions: {
async loadTodos({ commit }) {
const res = await axios.get('/api/todos');
commit('SET_TODOS', res.data);
},
},
};
// store/index.js
import todos from './modules/todos';
export default new Vuex.Store({
modules: { todos },
});
拆成 module 之後,state 的存取路徑要多一層模組名稱:
this.$store.state.todos.todos; // 模組名稱.state 欄位名稱
v-model 搭配 store 狀態:getter/setter computed
v-model 需要同時讀取跟寫入,直接綁 $store.state.xxx 沒辦法寫入(state 不能被直接賦值),要包一層帶 get/set 的 computed:
computed: {
keyword: {
get() {
return this.$store.state.keyword;
},
set(value) {
this.$store.commit('SET_KEYWORD', value);
},
},
}
<input v-model="keyword">
畫面上看起來就是普通的雙向綁定,實際上讀取跟寫入分別走 state 讀取與 commit 觸發 mutation 兩條路徑,符合「只有 mutation 能改 state」的規則。
現在還適用嗎
這篇整理的全部語法對既有的 Vuex 專案依然完全有效,Vuex 沒有被移除、也還在維護。但新專案的建議已經是 Pinia——getter/setter computed 這種為了配合 v-model 而繞的寫法,在 Pinia 裡不需要,直接讀寫 state 就好;mutations 這一整層在 Pinia 裡也被拿掉了。完整比較看《Pinia 與 Vuex:差在哪、怎麼選、怎麼寫》。