# 从 Vuex≤4 迁移

尽管VuexPiniastores结构不同,但许多逻辑可以重用。本指南旨在帮助您完成整个过程,并指出可能出现的一些常见问题。

# 准备

首先,按照入门指南 (opens new window)安装 Pinia

# 将模块重组为 Store

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

将这个概念转变为使用Pinia,最简单方法是,您以前是使用不同的模块,现在是使用单一的 store。每个store都必需一个id,类似于Vuex中的命名空间。这意味着每个store都是按设计命名的。嵌套模块也可以各自成为自己的store。相互依赖的store将被简便地导入到其他store

如何选择将Vuex模块重组到Piniastore完全取决于您,但这里还有一些建议:

# 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 # 'nested/module3' id
    ├── nested-module3.js # 'nested/module2' id
    └── nested.js         # 'nested' id

这为stores创建了一个扁平的结构,但也保留了和之前使用id等价的命名空间。如果你在store的根目录(在Vuexstore/index.js文件中)中有一些state/getters/actions/mutations,你可能希望创建另一个名为rootstore,并且它包含所有这些信息。

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

对于大型项目,您可能希望逐个模块进行转换,而不是一次性转换所有的内容。实际上,您可以在迁移过程中混合使用PiniaVuex,这种方式也是可行的,这也是命名Pinia目录为stores的另一个原因。

# 单个模块的转换

这是个将Vuex模块转换为Pinia``store前后的完整示例,请参阅下面的分步指南。Pinia示例使用选项store,因为它的结构与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('auth/user', {
  // 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. store添加一个必需的id,您可能希望保持与之前的名称空间相同

  2. 如果state还不是一个函数,则需将它转换成函数

  3. 转换getters

    1. 删除任何以相同名称返回状态的getters(如firstName:(state) => state.firstName)这些不是必需的,因为您可以直接从store实例访问任何状态
    2. 如果您需要访问其它getters,可以使用this代替,而不是使用第二个参数。请记住,如果您正在使用this,那么您将不得不使用常规函数而不是箭头函数。另外请注意,由于TS的限制,您需要返回指定的类型,请参阅此处 (opens new window)了解更多详细信息
    3. 如果使用rootStaterootGetters参数,则通过直接导入其他store来替换它们,或者如果它们仍然存在于Vuex中,则直接从Vuex访问它们
  4. 转换actions

    1. 从所有action中删除第一个context参数,所有东西都应该通过this访问
    2. 如果使用其它的stores,要么直接导入它们,要么在Vuex上访问它们,这与getters一样
  5. 转换mutations

    1. mutations不再存在。这些可以转换为actions,或者您可以直接分配给组件中的store(例如:userStore.firstName = 'First')
    2. 如果转换为actions,则需删除第一个参数state,并用this代替所有工作
    3. 一个常见的mutation是将状态重置回初始状态。这是store$reset方法的内置功能。请注意,此功能仅适用于option stores

如您所见,您的大部分代码都可以重用。如果遗漏了什么,类型安全还会帮助您确定需要更改什么。

# 组件内使用

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

如果您之前使用过 Vuex 的辅助函数,那么值得看不使用setup()的用法 (opens new window),因为这些辅助函数大多可以重用。

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

// 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/firstName'])

    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
    }
  }
})

# 组件外使用

只要注意不在函数外部使用store,更新组件之外的用法都是很简单的。这是在Vue Router导航守卫中使用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) => {
  // Must be used within the function!
  const authUserStore = useAuthUserStore()
  if (authUserStore.loggedIn) next()
  else next('/login')
})

想了解更多细节可以点击 这里 (opens new window)

# Vuex 进阶使用

如果您Vuexstore使用了它提供的一些更高级的功能,下面是一些关于如何在Pinia完成相同功能的指南。其中一些要点已收录在

比较摘要 (opens new window)中。

# 动态模块

无需在Pinia中动态注册模块。stores在设计上是动态的,仅在需要时才注册。如果store从没被使用,则永远不会“注册”。

# 热模块更新

HMR也受支持,但需要替换,请参阅HMR指南 (opens new window)

# 插件

如果您使用开源的Vuex插件,那么检查是否有Pinia的替代品。如果没有,您将需要自己编写或评估该插件是否仍然必要。

如果您已经编写了自己的插件,那么很可能需要对其更新,以便能与Pinia一起使用。请参阅

插件指南 (opens new window)