雖然 Vuex 和 Pinia store 的結(jié)構(gòu)不同,但很多邏輯都可以復(fù)用。本指南的作用是幫助你完成遷移,并指出一些可能出現(xiàn)的常見問題。
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
的另一個原因。
下面有一個完整的例子,介紹了將 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)容分解成幾個步驟:
id
,你可以讓它與之前的命名保持相同。state
不是一個函數(shù)的話 將它轉(zhuǎn)換為一個函數(shù)。getters
firstName: (state) => state.firstName
),這些都不是必需的,因為你可以直接從 store 實例中訪問任何狀態(tài)。this
訪問它們,而不是第二個參數(shù)。記住,如果你使用 this
,而且你不得不使用一個普通函數(shù),而不是一個箭頭函數(shù),那么由于 TS 的限制,你需要指定一個返回類型,更多細(xì)節(jié)請閱讀這篇文檔rootState
或 rootGetters
參數(shù),可以直接導(dǎo)入其他 store 來替代它們,或者如果它們?nèi)匀淮嬖谟?Vuex ,則直接從 Vuex 中訪問它們。actions
context
參數(shù)。所有的東西都應(yīng)該直接從 this
中訪問。mutations
action
,或者你可以在你的組件中直接賦值給 store (例如:userStore.firstName = 'First'
)state
參數(shù),用 this
代替任何賦值操作中的 state
。$reset
方法的內(nèi)置功能。注意,這個功能只存在于 option stores。正如你所看到的,你的大部分代碼都可以被重復(fù)使用。如果有什么遺漏,類型安全也應(yīng)該可以幫助你確定需要修改的地方。
現(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
}
}
})
只要你注意不在函數(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 store 使用了它所提供的一些更高級的功能,也有一些關(guān)于如何在 Pinia 中實現(xiàn)同樣效果的指導(dǎo)。其中一些要點已經(jīng)包含在這個對比總結(jié)里了。
在 Pinia 中不需要動態(tài)注冊模塊。store 設(shè)計之初就是動態(tài)的,只有在需要時才會被注冊。如果一個 store 從未被使用過,它就永遠(yuǎn)不會被 “注冊”。
如果你使用的是一個公共的 Vuex 插件,那么請檢查是否有一個 Pinia 版的替代品。如果沒有,你就需要自己寫一個,或者評估一下是否還有必要使用這個插件。
更多建議: