一. 概述
在开发Vue项目时,我们一般使用Vuex
来进行状态管理,但是在使用Vuex
时始终伴随着一些痛点。比如:需要使用Provide/Inject
来定义类型化的InjectionKey
以便支持TypeScript
,模块的结构嵌套、命名空间以及对新手比较难理解的流程规范等。Pinia
的出现很好的解决了这些痛点。本质上Pinia
也是Vuex
团队核心成员开发的,在Vuex
的基础上提出了一些改进。与Vuex
相比,Pinia
去除了Vuex
中对于同步函数Mutations
和异步函数Actions
的区分。并且实现了在Vuex5
中想要的大部分内容。
二.使用
在介绍Pinia
之前我们先来回顾一下Vuex
的使用流程
1.Vuex
Vuex
是一个专为Vue.js
应用程序开发的状态管理库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它主要用来解决多个组件状态共享的问题。
主流程: 在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
,流程大概如下:
- 定义类型化的
InjectionKey
。 - 将
store
安装到Vue
应用时提供类型化的InjectionKey
。 - 将类型化的
InjectionKey
传给useStore
方法并简化useStore
用法
1 | // store.ts |
main.ts
1 | import { createApp } from 'vue' |
组件中使用
1 | <script lang="ts"> |
Pinia 的使用
基本特点
Pinia
同样是一个Vue的状态管理工具,在Vuex
的基础上提出了一些改进。与vuex相比,Pinia
最大的特点是:简便。
- 它没有
mutation
,他只有state
,getters
,action
,在action
中支持同步与异步方法来修改state
数据 - 类型安全,与
TypeScript
一起使用时具有可靠的类型推断支持 - 模块化设计,通过构建多个存储模块,可以让程序自动拆分它们。
- 非常轻巧,只有大约 1kb 的大小。
- 不再有
modules
的嵌套结构,没有命名空间模块 Pinia
支持扩展,可以非常方便地通过本地存储,事物等进行扩展。- 支持服务器端渲染
安装与使用
安装
1 | yarn add pinia |
核心概念:
store
: 使用defineStore()
函数定义一个store,第一个参数是应用程序中store的唯一id. 里面包含state
、getters
和 actions
, 与Vuex相比没有了Mutations
.
1 | export const useStore = defineStore('main', { |
注意:store 是一个用reactive 包裹的对象,这意味着不需要在getter 之后写.value,但是,就像setup 中的props 一样,我们不能对其进行解构.
1 | export default defineComponent({ |
当然你可以使用computed
来响应式的获取state的值(这与Vuex中需要创建computed
引用以保留其响应性类似),但是我们通常的做法是使用storeToRefs
响应式解构Store.
1 | const store = useStore() |
State
: 在Pinia中,状态被定义为返回初始状态的函数.
1 | import { defineStore } from 'pinia' |
组件中state的获取与修改:
在Vuex
中我们修改state
的值必须在mutation
中定义方法进行修改,而在pinia
中我们有多中修改state的方式.
- 基本方法:
1
2const store = useStore()
store.counter++ - 重置状态:
1
2const store = useStore()
store.$reset() - 使用
$patch
修改state
[1] 使用部分state
对象进行修改
1 | const mainStore = useMainStore() |
[2] $patch
方法也可以接受一个函数来批量修改集合内部分对象的值
1 | cartStore.$patch((state) => { |
替换state
可以通过将其 $state 属性设置为新对象,来替换Store
的整个状态:1
mainStore.$state = { name: '', counter: 0 }
访问其他模块的
state
Vuex
中我们要访问其他带命名空间的模块的state我们需要使用rootState
1
2
3
4
5
6
7
8addAsyncTabs ({ 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 中访问其他
store
的state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import { 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 | export const useStore = defineStore('main', { |
如果需要使用
this
访问到 整个store
的实例,在TypeScript
需要定义返回类型.
在setup()
中使用:
1 | export default { |
访问其他模块的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中的getter1
2
3
4
5
6
7
8
9
10
11
12
13import { 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 | export const useStore = defineStore('main', { |
pinia
中没有mutation
属性,我们可以在action
中定义业务逻辑,action
可以是异步的,可以在其中await 任何 API调用甚至其他操作.
1 | ... |
访问其他store中的Action
要使用另一个 store中的action ,可以直接在操作内部使用它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { 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
,相信不会让你失望。
一些参考: