初見狀態管理工具 Vuex (4) - Modules


Posted by Calon on 2022-05-03

當專案越來越大時,Vuex 允許我們將 store 切分成 modules 來方便管理,並且每個 module 都有自己的 state、mutations、getters、actions:

const moduleA = {
  // 注意 state 是個 function
  state: () => ({...}),
  getters: {...},
  mutations: {...},
  actions: {...},
};

const moduleB = {
  state: () => ({...}),
  getters: {...},
  mutations: {...},
  actions: {...},
};

const store = createStore({
  state: {...},
  getters: {...},
  mutations: {...},
  actions: {...},
  modules: {
    moduleA,
    moduleB,
  },
});

而調用 modules 裡面的資料時只要在原本的 state 後面接上 module.module 的 state

const moduleA = {
  state: () => ({
    text: 'I'm a module.',
  }),
};

const store = createStore({
  modules: {
    moduleA,
  },
});

// 調用 moduleA 的 state 裡的 text
store.state.moduleA.text;

調用 getters、mutations、actions 則和之前的寫法一樣:

const moduleA = {
  state: () => ({
    text: 'moduleA',
  }),
  getters: {
    reverseText(state) {
      const textArr = state.text.split('');
      return textArr.reverse().join('');
    },
  },
  mutations: {
    addExclamationMark(state) {
      state.text = `${state.text}!`;
    },
  },
  actions: {
    async fetchRandomUser() {
      const json = await fetch('https://randomuser.me/api/')
        .then((res) => res.json());
      console.log(json);
    },
  },
};

const store = createStore({
  modules: {
    moduleA,
  },
});

// 調用 moduleA 裡面的 getters、mutations、actions
store.getters.reverseText;
store.commit('addExclamationMark');
store.dispatch('fetchRandomUser');

使用 mapState 導入 modules 的 state

我們可以使用 mapState 裡面傳入物件並搭配 function 來導入 modules 的 state:

computed: {
  ...mapState({
    text: (state) => state.moduleA.text,
  }),
},

而 mapGetters、mapMutations、mapActions 和之前的用法一樣。

取得外層 store 物件的 state 和 getters

在 module 裡面 getters 和 actions 都可以取得外層的 state,getters 還可以取得外層的 getters,只要在 getters、actions 的方法裡面帶上第三個參數 rootState 就可以取得外層的 state,而在 getters 帶上第四個參數 rootGetters,則可以取得外層 getters:

const moduleA = {
  getters: {
    getRootState(state, getters, rootState, rootGetters) {...},
  },
  actions: {
    fetch(context) { context.rootState... },
    // 也可以使用物件解構 { rootState }
  },
};

Namespacing

在上面的範例裡面我們可以發現調用 modules 的 getters、mutations、actions 時,不用像調用 state 時一樣加上 module 對應的名稱,如果我們這時在外層與 modules 裡面的 getters、mutations、actions 各有一個相同的名稱的方法,那結果會怎樣?

const moduleA = {
  state: () => ({
    text: 'moduleA',
  }),
  getters: {
    reverseText(state) {
      const textArr = state.text.split('');
      return textArr.reverse().join('');
    },
  },
  mutations: {
    commitText(state) {
      state.text += '!';
    },
  },
  actions: {
    action() {
      console.log('module action');
    },
  },
};

const store = createStore({
  state: {
    text: 'store',
  },
  getters: {
    reverseText(state) {
      const textArr = state.text.split('');
      return textArr.reverse().join('');
    },
  },
  mutations: {
    commitText(state) {
      state.text += '!';
    },
  },
  actions: {
    action() {
       console.log('store action');
    },
  },
});

store.getters.reverseText;
store.commit('commitText');
store.dispatch('action');

結果會是都有執行,並且 getters 會出現 [vuex] duplicate getter key: reverseText 的錯誤:

store.getters.reverseText;
// 出現 getters 重複 key 的錯誤
// [vuex] duplicate getter key: reverseText
// 並會印出外層 store 的 getters:erots

store.commit('commitText');
// 使用 dev tools 查看會發現兩個 text 都會被加上 !

store.dispatch('action');
// 分別印出 store action 與 moduleA action

會出現這樣的結果是因為在預設狀態下 getters、mutations、actions 都是註冊在全域底下。

那如果我就是想要有重複的命名但又想避免這樣的錯誤呢?
Vuex 也有提供方法來解決,只要在 modules 裡面加上 namespaced: true

const moduleA = {
 namespaced: true,
};

這樣 Vuex 就會自動幫我們在 getters、mutations、actions 前面根據 modules 的名稱加上對應的路徑,而我們要調用時就會變成下面這樣:

store.getters['moduleA/reverseText'];
store.commit('moduleA/commitText');
store.dispatch('moduleA/action');

而 Namespace 一樣可以用 mapGetters、mapMutations、mapActions 來導入:

computed: {
  ...mapGetters([
    'moduleA/reverseText', // this['moduleA/reverseText']
  ]),
},
methods: {
  ...mapMutations([
    'moduleA/commitText', // this['moduleA/commitText']
  ]),
  ...mapActions([
    'moduleA/action', // this['moduleA/action']
  ]),
},

如果覺得上面這樣寫太長太麻煩,我們可以在 mapGetters、mapMutations、mapActions 的第一個參數傳入 module 的路徑名稱:

computed: {
  ...mapGetters('moduleA', [
    'reverseText', // this.reverseText
  ]),
},
methods: {
  ...mapMutations('moduleA', [
    'commitText', // this.commitText'
  ]),
  // 假設 moduleA 裡面有 moduleB,並且要導入 moduleB 的 actions
  ...mapActions('moduleA/moduleB', [
    'action', // this.action
  ]),
},

你也可以用 Vuex 提供的 createNamespacedHelpers 來快入導入特定 module 裡面的 getters、mutations、actions:

import { createNamespacedHelpers } from 'vuex';

const { 
  mapState,
  mapActions,
  mapGetters,
  mapMutations,
  } = createNamespacedHelpers('moduleA');

export default {
  computed: {
    ...mapState(['text']), // moduleA.text
    ...mapGetters(['reverseText']), // moduleA/reverseText
  },
  methods: {
    ...mapActions(['fetchRandomUser']), // moduleA/fetchRandomUser
    ...mapMutations(['commitText']), // moduleA/commitText
  },
};

調用外層 store 的 commit、dispatch

如果想要在 module 裡使用外層的 mutations 或是 actions,我們只要在調用時傳入第三個參數 { root: true }

const moduleA = {
  actions: {
    commitAndDispatchRoot({ commit, dispatch }) {
      dispatch('fetchSomething', null, { root: true });
      commit('commitSomething', null, { root: true });
    },
  },
};

const store = createStore({
  mutations: {
    commitSomething() {
      ...
    },
  },
  actions: {
    fetchSomething() {
      ...
    },
  },
});

參考資料
  • Vuex 官方文件
  • 許國政(Kuro),《重新認識 Vue.js:008 天絕對看不完的 Vue.js 3 指南》

#vuex #Vue.js







Related Posts

[Day03] Lazy Evaluation

[Day03] Lazy Evaluation

每日心得筆記 2020-06-15(一)

每日心得筆記 2020-06-15(一)

沒有 Switch 了

沒有 Switch 了


Comments