从Vuex到Pinia

3,883 阅读4分钟

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

从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应用程序开发的状态管理库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它主要用来解决多个组件状态共享的问题。

Pic

主流程: 在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 的使用

截屏2022-07-11 21.19.55

基本特点

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