1月20日,尤大在知乎平台发布 Vue3
将自2月7日起成为默认版本的消息
Vue3
相关的技术栈也可以学习起来啦~本文就同大家一起初步体验下 “下一代的Vuex” -- Pinia。
什么是Pinia
Pinia 介绍
大概在2019年11月份左右,Pinia 的作者开始尝试重新设计 Store 状态管理以适用于 Composition API,其设计原则跟思想与 Vuex 保持一致。
在不同版本的 Vue 中,所使用的 Vuex 版本是不一样的。Vue2 中需要使用 Vuex 3.x的版本,而在 Vue3 中需要使用 Vuex 4.x。但是 Pinia 没有这个限制,无论是在 Vue2 中,还是在 Vue3 中均可以使用 Pinia,且不一定要与 Composition API 一起使用,API 的使用方式在两者中也是保持一致的。
现在,Pinia 也已经正式被纳入 Vue 官方体系中。笔者写这篇文章时,其版本已更新至 v2.0.9 。
为啥要用 Pinia
Pinia 是用于 Vue 中的状态管理器,允许你跨组件/页面共享状态。
如果小伙伴们已经对 Composition API 比较熟悉了的话,可能会考虑使用类似下面这种写法去共享全局状态:
export const state = reactive({})
在 SPA 应用中,这样去做是可以的,但是如果是在 SSR 中去使用的话会存在安全隐患。所以还是建议使用 Pinia。
使用 Pinia 有以下好处:
-
调试工具支持
笔者目前安装的是 beta 版本的 Vue devtool,小伙伴们可以去 谷歌应用商店 中搜索
Vue devtool
下载对应的 beta 版本。
- 调试工具中支持追踪 actions,mutations 的timeline(时间线);
- 可以显示当前组件中使用的 store 信息,也可以显示所有 store 容器的信息,支持直接修改数据进行调试;
- 支持 Time travel(时间旅行) 以方便调试。
- 热模块替换:改变状态时无需手动刷新页面。
- 插件支持:可以通过插件扩展 Pinia 的能力。
- 良好的 Typescript 支持:可以提供较为完善的代码提示以及代码自动补充。
- 支持 SSR。
Pinia 与 Vuex 的对比
Pinia 与 Vuex4.x 以下版本相比,有如下区别:
- 移除了 mutations: 在 Vuex 中,想要改变状态,需要通过提交 mutations 来实现,而 mutations 不能执行异步操作,因此诞生了 actions ,在异步获取数据后,通过 actions 调用 mutations 修改数据。现在在 Pinia 中,actions 同时支持同步与异步操作,所以 mutations 显得有些冗余了,因此不再需要;
- 对于 Typescript 的支持更加友好:使用 Pinia 时,不再需要定义复杂的 wrappers(包装器)去支持Typescript;
- 不会注入魔法字符串:在组件中使用 Vuex 时,经常会出现类似
this.$store.commit("xxxxx")
这样的魔法字符串,虽然会将这些字符串抽为常量暴露出来,但是维护起来还是不太方便。在 Pinia 中不再需要使用这种方式,而是直接调用defineStore
后暴露出来的方法,而后通过$patch
,action
的方式,或者直接修改 store 中的值即可;- 不需要动态添加 stores:只要你愿意,你可以随时定义 stores,它们默认是动态的;
- 不再需要嵌套结构的 modules;
- 没有带命名空间的模块。
Pina的使用
安装
我们先在本地搭建一个Vue3的项目,用于演示 Pinia
的使用,这里我们使用 Vite 来搭建:
Vite 需要 Node.js 版本 >= 12.0.0。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。
Vite官方提供了使用模板的方式构建本地项目,这里我们通过 pnpm 来构建一个 Vite + Vue
的本地项目。现在 Vue3 以及 Pinia 对于 Typescript 的支持已经非常良好了,建议大家在使用模板构建项目的时候直接选择 Typescript 对应的模板:
pnpm create vite pinia-useage-examples -- --template vue-ts
之后,我们在项目中安装 Pinia
:
pnpm add pinia
准备工作完成,接下来就来一起体验一下 Pinia
吧~
使用
首先,我们需要通过 createPinia
初始化 Pinia,并将其挂载到 Vue 的实例上:
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// 创建 pinia
const pinia = createPinia()
const app = createApp(App)
// 挂载到 Vue 实例上
app.use(pinia)
app.mount('#app')
之后按照惯例,我们先创建 src/store/index.ts
文件,用于存放 store:
// src/store/index.ts
import { defineStore } from 'pinia'
// 定义store, myFirstStore是store的名称,该名称必须唯一,不可重复
export const useStore = defineStore('myFirstStore', {
})
这里需要注意的是,defineStore
的第一个参数用于设置 store 的容器名称,该名称必须唯一,不可重复!
上文中提到,相比于 Vuex,Pinia 中移除了 mutations,剩余需要了解的内容主要包含四个部分:
- State: 用于存放数据,有点儿类似
data
的概念;- Getters: 用于获取数据,有点儿类似
computed
的概念;- Actions: 用于修改数据,有点儿类似
methods
的概念;- Plugins: Pinia 插件。
现在我们依次来看一看它们的用法。
State
定义 State
在 Vuex 中,通过对象的形式来定义 state ,而在 Pinia 中则需要通过函数的方式定义 state,这种方式有点儿类似组件中 data
的概念:
// src/store/index.ts
import { defineStore } from 'pinia'
// 定义store, myFirstStore是store的名称,该名称必须唯一,不可重复
export const useStore = defineStore('myFirstStore', {
state: () => {
return {
count: 0,
name: 'foo',
list: [1, 2, 3]
}
}
})
读取 State
state 定义完成后,我们在页面中可以通过引入暴露出去的 useStore
来读取 state 中的数据:
// src/App.vue
<template>
<p>count: {{ myStore.count }}</p>
<p>name: {{ myStore.name }}</p>
<p>list: {{ myStore.list }}</p>
</template>
<script setup lang="ts">
import { useStore } from './store'
const myStore = useStore()
</script>
看到这里,有的小伙伴不禁会问,在使用过程中,一直要写 myStore.xxx
不是很麻烦么?能否通过解构的方式来使用呢?
那我们就一起来试一试,看看是否可以通过解构的方式来获取 store 中的值:
// src/App.vue
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>count: {{ count }}</p>
<p>name: {{ name }}</p>
<p>list: {{ list }}</p>
</template>
<script setup lang="ts">
import { useStore } from './store'
const { count, name, list } = useStore()
</script>
通过这次尝试我们发现,确实可以通过解构的方式来获取到 store 的值。但是这种方式存在一个问题:直接通过解构的方式获取state中的值是非响应式的! 这就意味着后面在对 store 中的值进行修改之后,页面不会发生变化。
那我们改如何解决这个问题呢?答案是通过 Pinia 中提供的 storeToRefs
方法,将结构出来的值转换为响应式的值即可:
// src/App.vue
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useStore } from './store'
const { count, name, list } = storeToRefs(useStore())
</script>
除了使用这两种方式以外,Pinia 也提供了 mapState
与 computed
相结合的方式去获取 store 中的值,这种方式与 Vuex 中的 mapState
类似:
// src/App.vue
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>count: {{ res.count }}</p>
<p>name: {{ res.name }}</p>
<p>list: {{ res.list }}</p>
</template>
<script setup lang="ts">
import { mapState } from 'pinia'
import { useStore } from './store'
import { computed } from 'vue'
const res = computed(() => {
const data = {
...mapState(useStore, ['count', 'name', 'list'])
}
return {
count: data.count(),
name: data.name(),
list: data.list()
}
})
</script>
由于是通过 computed
获取到的值,所以结构之后的值就是响应式的,不需要使用 stateToRefs
方法进行转换。
在 computed
内部也可以对属性进行重命名,或者重新定义新的返回值返回值:
<script setup lang="ts">
import { mapState } from 'pinia'
import { useStore } from './store'
import { computed } from 'vue'
const res = computed(() => {
const data = {
...mapState(useStore, {
// 重命名count为myCount, 并返回count + 1的结果
myCount: state => state.count += 1,
// 重命名name为没有Name,并返回name的值
myName: 'name',
// 重命名list为myList,并返回插入4之后的list
myList: state => {
state.list.push(4)
return state.list
}
})
}
return {
count: data.myCount(),
name: data.myName(),
list: data.myList()
}
})
</script>
注意:
修改 State
现在我们已经可以获取到 State 中存储的值了,那么我们该如何去修改 State 中的值呢?其实很简单,主要分为以下几种方式:
第一种:直接修改
直接修改 State 中的值即可:
// src/App.vue
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>count: {{ count }}</p>
<button @click="handleChangeStore">change store</button>
</template>
<script lang="ts" setup>
import { useStore } from './store'
const store = useStore()
// 修改store中的值
const handleChangeStore = () => {
// 直接修改
store.count += 1
}
</script>
甚至可以通过暴力覆盖 $state
的方式将所有值进行替换修改:
// src/App.vue
// 省略好多好多代码。。。
// 直接清空store
store.$state = {}
第二种: 通过 $patch
的方式修改
当我们需要一次性修改多个值的时候,我们可以使用 $patch
方法来批量修改多个值:
// src/App.vue
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>name: {{ name }}</p>
<p>list: {{ list }}</p>
<button @click="handleChangeStore">change store</button>
</template>
<script lang="ts" setup>
import { useStore } from './store'
const store = useStore()
// 修改store中的值
const handleChangeStore = () => {
// 通过 $patch 的方式修改
store.$patch({
name: 'test',
list: [...store.list, 4]
})
}
</script>
$patch
会将传入的对象与 Store 中的 state 进行 merge 覆盖。
除了直接传入对象外,如果存在较为复杂的操作,$patch
方法也接受传入一个回调函数,在回调函数中对需要修改的数据进行操作:
// src/App.vue
// 省略好多好多代码。。。
store.$patch(state => {
state.name = `我的名字是:${state.name}`
})
第三种:通过 $reset
方法恢复原值
当我们修改了 state 中的值以后,倘若我们想让它们恢复到初始值,可以使用 $reset
方法。
第四种:通过调用 actions
中的方法去修改值
首先,我们需要在 Store 容器中定义 actions
:
// src/store/index.ts
// 定义store
export const useStore = defineStore('myFirstStore', {
// 省略一些代码
actions: {
changeCount () {
this.count ++
}
}
})
在 actions
中定义的方法,可以直接通过 this
来访问 state 中的属性,而无需像 Vuex 那样传入上下文才可以,使用方式有点儿类似 methods
。
定义完成后,我们就可以在引用处直接使用它了:
// src/App.vue
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>count: {{ count }}</p>
<button @click="handleChangeStore">change store</button>
</template>
<script setup lang="ts">
import { useStore } from './store'
const store = useStore()
// 修改store中的值
const handleChangeStore = () => {
// 通过actions的方式修改
store.changeCount()
}
</script>
当然,actions
中的方法也可以接收入参,使用方式与 methods
并无区别,这里不做过多赘述。
调试 State
打开控制台,切换到 Vue devtool 中,选择 Pinia
选项,就可以看到 store 容器的信息:
并且点击数据后面的编辑按钮,也可以修改 store 中的值进行调试,非常方便:
Getters
getters
的使用方式有点儿类似于 computed
, 返回一个值(没有返回值也可以)且在页面中可以直接使用。如果 getters
中使用了 state
里的数据,当对应的数据发生变化时,getters
的值也会相应发生变化:
// src/store/index.ts
// 定义store
export const useStore = defineStore('myFirstStore', {
// 省略一些代码
getters: {
countPlusOne (state) {
console.log('------countPlusOne------')
return state.count + 1
}
}
})
直接在页面中使用,并且 getters
也有缓存,在页面中多次使用时,只会计算一次:
// src/App.vue
<template>
<p>countPlusOne: {{ store.countPlusOne }}</p>
</template>
<script setup lang="ts">
import { useStore } from './store'
const store = useStore()
</script>
我们可以看到日志只打印了一次,说明后面两次是取的缓存中的数据。
注意:
getters
中定义的方法,入参state
是可选参数,如果不传入state
, 可以直接使用this
访问state
中的数据;- 如果不传入
state
的话,Typescript 会有报错提示,因为无法推导出getters
中方法的返回值类型,需要手动声明返回值类型。
Actions
上文中提到可以通过 Actions
的方式修改 State
中的值,只不过所使用的是同步的方式。在一开始对 Pinia 的介绍中也提到,Actions
同时支持同步与异步两种修改数据的方式。接下来我们看看如何通过 Actions
异步修改数据。
// src/store/index.ts
// 省略好多好多代码。。。
actions: {
async changeName () {
const newName: string = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve('newName')
}, 1000)
}).catch(err => {
})
this.name = newName
}
}
通过上述代码我们不难发现,其实 Actions
通过异步修改数据的方式与寻常使用 methods
异步修改 data
中的数据类似,相较于 Vuex 简便许多。
Plugins
插件介绍
通过自定义 Plugins
可以扩展 Stores
,它可以完成以下这些事情:
- 新增
store
中的属性;- 定义
store
时传入新的配置项;- 新增
store
中的方法;- 包装
store
中的既有方法;- 修改甚至取消
actions
;- 实现一些副作用操作,例如设置 local storage 缓存等等;
- 只会应用于特定的
store
: 在 Pinia 挂载到 app 上后,插件会挂载到之后创建的 Stores 上,在此之前 Plugins 不会被挂载。
插件定义 & 使用
插件本质上是一个函数,插件接受一个可选入参: context
, 改入参是个对象,其中包含四项内容:
- context.pinia: 通过
createPinia()
方式创建的 Pinia 实例 - context.app: 通过
createApp()
方式创建的 app 实例(仅支持Vue 3) - context.store: store 对象
- context.options: 获取定义 store 时传入的配置项
函数可以通过返回对象的方式,将对象中的值绑定到所有的 store 上去:
// src/main.ts
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
定义完成后,通过 pinia.use()
的方式使用这个插件:
// src/main.ts
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(SecretPiniaPlugin)
之后,在页面中打印 Store 中的 secret
属性时,即可看到我们设定的值。并且通过这种方式新增到 store 中的属性可以被 Vue devtool 捕获到:
当然你也可以选择直接将值绑定到 store 对象上,但是这样无法被 Vue devtool捕获:
// src/main.ts
// 定义插件
function SecretPiniaPlugin({ store }) {
// 直接将属性绑定到store上去
store.secret = 'the cake is a lie'
}
此时我们不难发现,上述步骤中的 customProperties
属性不见了。如果在调试过程中需要在 Vue devtool 中看到插件中定义的 store 内容,需要手动添加:
// src/main.ts
// 定义插件
function SecretPiniaPlugin({ store }) {
// 直接将属性绑定到store上去
store.secret = 'the cake is a lie'
// 在开发环境中手动添加该属性,以便 Vue devtool 可以捕获到
if (process.env.NODE_ENV === 'development') {
// add any keys you set on the store
store._customProperties.add('secret')
}
}
添加完属性后,当我们直接在页面中使用时,发现页面上并不能显示出插件插入到 store 中的属性,这是因为插入的属性是非响应式的,此时,我们只需将插入的属性值变为响应式数据即可:
import { createApp, ref } from 'vue'
import { createPinia } from 'pinia'
// 创建 pinia
const pinia = createPinia()
// 定义插件
function SecretPiniaPlugin({ store }) {
// 直接将属性绑定到store上去
store.secret = ref('the cake is a lie')
// 在开发环境中手动添加该属性,以便 Vue devtool 可以捕获到
if (process.env.NODE_ENV === 'development') {
// add any keys you set on the store
store._customProperties.add('secret')
}
}
// 使用插件
pinia.use(SecretPiniaPlugin)
设置完成后,再在页面中使用时,就可以在页面上看到数据啦~
此外,在插件中可以通过 store.$subscribe
以及 store.$onActions
方法监听 actions 的触发。
关于插件的更多玩法可以参考官方文档。
写在最后
行文至此,相信大家对于 Pinia
已经有了一个初步的印象。文章中若有描述不当之处还请诸位大佬指正。
好记性不如烂笔头,关于 Pinia 的其他更多玩法等待着小伙伴们去挖掘。赶快动手 卷起来 尝个鲜吧~