迁移 Vuex Migrating from Vuex ≤4

虽然 Vuex 和 Pinia store 的结构不同,但是很多逻辑是可以复用的。 本指南旨在帮助您完成整个过程并指出可能出现的一些常见问题。

Preparation

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

将模块重组为商店

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

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

如何选择将 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

这为商店创建了一个平面结构,但也保留了以前的命名空间与等效的 ids。 如果你在 store 的根目录中有一些 state/getters/actions/mutations(在 Vuex 的 store/index.js 文件中),你可能希望创建另一个名为 root 的 store 来保存所有这些信息。

Pinia 的目录通常称为 stores 而不是 store。 这是为了强调 Pinia 使用多个 store,而不是 Vuex 中的单个 store。

对于大型项目,您可能希望逐个模块进行转换,而不是一次转换所有内容。 您实际上可以在迁移期间将 Pinia 和 Vuex 混合在一起,因此这种方法也可以工作,这也是将 Pinia 目录命名为“stores”的另一个原因。

转换单个模块

这是将 Vuex 模块转换为 Pinia 商店之前和之后的完整示例,请参阅下面的分步指南。 Pinia 示例使用选项存储,因为其结构与 Vuex 最相似:

ts
// '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,
    // combine with some state from other modules
    fullUserDetails: (state, getters, rootState, rootGetters) => {
      return {
        ...state,
        fullName: getters.fullName,
        // 从另一个名为“auth”的模块中读取状态
        ...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
ts
// Pinia Store
import { defineStore } from 'pinia'
import { useAuthPreferencesStore } from './auth-preferences'
import { useAuthEmailStore } from './auth-email'
import vuexStore from '@/store' // 对于逐渐转换,请参阅 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,
    // 由于使用 `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
      }

      // 如果其他模块仍在 Vuex 中,则可以选择
      // return {
      //   ...state,
      //   fullName: this.fullName,
      //   ...vuexStore.state.auth.preferences,
      //   ...vuexStore.getters['auth/email'].details
      // }
    }
  },
  actions: {
    // 没有上下文作为第一个参数,使用 `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)
    },
    // mutations现在可以变成actions,而不是 `state` 作为第一个参数使用 `this`
    updateUser(payload) {
      this.firstName = payload.firstName
      this.lastName = payload.lastName
      this.userId = payload.userId
    },
    // 使用 `$reset` 轻松重置状态
    clearUser() {
      this.$reset()
    }
  }
})

让我们将上述内容分解为几个步骤:

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

如您所见,您的大部分代码都可以重用。 如果遗漏任何内容,类型安全还应该帮助您确定需要更改的内容。

组件内部使用

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

如果您之前使用过 Vuex 的 map 助手,那么值得查看 Usage without setup() 指南,因为这些助手中的大多数都可以重用。

如果您使用的是 useStore,则直接导入新商店并访问其上的状态。 例如:

ts
// Vuex
import { computed, defineComponent } 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 { computed, defineComponent } 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 导航守卫中使用 store 的示例:

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 一起使用。 请参阅 插件指南