从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用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import { store, key } from './store'

const app = createApp({ ... })

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

app.mount('#app')

组件中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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 支持扩展,可以非常方便地通过本地存储,事物等进行扩展。
  • 支持服务器端渲染

安装与使用

安装

1
2
3
yarn add pinia
# 或者使用 npm
npm install pinia

核心概念:

store: 使用defineStore()函数定义一个store,第一个参数是应用程序中store的唯一id. 里面包含stategettersactions, 与Vuex相比没有了Mutations.
1
2
3
4
5
6
7
8
9
10
11
12
export const useStore = defineStore('main', {
state: () => {
return {
name: 'ming',
doubleCount: 2
}
},
getters: {
},
actions: {
}
})

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.

1
2
3
const store = useStore()
// 正确的响应式解构
const { name, doubleCount } = storeToRefs(store)
State: 在Pinia中,状态被定义为返回初始状态的函数.
1
2
3
4
5
6
7
8
9
10
11
12
import { defineStore } from 'pinia'

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

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

  • 基本方法:
    1
    2
    const store = useStore()
    store.counter++
  • 重置状态:
    1
    2
    const store = useStore()
    store.$reset()
  • 使用$patch修改state
    [1] 使用部分state对象进行修改
1
2
3
4
5
const mainStore = useMainStore()
mainStore.$patch({
name: '',
counter: mainStore.counter++
})

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

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

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

    • Vuex中我们要访问其他带命名空间的模块的state我们需要使用rootState
    1
    2
    3
    4
    5
    6
    7
    8
    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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      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状态的计算值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()中使用:

1
2
3
4
5
6
7
8
export default {
setup() {
const store = useStore()

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

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

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      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 属性定义.

1
2
3
4
5
6
7
8
9
10
11
12
13
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调用甚至其他操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
//定义一个action
asyncAddCounter () {
setTimeout(() => {
this.counter++
}, 1000)
}
...
///setup()中调用
export default defineComponent({
setup() {
const main = useMainStore()
// Actions 像 methods 一样被调用:
main.asyncAddCounter()
return {}
}
})
  • 访问其他store中的Action

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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

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

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

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

三. 总结

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

一些参考:

Vuex

Pinia