从 Vuex ≤4 迁移
尽管 Vuex 和 Pinia 商店的结构不同,但许多逻辑可以重复使用。本指南旨在帮助您完成此过程并指出可能出现的一些常见问题。
准备
首先,按照 入门指南 安装 Pinia。
将模块重构为商店
Vuex 有一个包含多个模块的单个商店的概念。这些模块可以选择命名空间,甚至可以相互嵌套。
将该概念转换为 Pinia 的最简单方法是,您之前使用的每个模块现在都是一个商店。每个商店都需要一个id
,它类似于 Vuex 中的命名空间。这意味着每个商店在设计上都是命名空间的。嵌套模块也可以各自成为自己的商店。相互依赖的商店只需导入另一个商店即可。
您选择如何将 Vuex 模块重构为 Pinia 商店完全取决于您,但以下是一个建议
# Vuex example (assuming namespaced modules)
src
└── store
├── index.js # Initializes Vuex, imports modules
└── modules
├── module1.js # 'module1' namespace
└── nested
├── index.js # 'nested' namespace, imports module2 & module3
├── module2.js # 'nested/module2' namespace
└── module3.js # 'nested/module3' namespace
# Pinia equivalent, note ids match previous namespaces
src
└── stores
├── index.js # (Optional) Initializes Pinia, does not import stores
├── module1.js # 'module1' id
├── nested-module2.js # 'nestedModule2' id
├── nested-module3.js # 'nestedModule3' id
└── nested.js # 'nested' id
这为商店创建了一个扁平结构,但也使用等效的id
保留了先前的命名空间。如果您在商店的根目录(在 Vuex 的store/index.js
文件中)有一些状态/获取器/操作/变异,您可能希望创建一个名为root
的另一个商店,其中包含所有这些信息。
Pinia 的目录通常称为stores
而不是store
。这是为了强调 Pinia 使用多个商店,而不是 Vuex 中的单个商店。
对于大型项目,您可能希望逐个模块地进行此转换,而不是一次性转换所有内容。您实际上可以在迁移期间混合使用 Pinia 和 Vuex,因此这种方法也可以工作,这也是将 Pinia 目录命名为stores
而不是store
的另一个原因。
转换单个模块
以下是一个将 Vuex 模块转换为 Pinia 商店的完整示例,请参见下面的分步指南。Pinia 示例使用选项商店,因为其结构与 Vuex 最相似
// Vuex module in the 'auth/user' namespace
import { Module } from 'vuex'
import { api } from '@/api'
import { RootState } from '@/types' // if using a Vuex type definition
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,
// combine with some state from other modules
fullUserDetails: (state, getters, rootState, rootGetters) => {
return {
...state,
fullName: getters.fullName,
// read the state from another module named `auth`
...rootState.auth.preferences,
// read a getter from a namespaced module called `email` nested under `auth`
...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' // for gradual conversion, see fullUserDetails
interface State {
firstName: string
lastName: string
userId: number | null
}
export const useAuthUserStore = defineStore('authUser', {
// convert to a function
state: (): State => ({
firstName: '',
lastName: '',
userId: null
}),
getters: {
// firstName getter removed, no longer needed
fullName: (state) => `${state.firstName} ${state.lastName}`,
loggedIn: (state) => state.userId !== null,
// must define return type because of using `this`
fullUserDetails (state): FullUserDetails {
// import from other stores
const authPreferencesStore = useAuthPreferencesStore()
const authEmailStore = useAuthEmailStore()
return {
...state,
// other getters now on `this`
fullName: this.fullName,
...authPreferencesStore.$state,
...authEmailStore.details
}
// alternative if other modules are still in Vuex
// return {
// ...state,
// fullName: this.fullName,
// ...vuexStore.state.auth.preferences,
// ...vuexStore.getters['auth/email'].details
// }
}
},
actions: {
// no context as first argument, use `this` instead
async loadUser (id: number) {
if (this.userId !== null) throw new Error('Already logged in')
const res = await api.user.load(id)
this.updateUser(res)
},
// mutations can now become actions, instead of `state` as first argument use `this`
updateUser (payload) {
this.firstName = payload.firstName
this.lastName = payload.lastName
this.userId = payload.userId
},
// easily reset state using `$reset`
clearUser () {
this.$reset()
}
}
})
让我们将以上内容分解为步骤
- 为商店添加一个必需的
id
,您可能希望将其保留为与之前的命名空间相同。还建议确保id
为驼峰式,因为这使得它更容易与mapStores()
一起使用。 - 如果
state
不是函数,则将其转换为函数 - 转换
getters
- 删除任何以相同名称返回状态的获取器(例如
firstName: (state) => state.firstName
),因为您可以直接从商店实例访问任何状态,所以这些不是必需的 - 如果您需要访问其他获取器,它们位于
this
上,而不是使用第二个参数。请记住,如果您使用的是this
,那么您将不得不使用普通函数而不是箭头函数。还要注意,您将需要指定一个返回类型,因为 TS 的限制,请参阅此处了解更多详细信息 - 如果使用
rootState
或rootGetters
参数,请通过直接导入另一个商店来替换它们,或者如果它们仍然存在于 Vuex 中,则直接从 Vuex 访问它们
- 删除任何以相同名称返回状态的获取器(例如
- 转换
actions
- 从每个操作中删除第一个
context
参数。所有内容都应该从this
中访问,而不是使用第二个参数 - 如果使用其他商店,请直接导入它们或从 Vuex 访问它们,与获取器相同
- 从每个操作中删除第一个
- 转换
mutations
- 变异不再存在。这些可以转换为
actions
,或者您也可以直接在组件中分配给商店(例如userStore.firstName = 'First'
) - 如果转换为操作,请删除第一个
state
参数,并将任何分配替换为this
- 一个常见的变异是将状态重置为其初始状态。这是商店的
$reset
方法的内置功能。请注意,此功能仅适用于选项商店。
- 变异不再存在。这些可以转换为
如您所见,大部分代码都可以重复使用。类型安全还应该帮助您识别需要更改的内容(如果有)。
在组件内部使用
现在您的 Vuex 模块已转换为 Pinia 商店,任何使用该模块的组件或其他文件都需要更新。
如果您之前使用过 Vuex 的map
帮助程序,那么值得查看在没有 setup() 的情况下使用指南,因为大多数这些帮助程序都可以重复使用。
如果您使用的是useStore
,那么改为直接导入新的商店并在其上访问状态。例如
// 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 {
// you can also access the whole store in your component by returning it
authUserStore,
firstName,
fullName
}
}
})
在组件外部使用
更新组件外部的使用应该很简单,只要您小心不要在函数外部使用商店。以下是在 Vue Router 导航守卫中使用商店的示例
// 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) => {
// Must be used within the function!
const authUserStore = useAuthUserStore()
if (authUserStore.loggedIn) next()
else next('/login')
})
更多详细信息可以在此处找到。
高级 Vuex 使用
如果您使用 Vuex 商店使用它提供的一些更高级的功能,以下是如何在 Pinia 中实现相同功能的指导。这些要点中的一些已经在此比较摘要中介绍过。
动态模块
在 Pinia 中不需要动态注册模块。商店在设计上是动态的,只有在需要时才会注册。如果从未使用过商店,它将永远不会被“注册”。
热模块替换
HMR 也受支持,但需要替换,请参阅HMR 指南。
插件
如果您使用的是公共 Vuex 插件,请检查是否有 Pinia 替代方案。如果没有,您将需要编写自己的插件或评估该插件是否仍然必要。
如果您自己编写了一个插件,那么它很可能可以更新以与 Pinia 一起使用。请参阅插件指南。