Pinia简介
简介
Pinia
是 Vue
新一代的轻量级状态管理库,相当于Vuex
,也是Vue核心团队推荐的状态管理库。
并且实现了一些Vuex 5
的RFCs,同时支持 Vue2
和 Vue3
。
未来很有可能替代Vuex
,比Vuex
更容易上手。
名字来源
Pinia
是西班牙语中菠萝一词最相似的英语发音:piña
。菠萝实际上是一组单独的果实,它们结合在一起形成一个水果。与store
类似,每个store
都是单独存在的,但它们最终都是相互关联的。它也是一种美味的热带水果,原产于南美洲。
诞生
Pinia
从2019年11月左右,开始尝试重新使用Composition API 设计Vue Store
。
Pinia
试图尽可能接近 Vuex
的理念,旨在测试 Vuex
下一次迭代的一个方案。目前,Vuex 5的open RFC,API与Pinia的API非常相似,这说明Pinia
成功了。
注意,Pinia的作者(Eduardo),也是Vue.js
核心团队的一员,积极参与 Router
和 Vuex
等API的设计。设计 Pinia
的目的是想重新设计使用store
的体验,同时保持Vue
的容易理解的理念。将Pinia
的API与Vuex
保持尽可能的接近,因为它不断向前发展,使人们能够方便地迁移到Vuex
,甚至在未来融合两个项目。
翻译自:pinia.vuejs.org/introductio…
特性
Pinia
具有以下几点特性:
- 直观,像定义
components
一样地定义store
- 完整的
Typescript
支持 - 去除
mutations
,只有state,getters,actions
actions
支持同步和异步Vue Devtools
支持Pinia
,提供更好的开发体验- 能够构建多个
stores
,并实现自动地代码拆分 - 极其轻量(1kb),甚至感觉不到它的存在
图解pinia
对比vuex
安装Pinia
下述demo使用vue3
,
先用vite
快速创建一个vue
项目:
npm init vite@latest
安装pinia
:
npm install pinia
在 src/main.ts
文件,使用Vue.use()
方法将pinia
作为插件使用:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
createApp(App).use(createPinia()).mount('#app')
store定义
defineStore
Pinia
是通过defineStore()
这个方法来定义的,它的第一个参数为当前store
的id(store名称),需要保证唯一。
创建store文件:common.ts
import { defineStore } from 'pinia'
export default defineStore('common', {
// other options
});
useStore
通过 import
导入 javascript
模块的方式引入,引入后,直接使用变量接收即可。
<script setup lang="ts">
import useCommonStore from '../store/common';
// setup内不用导出,定义变量即可使用
const common = useCommonStore();
</script>
引入后,F12打开Vue Devtools
查看,如下图所示:
可以看到定义的变量以及pinia定义的store
对比Vuex
从以上的Pinia
定义store
和使用store
,可以看出,Pinia
不同于Vuex
:
Vuex
:
Vuex
的store
需要一个主入口- 通过
modules
属性,拆分成不同的模块 - 自动挂载在
Vue
实例上,通过this.$store
去调用或者mapGetters
等方法
Pinia
:
Pinia
的store
不需要主入口- 直接创建不同的
store
文件即可实现多模块 - 在使用时,直接通过
javascript
的模块导入,即可使用,可方便看到从哪个文件导入
State(数据)
state
存储store
的数据部分,Pinia
的state
是一个返回不同data对象的函数,完整类型推断建议使用箭头函数。
非常类似于我们组件定义中的data项。
定义state
store/common.ts
:
export default defineStore('todo', {
state: () => {
return {
name: '小花的store',
list: [],
}
}
});
使用state
以javascript
中的模块导出的方式导出store
数据,state
中的数据均可通过变量.state数据名
获取:
直接获取:
<script setup lang="ts">
import useCommonStore from '../store/common';
const common = useCommonStore();
</script>
<template>
<div>
{{ common.name }}
</div>
</template>
解构获取:
store
是一个reactive
响应式对象,直接解构会使其失去响应式,类似setup
中的props
,为了既可解构又可保持其响应式,可使用storeToRefs
,它将为每个reactive
属性创建refs
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import useCommonStore from '../store/common';
const { name } = storeToRefs(useCommonStore());
</script>
<template>
<div>
{{ name }}
</div>
</template>
修改state
可以通过以下三种方式进行修改state:
- 直接修改state:
<script setup lang="ts">
import useCommonStore from '../store/common';
const common = useCommonStore();
common.count++
</script>
- $patch以对象形式修改:
<script setup lang="ts">
import useCommonStore from '../store/common';
const common = useCommonStore();
......
common.$patch({
name: '小花的store',
list: [
{
name: '旺仔牛奶',
stock: 100,
price: 5
},
{
name: '大辣片',
stock: 1000,
price: 5
}
],
})
</script>
缺点: 如果只需修改state数据中的某一项,仍然需要将整个对象传给store。
- $patch接收函数:
接收一个函数做为参数,函数参数为state
对象,可直接针对要修改的属性进行修改。
<script setup lang="ts">
import useCommonStore from '../store/common';
const common = useCommonStore();
...
common.$patch(state => {
state.list[1].stock = 1000;
});
</script>
替换state
可以通过设置 store
的 $state
属性为一个新对象,来替换 store
的整个state
。
// 重新设置$state的值为一个新的对象
import useCommonStore from '../store/common';
const common = useCommonStore();
......
common.$state = {
name: '小花的store new',
grade: 'A',
list: [
{
name: '旺仔牛奶new',
stock: 100,
price: 5
},
{
name: '大辣片new',
stock: 100,
price: 5
}
],
}
重置state
可以通过 store
的 $reset()
方法重置 state
的值为初始值,比如修改了name、库存等,可一键重置,将值初始化为初始状态的值。
import useCommonStore from '../store/common';
const common = useCommonStore();
common.$reset();
订阅state
通过store
的$subscribe()
方法监听state及其变化,类似于Vuex
的subscribe
方法。与常规watch()
相比,使用$subscribe()
的优点是,在patch
之后,subscribe
只会触发一次。
import useCommonStore from '../store/common';
const common = useCommonStore();
......
// 监听整个store
common.$subscribe((mutation, state) => {
console.log('mutation: ', mutation);
console.log('state: ', state);
})
当我们触发页面上更改 store
的操作时,则会触发 subscribe
监听,监听函数有两个参数 mutation
和 state
。
mutation
:包含3个参数
type
:操作类型,'direct' | 'patch object' | 'patch function'
storeId
:操作的store id
events
:操作的事件详情,包括针对的数据、新值、旧值等
state
:Proxy类型的对象
state订阅与组件分离
默认情况下,状态订阅绑定到添加它们的组件(如果store是在组件的setup()中)。也就是说,当卸载组件时,它们将自动删除。如果要在卸载组件后保留它们,可将{detached:true}
作为第二个参数传递,以从当前组件分离state订阅。
export default {
setup() {
const someStore = useSomeStore()
// this subscription will be kept after the component is unmounted
someStore.$subscribe(callback, { detached: true })
// ...
},
}
在pinia
实例上监听整个state
watch(
pinia.state,
(state) => {
// persist the whole state to the local storage whenever it changes
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
Getters(计算数据)
Getters
完全等同于 store
中 state
的 computed values
。可以使用defineStore()
中的 getters
属性定义它们。
接收state
作为第一个参数,推荐使用箭头函数。
定义getters
export default defineStore('common', {
// getters是一个对象
getters: {
doubleCount: (state) => state.count * 2,
newList: (state) => {
return state.list.filter(item => item.price > 5)
}
}
});
使用getters
<script setup lang="ts">
import useCommonStore from '../store/common';
const common = useCommonStore();
const { doubleCount, newList } = storeToRefs(useCommonStore());
</script>
<template>
<div> {{ doubleCount }} </div>
<div> {{ newList }} </div>
</template>
访问其他getters
与计算属性一样,可以组合多个 getters
,通过this.
去访问其他getters
。
export default defineStore('common', {
// getters是一个对象
getters: {
doubleCount: (state) => state.count * 2,
newList: (state) => {
return state.list.filter(item => item.price > 5)
},
// 必须显示定义返回的类型,内部通过this访问store中的数据
doubleCountPlus(): number {
// autocompletion and typings for the whole store ✨
return this.doubleCount * 2 + 1
},
}
});
给getters传递参数
getters
只是一个计算属性,因此不可能向其传递任何参数。但是,可以从getters
返回一个函数来接受任何参数。
export default defineStore('common', {
getListById: state => {
return (id: number) => state.list.find((item) => item.id === id);
}
});
<script setup lang="ts">
import useCommonStore from '../store/common';
const common = useCommonStore();
const { getListById } = storeToRefs(useCommonStore());
</script>
<template>
<div> {{ getListById(2) }} </div>
</template>
注意:使用这种方式的时候,getters
不再被缓存,只是函数调用。
访问其他store的getters
访问其他 store
的 getters
,可以直接引入其他 store
文件,在 getters
内部使用它。
import { useOtherStore } from './other-store'
export const useStore = defineStore('main', {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
在options API中使用
不在setup
中使用:
使用 mapState
访问store中的数据。
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
// gives access to this.doubleCounter inside the component
// same as reading from store.doubleCounter
...mapState(useCounterStore, ['doubleCount'])
// same as above but registers it as this.myOwnName
...mapState(useCounterStore, {
myOwnName: 'doubleCounter',
// you can also write a function that gets access to the store
double: store => store.doubleCount,
}),
},
}
Actions(方法)
Actions
相当于组件中的方法,可以用 defineStore()
中的 actions
属性定义,非常适合定义业务逻辑。
定义actions
export const useStore = defineStore('common', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
})
使用actions
同步的方式:
<script setup lang="ts">
import useCommonStore from '../store/common';
const common = useCommonStore();
</script>
<template>
<button @click="common.increment()">触发actions</button>
</template>
异步的方式:
import { mande } from 'mande'
const api = mande('/api/users')
export const useUsers = defineStore('users', {
state: () => ({
userData: null,
// ...
}),
actions: {
async registerUser(login, password) {
try {
this.userData = await api.post({ login, password })
showTooltip(`Welcome back ${this.userData.name}!`)
} catch (error) {
showTooltip(error)
// let the form component display the error
return error
}
},
},
})
访问其他store的actions
import { useAuthStore } from './auth-store'
export const useSettingsStore = defineStore('settings', {
state: () => ({
// ...
}),
actions: {
async fetchUserPreferences(preferences) {
const auth = useAuthStore()
if (auth.isAuthenticated) {
this.preferences = await fetchPreferences()
} else {
throw new Error('User must be authenticated')
}
},
},
})
在options Api中使用
不在setup
中使用:
使用 mapActions
访问store中的数据。
import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
methods: {
// gives access to this.increment() inside the component
// same as calling from store.increment()
...mapActions(useCounterStore, ['increment'])
// same as above but registers it as this.myOwnName()
...mapActions(useCounterStore, { myOwnName: 'doubleCounter' }),
},
}
订阅actions
使用store.$onAction()
订阅actions,传递给它的回调函数在action
之前执行,after
在actions
resolves之后执行,onError
在actions
抛出异常和错误的时候执行。
const unsubscribe = someStore.$onAction(
({
name, // name of the action
store, // store instance, same as `someStore`
args, // array of parameters passed to the action
after, // hook after the action returns or resolves
onError, // hook if the action throws or rejects
}) => {
// a shared variable for this specific action call
const startTime = Date.now()
// this will trigger before an action on `store` is executed
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// this will trigger if the action succeeds and after it has fully run.
// it waits for any returned promised
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})
// this will trigger if the action throws or returns a promise that rejects
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
// manually remove the listener
unsubscribe()
$onAction
一般是在组件的 setup
建立,它会随着组件的 unmounted
而自动取消。如果你不想让它取消订阅,可以将第二个参数设置为 true
:
someStore.$onAction(callback, true)
Plugins(插件)
通过一些low level Api(底层API),可以对pinia store
进行扩展:
- 给
store
添加新的属性 - 给
store
添加新的选项 - 给
store
添加新的方法 - 包装已经存在的方法
- 修改或者删除
actions
- 基于特定的
store
做扩展
Plugins
通过 pinia.use()
添加到 pinia
实例中。
在store
目录下创建pinia-plugin.ts
,存储plugins
相关:
import { PiniaPluginContext } from "pinia";
export default function piniaPlugin(context: PiniaPluginContext) {
console.log('context:', context);
}
然后在main.ts
中引入插件
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPlugin from './store/pinia-plugin'
import App from './App.vue'
const pinia = createPinia();
pinia.use(piniaPlugin);
createApp(App).use(pinia).mount('#app')
运行后,打印出来的context如下图:
context
内容分为4部分:
-
createApp()
中创建的app
实例 defineStore
中的配置createPinia()
中创建的pinia
实例- 当前
store
对象
我们可以基于上面的context
进行扩展。
总结
本文对pinia
的诞生、安装、state
、getters
、actions
、plugins
,都进行了详细介绍。
pinia
去掉了mutation
,支持直接导入使用,配合vue3 setup
,使得store
管理起来更加的方便。
如有不对之处,欢迎批评指正!也可以留言相互交流学习!