Pinia 從 Vuex ≤4 遷移

2023-09-28 15:22 更新

雖然 Vuex 和 Pinia store 的結構不同,但很多邏輯都可以復用。本指南的作用是幫助你完成遷移,并指出一些可能出現(xiàn)的常見問題。

重構 store 的模塊

Vuex 有一個概念,帶有多個模塊的單一 store。這些模塊可以被命名,甚至可以互相嵌套。

將這個概念過渡到 Pinia 最簡單的方法是,你以前使用的每個模塊現(xiàn)在都是一個 store。每個 store 都需要一個 id,類似于 Vuex 中的命名空間。這意味著每個 store 都有命名空間的設計。嵌套模塊也可以成為自己的 store。互相依賴的 store 可以直接導入其他 store。

你的 Vuex 模塊如何重構為 Pinia store,完全取決于你,不過這里有一個示例:

  1. ## Vuex 示例(假設是命名模塊)。
  2. src
  3. └── store
  4. ├── index.js # 初始化 Vuex,導入模塊
  5. └── modules
  6. ├── module1.js # 命名模塊 'module1'
  7. └── nested
  8. ├── index.js # 命名模塊 'nested',導入 module2 與 module3
  9. ├── module2.js # 命名模塊 'nested/module2'
  10. └── module3.js # 命名模塊 'nested/module3'
  11. ## Pinia 示例,注意 ID 與之前的命名模塊相匹配
  12. src
  13. └── stores
  14. ├── index.js # (可選) 初始化 Pinia,不必導入 store
  15. ├── module1.js # 'module1' id
  16. ├── nested-module2.js # 'nested/module2' id
  17. ├── nested-module3.js # 'nested/module3' id
  18. └── nested.js # 'nested' id

這為 store 創(chuàng)建了一個扁平的結構,但也保留了和之前等價的 id 命名方式。如果你在根 store (在 Vuex 的 store/index.js 文件中)中有一些 state/getter/action/mutation,你可以創(chuàng)建一個名為 root 的 store,來保存它們。

Pinia 的目錄一般被命名為 stores 而不是 store。這是為了強調 Pinia 可以使用多個 store,而不是 Vuex 的單一 store。

對于大型項目,你可能希望逐個模塊進行轉換,而不是一次性全部轉換。其實在遷移過程中,你可以同時使用 Pinia 和 Vuex。這樣也完全可以正常工作,這也是將 Pinia 目錄命名為 stores 的另一個原因。

轉換單個模塊

下面有一個完整的例子,介紹了將 Vuex 模塊轉換為 Pinia store 的完整過程,請看下面的逐步指南。Pinia 的例子使用了一個 option store,因為其結構與 Vuex 最為相似。

  1. // 'auth/user' 命名空間中的 Vuex 模塊
  2. import { Module } from 'vuex'
  3. import { api } from '@/api'
  4. import { RootState } from '@/types' // 如果需要使用 Vuex 的類型便需要引入
  5. interface State {
  6. firstName: string
  7. lastName: string
  8. userId: number | null
  9. }
  10. const storeModule: Module<State, RootState> = {
  11. namespaced: true,
  12. state: {
  13. firstName: '',
  14. lastName: '',
  15. userId: null
  16. },
  17. getters: {
  18. firstName: (state) => state.firstName,
  19. fullName: (state) => `${state.firstName} ${state.lastName}`,
  20. loggedIn: (state) => state.userId !== null,
  21. // 與其他模塊的一些狀態(tài)相結合
  22. fullUserDetails: (state, getters, rootState, rootGetters) => {
  23. return {
  24. ...state,
  25. fullName: getters.fullName,
  26. // 讀取另一個名為 `auth` 模塊的 state
  27. ...rootState.auth.preferences,
  28. // 讀取嵌套于 `auth` 模塊的 `email` 模塊的 getter
  29. ...rootGetters['auth/email'].details
  30. }
  31. }
  32. },
  33. actions: {
  34. async loadUser ({ state, commit }, id: number) {
  35. if (state.userId !== null) throw new Error('Already logged in')
  36. const res = await api.user.load(id)
  37. commit('updateUser', res)
  38. }
  39. },
  40. mutations: {
  41. updateUser (state, payload) {
  42. state.firstName = payload.firstName
  43. state.lastName = payload.lastName
  44. state.userId = payload.userId
  45. },
  46. clearUser (state) {
  47. state.firstName = ''
  48. state.lastName = ''
  49. state.userId = null
  50. }
  51. }
  52. }
  53. export default storeModule

  1. // Pinia Store
  2. import { defineStore } from 'pinia'
  3. import { useAuthPreferencesStore } from './auth-preferences'
  4. import { useAuthEmailStore } from './auth-email'
  5. import vuexStore from '@/store' // 逐步轉換,見 fullUserDetails
  6. interface State {
  7. firstName: string
  8. lastName: string
  9. userId: number | null
  10. }
  11. export const useAuthUserStore = defineStore('auth/user', {
  12. // 轉換為函數(shù)
  13. state: (): State => ({
  14. firstName: '',
  15. lastName: '',
  16. userId: null
  17. }),
  18. getters: {
  19. // 不在需要 firstName getter,移除
  20. fullName: (state) => `${state.firstName} ${state.lastName}`,
  21. loggedIn: (state) => state.userId !== null,
  22. // 由于使用了 `this`,必須定義一個返回類型
  23. fullUserDetails (state): FullUserDetails {
  24. // 導入其他 store
  25. const authPreferencesStore = useAuthPreferencesStore()
  26. const authEmailStore = useAuthEmailStore()
  27. return {
  28. ...state,
  29. // `this` 上的其他 getter
  30. fullName: this.fullName,
  31. ...authPreferencesStore.$state,
  32. ...authEmailStore.details
  33. }
  34. // 如果其他模塊仍在 Vuex 中,可替代為
  35. // return {
  36. // ...state,
  37. // fullName: this.fullName,
  38. // ...vuexStore.state.auth.preferences,
  39. // ...vuexStore.getters['auth/email'].details
  40. // }
  41. }
  42. },
  43. actions: {
  44. //沒有作為第一個參數(shù)的上下文,用 `this` 代替
  45. async loadUser (id: number) {
  46. if (this.userId !== null) throw new Error('Already logged in')
  47. const res = await api.user.load(id)
  48. this.updateUser(res)
  49. },
  50. // mutation 現(xiàn)在可以成為 action 了,不再用 `state` 作為第一個參數(shù),而是用 `this`。
  51. updateUser (payload) {
  52. this.firstName = payload.firstName
  53. this.lastName = payload.lastName
  54. this.userId = payload.userId
  55. },
  56. // 使用 `$reset` 可以輕松重置 state
  57. clearUser () {
  58. this.$reset()
  59. }
  60. }
  61. })

讓我們把上述內(nèi)容分解成幾個步驟:

  1. 為 store 添加一個必要的 id,你可以讓它與之前的命名保持相同。
  2. 如果 state 不是一個函數(shù)的話 將它轉換為一個函數(shù)。
  3. 轉換 getters
    1. 刪除任何返回同名 state 的 getters (例如: firstName: (state) => state.firstName),這些都不是必需的,因為你可以直接從 store 實例中訪問任何狀態(tài)。
    2. 如果你需要訪問其他的 getter,可通過 this 訪問它們,而不是第二個參數(shù)。記住,如果你使用 this,而且你不得不使用一個普通函數(shù),而不是一個箭頭函數(shù),那么由于 TS 的限制,你需要指定一個返回類型,更多細節(jié)請閱讀這篇文檔
    3. 如果使用 rootStaterootGetters 參數(shù),可以直接導入其他 store 來替代它們,或者如果它們?nèi)匀淮嬖谟?Vuex ,則直接從 Vuex 中訪問它們。
  4. 轉換 actions
    1. 從每個 action 中刪除第一個 context 參數(shù)。所有的東西都應該直接從 this 中訪問。
    2. 如果使用其他 store,要么直接導入,要么與 getters 一樣,在 Vuex 上訪問。
  5. 轉換 mutations
    1. Mutation 已經(jīng)被棄用了。它們可以被轉換為 action,或者你可以在你的組件中直接賦值給 store (例如:userStore.firstName = 'First')
    2. 如果你想將它轉換為 action,刪除第一個 state 參數(shù),用 this 代替任何賦值操作中的 state
    3. 一個常見的 mutation 是將 state 重置為初始 state。而這就是 store 的 $reset 方法的內(nèi)置功能。注意,這個功能只存在于 option stores。

正如你所看到的,你的大部分代碼都可以被重復使用。如果有什么遺漏,類型安全也應該可以幫助你確定需要修改的地方。

組件內(nèi)的使用 %{#usage-inside-components}%

現(xiàn)在你的 Vuex 模塊已經(jīng)被轉換為 Pinia store,但其他使用該模塊的組件或文件也需要更新。

如果你以前使用的是 Vuex 的 map 輔助函數(shù),可以看看不使用 setup() 的用法指南,因為這些輔助函數(shù)大多都是可以復用的。

如果你以前使用的是 useStore,那么就直接導入新 store 并訪問其上的 state。比如說:

  1. // Vuex
  2. import { defineComponent, computed } from 'vue'
  3. import { useStore } from 'vuex'
  4. export default defineComponent({
  5. setup () {
  6. const store = useStore()
  7. const firstName = computed(() => store.state.auth.user.firstName)
  8. const fullName = computed(() => store.getters['auth/user/fullName'])
  9. return {
  10. firstName,
  11. fullName
  12. }
  13. }
  14. })

  1. // Pinia
  2. import { defineComponent, computed } from 'vue'
  3. import { useAuthUserStore } from '@/stores/auth-user'
  4. export default defineComponent({
  5. setup () {
  6. const authUserStore = useAuthUserStore()
  7. const firstName = computed(() => authUserStore.firstName)
  8. const fullName = computed(() => authUserStore.fullName)
  9. return {
  10. // 你也可以在你的組件中通過返回 store 來訪問整個 store
  11. authUserStore,
  12. firstName,
  13. fullName
  14. }
  15. }
  16. })

組件外的使用 %{#usage-outside-components}%

只要你注意不在函數(shù)外使用 store,單獨更新組件外的用法應該很簡單。下面是一個在 Vue Router 導航守衛(wèi)中使用 store 的例子:

  1. // Vuex
  2. import vuexStore from '@/store'
  3. router.beforeEach((to, from, next) => {
  4. if (vuexStore.getters['auth/user/loggedIn']) next()
  5. else next('/login')
  6. })

  1. // Pinia
  2. import { useAuthUserStore } from '@/stores/auth-user'
  3. router.beforeEach((to, from, next) => {
  4. // 必須在函數(shù)內(nèi)部使用
  5. const authUserStore = useAuthUserStore()
  6. if (authUserStore.loggedIn) next()
  7. else next('/login')
  8. })

Vuex 高級用法

如果你的 Vuex store 使用了它所提供的一些更高級的功能,也有一些關于如何在 Pinia 中實現(xiàn)同樣效果的指導。其中一些要點已經(jīng)包含在這個對比總結里了。

動態(tài)模塊

在 Pinia 中不需要動態(tài)注冊模塊。store 設計之初就是動態(tài)的,只有在需要時才會被注冊。如果一個 store 從未被使用過,它就永遠不會被 “注冊”。

插件

如果你使用的是一個公共的 Vuex 插件,那么請檢查是否有一個 Pinia 版的替代品。如果沒有,你就需要自己寫一個,或者評估一下是否還有必要使用這個插件。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號