跳至内容

从 Vuex ≤4 迁移

尽管 Vuex 和 Pinia 商店的结构不同,但许多逻辑可以重复使用。本指南旨在帮助您完成此过程并指出可能出现的一些常见问题。

准备

首先,按照 入门指南 安装 Pinia。

将模块重构为商店

Vuex 有一个包含多个模块的单个商店的概念。这些模块可以选择命名空间,甚至可以相互嵌套。

将该概念转换为 Pinia 的最简单方法是,您之前使用的每个模块现在都是一个商店。每个商店都需要一个id,它类似于 Vuex 中的命名空间。这意味着每个商店在设计上都是命名空间的。嵌套模块也可以各自成为自己的商店。相互依赖的商店只需导入另一个商店即可。

您选择如何将 Vuex 模块重构为 Pinia 商店完全取决于您,但以下是一个建议

bash
# 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 最相似

ts
// 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
ts
// 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()
    }
  }
})

让我们将以上内容分解为步骤

  1. 为商店添加一个必需的id,您可能希望将其保留为与之前的命名空间相同。还建议确保id驼峰式,因为这使得它更容易与mapStores()一起使用。
  2. 如果state不是函数,则将其转换为函数
  3. 转换getters
    1. 删除任何以相同名称返回状态的获取器(例如firstName: (state) => state.firstName),因为您可以直接从商店实例访问任何状态,所以这些不是必需的
    2. 如果您需要访问其他获取器,它们位于this上,而不是使用第二个参数。请记住,如果您使用的是this,那么您将不得不使用普通函数而不是箭头函数。还要注意,您将需要指定一个返回类型,因为 TS 的限制,请参阅此处了解更多详细信息
    3. 如果使用rootStaterootGetters参数,请通过直接导入另一个商店来替换它们,或者如果它们仍然存在于 Vuex 中,则直接从 Vuex 访问它们
  4. 转换actions
    1. 从每个操作中删除第一个context参数。所有内容都应该从this中访问,而不是使用第二个参数
    2. 如果使用其他商店,请直接导入它们或从 Vuex 访问它们,与获取器相同
  5. 转换mutations
    1. 变异不再存在。这些可以转换为actions,或者您也可以直接在组件中分配给商店(例如userStore.firstName = 'First'
    2. 如果转换为操作,请删除第一个state参数,并将任何分配替换为this
    3. 一个常见的变异是将状态重置为其初始状态。这是商店的$reset方法的内置功能。请注意,此功能仅适用于选项商店。

如您所见,大部分代码都可以重复使用。类型安全还应该帮助您识别需要更改的内容(如果有)。

在组件内部使用

现在您的 Vuex 模块已转换为 Pinia 商店,任何使用该模块的组件或其他文件都需要更新。

如果您之前使用过 Vuex 的map帮助程序,那么值得查看在没有 setup() 的情况下使用指南,因为大多数这些帮助程序都可以重复使用。

如果您使用的是useStore,那么改为直接导入新的商店并在其上访问状态。例如

ts
// 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
    }
  }
})
ts
// 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 导航守卫中使用商店的示例

ts
// Vuex
import vuexStore from '@/store'

router.beforeEach((to, from, next) => {
  if (vuexStore.getters['auth/user/loggedIn']) next()
  else next('/login')
})
ts
// 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 一起使用。请参阅插件指南