以下是 Vuex 常用的操作,以及使用 TypeScript 所會遇到的狀況處理。

Getter

將 Store 中的 State 取出來並預先做一些處理。

先新增一個 User 進到 Store 裡,使用 getters 來加入一些字串:

  • store.ts
    import { InjectionKey } from 'vue'
    import { createStore, Store } from 'vuex'
    
    interface User {
      name: string,
      age: number,
      gender: string
    }
    
    export interface State {
      count: number,
      user: User,
    }
    
    export const key: InjectionKey<Store<State>> = Symbol()
    
    export const store = createStore<State>({
      state: {
        count: 0,
        user: {
          name: 'Paul',
          age: 80,
          gender: 'male'
        }
      },
      getters: {
        getUserInfo: state => {
          return state.user.name + ' is ' + state.user.age + ' years old.'
        }
      },
      //...	
    })
    
  • Home.vue
    <template>
      <p class="text-md">{{ userInfo }}</p>
    </template>
    
    <script setup lang="ts">
    import { computed } from 'vue';
    import { useStore } from 'vuex';
    import { key }  from '../store';
    
    const store = useStore(key);
    const userInfo = computed(() => store.getters.getUserInfo );
    
    //...
    </script>
    

Mutations

Mutations 是 Vuex 唯一能修改 State 的方式。

const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // mutate state
      state.count++
    }
  }
})

使用方式為:

store.commit('increment')

帶有參數的 method

如果所設定的 increment 帶有參數:

const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state, n) {
      // mutate state
      state.count += n
    }
  }
})

使用方式為:

store.commit('increment', 5)

其他更進階的使用方式可以參考 Mutations

Actions

與 Mutations 相似,但有所不同的是:

  • Actions 不會改變 State,而是呼叫 Mutations 來改變
  • Actions 可以異步( asynchronous ) 操作
const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

可以簡寫成:

//...
actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

使用方式為:

store.dispatch('increment')

如果想要感受一下異步操作,可以將 Actions 設定一個時間來執行:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

Module

當應用程式變大時,如果將所有狀態都集中在 store 實例中,就會變得複雜且難以維護。

所以 Vuex 提供了將 store 分割成模塊(module) 的方式,讓每個 module 都擁有自己的 state、mutation、action、getter 甚至是子模塊。

以下會實作 JavaScript 與 TypeScript 的作法。

使用 JavaScript

首先,建立一個資料夾名為 store/ 來存放有關 Vuex 的文件。所以這邊可以將上面在使用的 store.js 放入 store/ 中。

接著建立一個 user.js 檔案:

export const userModule = {
  state: () => ({
  name: 'Paul',
    age: 80,
    gender: 'male',
  }),
  getters: {
    getUserInfo => {
      return state.name + ' is ' + state.age + ' years old.'
    }
  },
}

接下來將 count 狀態提取出來,變成一個獨立的 module ( 這裡為了方便,就不另外再建立新的檔案來存放 count 狀態 ):

import { InjectionKey } from 'vue'
import { createStore, Store } from 'vuex'
import { countModule } from './buttonCount';
import { userModule } from './user'

const countModule = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment(state) {
      state.count++;
    }
  },
}


export const store = createStore({
  modules: {
    count: countModule,
    user: userModule  
  }
})

使用 TypeScript

使用 TypeScript 就會稍稍的有一些麻煩,這是因為,如果直接使用 modules 來引入我們預先設定好的 module,TypeScript 會抓不到這些 module 的 state。所以這裡我參考了將modules 的類型添加到 state 中 來讓 TypeScript 來知道這些 state 的資訊。

當然,如果不理會這個問題並直接使用 modules 來呼叫,程式還是可以運作

  • user.ts

    import { Module } from 'vuex';
    
    export type User = {
      name: string;
      age: number;
      gender: string;
    };
    
    export const userModule: Module<User, any> = {
      state: (): User => ({
        name: 'Paul',
        age: 80,
        gender: 'male',
      }),
      getters: {
        getUserInfo: (state) => {
          return state.name + ' is ' + state.age + ' years old.';
        },
      },
    };
    
  • buttonCount.ts:以防搞混,我先將 button 的 state 提取出來

    import { Module } from 'vuex';
    
    type ICount = {
      count: number;
    };
    
    export const countModule: Module<ICount, any> = {
      state: (): ICount => ({
        count: 0,
      }),
      mutations: {
        increment(state: ICount) {
          state.count++;
        },
      },
    };
    
  • store.ts

    import { InjectionKey } from 'vue';
    import { createStore, Store, useStore as baseUseStore } from 'vuex';
    import { countModule } from './buttonCount';
    import { userModule } from './user';
    
    const modules = {
      countModule,
      userModule,
    };
    
    // 獲取所有 module 的 state
    type modulesState = {
      [key in keyof typeof modules]: Exclude<Exclude<(typeof modules)[key]['state'], undefined>, () => any>;
    };
    
    export interface State extends modulesState {}
    
    export const key: InjectionKey<Store<modulesState>> = Symbol();
    
    export const store = createStore<modulesState>({
      modules,
    });
    
    export function useStore() {
      return baseUseStore(key);
    }
    
    • Home.vue
    <template>
      <p class="text-xl">{{ incrementCount }}</p>
    </template>
    
    <script>
    import { useStore } from '../store/store';
    
    const incrementCount = computed(() => store.state.countModule.count);
    </script>
    

    這樣編譯器就不會在使用其他 module 的 state 時顯示錯誤了。

Namespace

如果今天有兩個 Module 且都擁有同個名為 increment 的方法,這時候就需要 namespace 來幫忙。

store.js

const moduleA = {
  // ...
  namespaced: true,
  mutations: {
    increment(state) {
      state.count++
    },
  },
}

const moduleB = {
  // ...
  namespaced: true,
  mutations: {
    increment(state) {
      state.count++
    },
  },
}

const store = createStore({ 
  modules: {
    a: moduleA,
    b: moduleB,
  }
})

外部使用:

commit('a/increment')
commit('b/increment')

只要有設定 namespace 的 module,不管是使用 gettersmutationsactions,都必須要在前面加上該模組的名稱。這種方式也常運用在 nested module

const store = createStore({
  modules: {
    account: {
      namespaced: true,

      // module assets
      state: () => ({ ... }), // module state is already nested and not affected by namespace option
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // nested modules
      modules: {
        // inherits the namespace from parent module
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // further nest the namespace
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

註冊動態 Module

可以在 createStore 之後使用 store.registerModule 建立 module:

import { createStore } from 'vuex'

const store = createStore({ /* options */ })

// register a module `myModule`
store.registerModule('myModule', {
  // ...
})

// register a nested module `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

結語

以上就是 Vuex 基本的使用方式。如果使用 TypeScript 比較多的問題是出現在 Modules,要能讓編譯器了解這些 Module 裡的 State,就要先費一番力了。