从Vuex到Pinia

本文由奇舞团前端研发工程师撰写,介绍了从Vuex过渡到Pinia的过程。Pinia作为Vuex的轻量级替代,简化了状态管理,去除了Vuex中的Mutation和Action区分,提供了更好的类型安全性和模块化设计。Pinia的使用包括通过函数定义store,使用Getter和Action进行状态获取与业务逻辑处理。相比于Vuex,Pinia更易于理解和使用,尤其在与Vue3的组合式API配合时,具有更好的类型推断支持。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文作者为奇舞团前端研发工程师

从Vuex到Pinia

一. 概述

在开发Vue项目时,我们一般使用Vuex来进行状态管理,但是在使用Vuex时始终伴随着一些痛点。比如:需要使用Provide/Inject来定义类型化的InjectionKey以便支持TypeScript,模块的结构嵌套、命名空间以及对新手比较难理解的流程规范等。Pinia的出现很好的解决了这些痛点。本质上Pinia也是Vuex团队核心成员开发的,在Vuex的基础上提出了一些改进。与Vuex相比,Pinia去除了Vuex中对于同步函数Mutations和异步函数Actions的区分。并且实现了在Vuex5中想要的大部分内容。

二.使用

在介绍Pinia之前我们先来回顾一下Vuex的使用流程

1.Vuex

Vuex是一个专为Vue.js应用程序开发的状态管理库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它主要用来解决多个组件状态共享的问题。

a67acfe47288b2185586f4cdad8f7bf5.png

主流程: 在Store中创建要共享的状态state,修改state流程:Vue Compontents dispatch Actions(在Actions中定义异步函数),Action commit Mutations,在Mutations中我们定义直接修改state的纯函数,state修改促使Vue compontents 做响应式变化。

(1) 核心概念
  • State: 就是组件所依赖的状态对象。我们可以在里面定义我们组件所依赖的数据。可以在Vue组件中通过this.$store.state.xxx获取state里面的数据.

  • Getter:从 store 中的 state 派生出的一些状态,可以把他理解为是store的计算属性.

  • Mutation:更改 store 中状态的唯一方法是提交 mutation,我们通过在mutation中定义方法来改变state里面的数据.

在Vue组件中,我们通过store.commit('方法名'),来提交mutation需要注意的是,Mutation 必须是同步函数

  • Action: action类似于 mutation,不同在于:

Action 提交的是 mutation,而不是直接变更状态.

Action 可以包含任意异步操作.

  • Module: 当我们的应用较大时,为了避免所有状态会集中到一个比较大的对象中,Vuex允许我们将 store 分割成模块(module),你可以把它理解为Redux中的combineReducer的作用.

(2) 在组合式API中对TypeScript的支持

在使用组合式API编写Vue组件时候,我们希望使用useStore返回类型化的store,流程大概如下:

  1. 定义类型化的 InjectionKey

  2. store 安装到 Vue 应用时提供类型化的 InjectionKey

  3. 将类型化的 InjectionKey传给useStore方法并简化useStore用法

// store.ts
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import { InjectionKey } from 'vue'

export interface IState {
  count: number
}

// 1.定义 injection key
export const key: InjectionKey<Store<IState>> = Symbol()

export default createStore<IState>({
  state: {
    count: 0
  },
  mutations: {
    addCount (state:IState) {
      state.count++
    }
  },
  actions: {
    asyncAddCount ({ commit, state }) {
      console.log('state.count=====>', state.count++)

      setTimeout(() => {
        commit('addCount')
      }, 2000)
    }
  },
 })

// 定义自己的 `useStore` 组合式函数
export function useStore () {
  return baseUseStore(key)
}

main.ts

import { createApp } from 'vue'
import { store, key } from './store'

const app = createApp({ ... })

// 传入 injection key
app.use(store, key)

app.mount('#app')

组件中使用

<script lang="ts">
import { defineComponent, toRefs } from 'vue'
import { useStore } from '../store'

export default defineComponent({
  setup () {
    const store = useStore()
    const clickHandel = () => {
      console.log('====>')
      store.commit('addCount')
    }

    const clickAsyncHandel = () => {
      console.log('====>')
      store.dispatch('asyncAddCount')
    }
    return {
      ...toRefs(store.state),
      clickHandel,
      clickAsyncHandel
    }
  }
})
</script>

Pinia 的使用

a4c497ee7c98932f5f5b8f9b24d5e266.png

基本特点

Pinia同样是一个Vue的状态管理工具,在Vuex的基础上提出了一些改进。与vuex相比,Pinia 最大的特点是:简便。

  • 它没有mutation,他只有stategettersaction,在action中支持同步与异步方法来修改state数据

  • 类型安全,与 TypeScript 一起使用时具有可靠的类型推断支持

  • 模块化设计,通过构建多个存储模块,可以让程序自动拆分它们。

  • 非常轻巧,只有大约 1kb 的大小。

  • 不再有 modules 的嵌套结构,没有命名空间模块

  • Pinia 支持扩展,可以非常方便地通过本地存储,事物等进行扩展。

  • 支持服务器端渲染

安装与使用

安装
yarn add pinia
# 或者使用 npm
npm install pinia
核心概念:
store: 使用defineStore()函数定义一个store,第一个参数是应用程序中store的唯一id. 里面包含stategettersactions, 与Vuex相比没有了Mutations.
export const useStore = defineStore('main', {
 state: () => {
    return {
      name: 'ming',
      doubleCount: 2
    }
  },
  getters: {
  },
  actions: {
  }
})

注意:store 是一个用reactive 包裹的对象,这意味着不需要在getter 之后写.value,但是,就像setup 中的props 一样,我们不能对其进行解构.

export default defineComponent({
  setup() {
    const store = useStore()
    // ❌ 这不起作用,因为它会破坏响应式
    // 这和从 props 解构是一样的
    const { name, doubleCount } = store

    return {
      // 一直会是 "ming"
      name,
      // 一直会是 2
      doubleCount,
      // 这将是响应式的
      doubleValue: computed(() => store.doubleCount),
      }
  },
})

当然你可以使用computed来响应式的获取state的值(这与Vuex中需要创建computed引用以保留其响应性类似),但是我们通常的做法是使用storeToRefs响应式解构Store.

const store = useStore()
// 正确的响应式解构
const { name, doubleCount } = storeToRefs(store)
State: 在Pinia中,状态被定义为返回初始状态的函数.
import { defineStore } from 'pinia'

const useStore = defineStore('main', {
  // 推荐使用 完整类型推断的箭头函数
  state: () => {
    return {
      // 所有这些属性都将自动推断其类型
      counter: 0,
      name: 'Eduardo'
    }
  },
})
组件中state的获取与修改:

Vuex中我们修改state的值必须在mutation中定义方法进行修改,而在pinia中我们有多中修改state的方式.

  • 基本方法:

const store = useStore()
store.counter++
  • 重置状态:

const store = useStore()
store.$reset()
  • 使用$patch修改state [1] 使用部分state对象进行修改

const mainStore = useMainStore()
mainStore.$patch({
   name: '',
   counter: mainStore.counter++
 })

[2] $patch方法也可以接受一个函数来批量修改集合内部分对象的值

cartStore.$patch((state) => {
  state.counter++
  state.name = 'test'
})
  • 替换state 可以通过将其 $state 属性设置为新对象,来替换Store的整个状态:

mainStore.$state = { name: '', counter: 0 }
  • 访问其他模块的state

    • Vuex中我们要访问其他带命名空间的模块的state我们需要使用rootState

addAsyncTabs ({ state, commit, rootState, rootGetters }:ActionContext<TabsState, RootState>, tab:ITab): void {
      /// 通过rootState 访问main的数据
      console.log('rootState.main.count=======', rootState.main.count)
      if (state.tabLists.some(item => item.id === tab.id)) { return }
      setTimeout(() => {
        state.tabLists.push(tab)
      }, 1000)
    },
  • Pinia 中访问其他storestate

import { useInputStore } from './inputStore'
    
    export const useListStore = defineStore('listStore', {
      state: () => {
        return {
          itemList: [] as IItemDate[],
          counter: 0
        }
      },
      getters: {
      },
      actions: {
        addList (item: IItemDate) {
          this.itemList.push(item)
          ///获取store,直接调用
          const inputStore = useInputStore()
          inputStore.inputValue = ''
        }
    })
Getter: Getter完全等同于Store状态的计算值.
export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    // 自动将返回类型推断为数字
    doubleCount(state) {
      return state.counter * 2
    },
    // 返回类型必须明确设置
    doublePlusOne(): number {
      return this.counter * 2 + 1
    },
  },
})

如果需要使用this访问到 整个store的实例,在TypeScript需要定义返回类型. 在setup()中使用:

export default {
  setup() {
    const store = useStore()

    store.counter = 3
    store.doubleCount // 6
  },
}
  • 访问其他模块的getter

    • 对于Vuex而言如果要访问其他命名空间模块的getter,需要使用rootGetters属性

/// action 方法
    addAsyncTabs ({ state, commit, rootState, rootGetters }:ActionContext<TabsState, RootState>, tab:ITab): void {
      /// 通过rootGetters 访问main的数据
        console.log('rootGetters[]=======', rootGetters['main/getCount'])
      }
  • Pinia中访问其他store中的getter

import { useOtherStore } from './other-store'
    
    export const useStore = defineStore('main', {
      state: () => ({
        // ...
      }),
      getters: {
        otherGetter(state) {
          const otherStore = useOtherStore()
          return state.localData + otherStore.data
        },
      },
    })
Action:actions 相当于组件中的methods,使用defineStore()中的 actions 属性定义.
export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  actions: {
    increment() {
      this.counter++
    },
    randomizeCounter() {
      this.counter = Math.round(100 * Math.random())
    },
  },
})

pinia中没有mutation属性,我们可以在action中定义业务逻辑,action可以是异步的,可以在其中await 任何 API调用甚至其他操作.

...
//定义一个action
asyncAddCounter () {
  setTimeout(() => {
    this.counter++
  }, 1000)
}
...
///setup()中调用
export default defineComponent({
  setup() {
    const main = useMainStore()
    // Actions 像 methods 一样被调用:
    main.asyncAddCounter()
    return {}
  }
})
  • 访问其他store中的Action

    要使用另一个 store中的action ,可以直接在操作内部使用它:

import { useAuthStore } from './auth-store'
  
  export const useSettingsStore = defineStore('settings', {
    state: () => ({
      // ...
    }),
    actions: {
      async fetchUserPreferences(preferences) {
        const auth = useAuthStore()
        ///调用其他store的action
        if (auth.isAuthenticated()) {
          this.preferences = await fetchPreferences()
        } else {
          throw new Error('User must be authenticated')
        }
      },
    },
  })

Vuex中如果要调用另一个模块的Action,我们需要在当前模块中注册该方法为全局的Action

/// 注册全局Action
 globalSetCount: {
  root: true,/// 设置root 为true
    handler ({ commit }:ActionContext<MainState, RootState>, count:number):void {
       commit('setCount', count)
     }
    }

在另一个模块中对其进行dispatch调用

/// 调用全局命名空间的函数
 handelGlobalAction ({ dispatch }:ActionContext<TabsState, RootState>):void {
   dispatch('globalSetCount', 100, { root: true })
 }

三. 总结

与 Vuex[1] 相比,Pinia[2] 提供了一个更简单的 API,具有更少的操作,提供Composition API,最重要的是,在与TypeScript一起使用时具有可靠的类型推断支持,如果你正在开发一个新项目并且使用了TypeScript,可以尝试一下pinia,相信不会让你失望。

参考资料

[1]

Vuex: https://vuex.vuejs.org/zh/#什么是"状态管理模式"?

[2]

Pinia: https://pinia.web3doc.top/core-concepts/

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

02b1af8ace049b253f91c2b9800b2980.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值