Pinia 從 Vuex ≤4 遷移

2023-09-28 15:22 更新

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

重構(gòu) store 的模塊

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

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

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

## Vuex 示例(假設(shè)是命名模塊)。
src
└── store
    ├── index.js           # 初始化 Vuex,導(dǎo)入模塊
    └── modules
        ├── module1.js     # 命名模塊 'module1'
        └── nested
            ├── index.js   # 命名模塊 'nested',導(dǎo)入 module2 與 module3
            ├── module2.js # 命名模塊 'nested/module2'
            └── module3.js # 命名模塊 'nested/module3'


## Pinia 示例,注意 ID 與之前的命名模塊相匹配
src
└── stores
    ├── index.js          # (可選) 初始化 Pinia,不必導(dǎo)入 store
    ├── module1.js        # 'module1' id
    ├── nested-module2.js # 'nested/module2' id
    ├── nested-module3.js # 'nested/module3' id
    └── nested.js         # 'nested' id

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

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

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

轉(zhuǎn)換單個模塊

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

// 'auth/user' 命名空間中的 Vuex 模塊
import { Module } from 'vuex'
import { api } from '@/api'
import { RootState } from '@/types' // 如果需要使用 Vuex 的類型便需要引入


interface State {
  firstName: string
  lastName: string
  userId: number | null
}


const storeModule: Module<State, RootState> = {
  namespaced: true,
  state: {
    firstName: '',
    lastName: '',
    userId: null
  },
  getters: {
    firstName: (state) => state.firstName,
    fullName: (state) => `${state.firstName} ${state.lastName}`,
    loggedIn: (state) => state.userId !== null,
    // 與其他模塊的一些狀態(tài)相結(jié)合
    fullUserDetails: (state, getters, rootState, rootGetters) => {
      return {
        ...state,
        fullName: getters.fullName,
        // 讀取另一個名為 `auth` 模塊的 state
        ...rootState.auth.preferences,
        // 讀取嵌套于 `auth` 模塊的 `email` 模塊的 getter
        ...rootGetters['auth/email'].details
      }
    }
  },
  actions: {
    async loadUser ({ state, commit }, id: number) {
      if (state.userId !== null) throw new Error('Already logged in')
      const res = await api.user.load(id)
      commit('updateUser', res)
    }
  },
  mutations: {
    updateUser (state, payload) {
      state.firstName = payload.firstName
      state.lastName = payload.lastName
      state.userId = payload.userId
    },
    clearUser (state) {
      state.firstName = ''
      state.lastName = ''
      state.userId = null
    }
  }
}


export default storeModule

// Pinia Store
import { defineStore } from 'pinia'
import { useAuthPreferencesStore } from './auth-preferences'
import { useAuthEmailStore } from './auth-email'
import vuexStore from '@/store' // 逐步轉(zhuǎn)換,見 fullUserDetails


interface State {
  firstName: string
  lastName: string
  userId: number | null
}


export const useAuthUserStore = defineStore('auth/user', {
  // 轉(zhuǎn)換為函數(shù)
  state: (): State => ({
    firstName: '',
    lastName: '',
    userId: null
  }),
  getters: {
    // 不在需要 firstName getter,移除
    fullName: (state) => `${state.firstName} ${state.lastName}`,
    loggedIn: (state) => state.userId !== null,
    // 由于使用了 `this`,必須定義一個返回類型
    fullUserDetails (state): FullUserDetails {
      // 導(dǎo)入其他 store
      const authPreferencesStore = useAuthPreferencesStore()
      const authEmailStore = useAuthEmailStore()
      return {
        ...state,
        // `this` 上的其他 getter
        fullName: this.fullName,
        ...authPreferencesStore.$state,
        ...authEmailStore.details
      }


      // 如果其他模塊仍在 Vuex 中,可替代為
      // return {
      //   ...state,
      //   fullName: this.fullName,
      //   ...vuexStore.state.auth.preferences,
      //   ...vuexStore.getters['auth/email'].details
      // }
    }
  },
  actions: {
    //沒有作為第一個參數(shù)的上下文,用 `this` 代替
    async loadUser (id: number) {
      if (this.userId !== null) throw new Error('Already logged in')
      const res = await api.user.load(id)
      this.updateUser(res)
    },
    // mutation 現(xiàn)在可以成為 action 了,不再用 `state` 作為第一個參數(shù),而是用 `this`。
    updateUser (payload) {
      this.firstName = payload.firstName
      this.lastName = payload.lastName
      this.userId = payload.userId
    },
    // 使用 `$reset` 可以輕松重置 state
    clearUser () {
      this.$reset()
    }
  }
})

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

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

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

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

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

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

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

// Vuex
import { defineComponent, computed } from 'vue'
import { useStore } from 'vuex'


export default defineComponent({
  setup () {
    const store = useStore()


    const firstName = computed(() => store.state.auth.user.firstName)
    const fullName = computed(() => store.getters['auth/user/fullName'])


    return {
      firstName,
      fullName
    }
  }
})

// Pinia
import { defineComponent, computed } from 'vue'
import { useAuthUserStore } from '@/stores/auth-user'


export default defineComponent({
  setup () {
    const authUserStore = useAuthUserStore()


    const firstName = computed(() => authUserStore.firstName)
    const fullName = computed(() => authUserStore.fullName)


    return {
      // 你也可以在你的組件中通過返回 store 來訪問整個 store
      authUserStore,
      firstName,
      fullName
    }
  }
})

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

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

// Vuex
import vuexStore from '@/store'


router.beforeEach((to, from, next) => {
  if (vuexStore.getters['auth/user/loggedIn']) next()
  else next('/login')
})

// Pinia
import { useAuthUserStore } from '@/stores/auth-user'


router.beforeEach((to, from, next) => {
  // 必須在函數(shù)內(nèi)部使用
  const authUserStore = useAuthUserStore()
  if (authUserStore.loggedIn) next()
  else next('/login')
})

Vuex 高級用法

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

動態(tài)模塊

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

插件

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號