介绍
Pinia.js 是新一代的状态管理器,由 Vue.js团队中成员所开发的,因此也被认为是下一代的 Vuex,即 Vuex5.x,在 Vue3.0 的项目中使用也是备受推崇。
Pinia.js 有如下特点:
- 完整的 typescript 的支持;
- 足够轻量,压缩后的体积只有1.6kb;
- 去除 mutations,只有 state,getters,actions(这是我最喜欢的一个特点);
- actions 支持同步和异步;
- 没有模块嵌套,只有 store 的概念,store 之间可以自由使用,更好的代码分割;
- 无需手动添加 store,store 一旦创建便会自动添加;
开始使用
安装 Pinia
$ yarn add pinia$ npm i pinia$ pnpm i pinia
创建Store
根据Pinia官方给的格式建议,我们可以在src目录下创建store目录,再在其下创建index.ts文件作为store的全局配置文件,并导出store,方便全局挂载。 ```typescript //src/store/index.ts import {App} from “vue” import {createPinia} from “pinia”
const store = createPinia()
export function setupStore(app:App
export {store}
在main.ts文件中进行全局挂载store文件:```typescript//src/main.tsimport { createApp } from 'vue'import App from './App.vue'import {setupStore} from "@stores/index"const app = createApp(App);// 挂载storesetupStore(app)app.mount("#app")
简单示例
先定义一个counter的store状态管理文件。
//src/store/modules/counter.tsimport {defineStore} from "pinia"export const useCounterStore = defineStore("counter",{state:()=>{return {count:0}},actions:{increment(){this.count++}}})
而后在vue文件进行使用:
<template><div>count:{{counter.count}}</div></template><script lang="ts" setup>import { useCounterStore } from '@store/modules/counter';const counter = useCounterStore();counter.count++counter.$patch({count:counter.count})counter.increment()</script>
State 数据状态
定义state状态
在使用仓库状态数据前,你需要先使用defineStore()进行定义一个仓库,需要使用唯一的名字进行命名。
//src/store/modules/counter.tsimport {defineStore} from "pinia"export const useCounterStore = defineStore("counter",{state:()=>{return {count:0}}})
使用state状态
在pinia中,state是使用函数进行定义,并返回初始化数据,这样可以允许pinia作为服务端和客户端进行使用。
我们已经定义了一个store仓库,使用useStore()进行获取仓库state数据。
<template><div>count:{{count}}</div><div>doubleCount:{{doubleCount}}</div></template><script lang="ts" setup>import { useCounterStore } from '@store/modules/counter';const counter = useCounterStore();//使用computed获取响应式数据const doubleCount = computed(()=>counter.count*2);const {count} = counter;</script>
需要注意的是,在导入store使用state状态时,直接进行解构会使得变量失去响应式。上面的数据中,count变量是失去了响应式的,它的值一直恒定不变。对此,应该像composition API使用props一样,从pinia中导出storeToRefs进行获取响应式数据。
const counter = useCounterStore();const {count} = storeToRefs(counter)
对此,count现在转为响应式的数据了,这样将会把store中的state变量成了响应式,storeToRefs将会自动跳过所有的actions和不需要进行响应式处理的属性。
修改state状态
通常,你可以通过store实例进行直接获取和修改state数据,可以使用store.$reset()进行重置state的数据回复到初始值。
import { useCounterStore } from '@stores/modules/counter';const counter = useCounterStore();counter.count++;
当然,你还可以通过使用pinia的内置API即$patch()进行更新,它允许你在同一时间进行多个数据的改变。$patch()允许你去组合多个改变的值,可以像数组进行增删改查。
import { useTodosStore } from "@stores/modules/todos";const todosStore = useTodosStore()todosStore.$patch(state=>{state.todos.push({name:"yichuan",age:18})state.isTrue = true})
$patch()允许你去组合多个改变的值,可以像数组进行增删改查。
替换state的值,可以通过设置$state的值进行替换store中的state值。
store.$state = {counter:666, name:"onechuan"}
当然也可以通过pinia的实例去改变整个state的值。
pinia.state.value= {}
但是,一般不建议直接修改state的值,Pinia中建议使用actions方法去修改和管理state的值。
监听state状态变化
订阅state的值:你可以通过store的$subscribe()方法去观察state的改变,类似于subscribe方法。与常规watch()相比,使用$subscribe()的优势在于,在补丁发布后,订阅只会触发一次。
numerStore.$subscribe((mutation,state)=>{mutation.countermutation.namemutation.isAdminlocalStorage.setItem("numer",JSON.stringify(state))})
默认情况下,状态订阅被绑定到添加它们的组件上(如果存储在组件的setup()中)。这就以为着当组件被卸载的时候,将自动移除。如果你想再组件被移除后继续保持它们,可以通过设置{detached:true}作为第二个参数来从当前组件中分离状态订阅。
const someStore = useSomeStore()someStore.$subscribe(callback, { detached: true })
Getters
getters与store状态的computed的值完全相同,可以通过defineStore()中的getters属性来定义,它们通过接收state作为箭头函数的第一个参数。
export const useStore = defineStore('counter', {state: () => ({counter: 0,}),getters: {doubleCount: (state) => state.counter * 2,},})
绝大多数情况,getters将只依赖于state,然而,它们也有可能需要去使用其它的getters。因此,在定义一个普通函数时,我们可以通过这个函数访问整个store实例,但需要定义返回类型的类型。由于ts中的已知的限制,并不影响使用箭头函数定义的getter和不使用this。
import {defineStore} from "pinia"export const useNumerStore = defineStore("numer",{state:()=>({counter:0,name:"numer",isAdmin:true}),getters:{doubleCount(state){return state.counter * 2},// 当使用this的时候,必须准确地设置返回值的类型doublePlusOne():number{return this.counter * 2 + 1}}})
当然你也可以通过计算属性去获取多个getters,需要通过this去获取任意的其他getter。
export const useStore = defineStore("main",{state:()=>({counter:0}),getters:{doubleCount:state=>state.counter * 2,doubleCountPlusOne(){return this.doubleCount + 1}}})
getter只是在幕后的计算属性,因此不能向其传递任何参数。但是你可以从getter返回一个函数来接收任何参数。
export const useStore = defineStore('main', {getters: {getUserById: (state) => {return (userId) => state.users.find((user) => user.id === userId)},},})
在组件使用:
<div>用户:{{getUserById(1)}}</div><script setup lang="ts">const numerStore = useNumerStore()const {getUserById} = numerStore</script>
注意,当这样做时,getter不再被缓存,它们只是您调用的函数。不过,您可以在getter本身中缓存一些结果,这并不常见,但应该会证明更高效.
export const useStore = defineStore('main', {getters: {getActiveUserById(state) {const activeUsers = state.users.filter((user) => user.active)return (userId) => activeUsers.find((user) => user.id === userId)},},})
获取其它store的getters,要使用另一个store getter,你可以直接在getter中使用它,其实和在vue文件中使用差别不大。
import { useOtherStore } from './other-store'export const useStore = defineStore('main', {state: () => ({// ...}),getters: {otherGetter(state) {const otherStore = useOtherStore()return state.localData + otherStore.data},},})
Actions
actions等价于组件中的方法,它们可以在defineStore()中进行定义actions属性,并且可以完美地去定义业务逻辑。
export const useStore = defineStore('main', {state: () => ({counter: 0,}),actions: {increment() {this.counter++},randomizeCounter() {this.counter = Math.round(100 * Math.random())},},})
在上面的代码中,我们可以看到actions有点类似getters,但事实上是有所不同的。
- 相同点:actions和getters都可以全类型支持来访问整个store实例。
- 不同点:actions操作可以是异步的,可以在其中等待任何api调用甚至其他操作。
注意,只要你得到了一个Promise,你使用的库并不重要,你甚至可以使用本地的fetch函数(仅适用于浏览器):
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 errorreturn error}},},})
同样的,action也可以像state和getters进行相互使用,action可以通过this直接访问。
// src/store/user.tsexport const useUserStore = defineStore({"user",state: () => ({userData: null}),actions:{async login(account, pwd) {const { data } = await api.login(account, pwd)this.setData(data) // 调用另一个 action 的方法return data},setData(data) {this.userData = data}}})
也可以在action 里调用其他 store 里的 action,引入对应的 store 后即可访问其内部的方法了。
// src/store/user.tsimport { useAppStore } from './app'export const useUserStore = defineStore({id: 'user',actions: {async login(account, pwd) {const { data } = await api.login(account, pwd)const appStore = useAppStore()appStore.setData(data) // 调用 app store 里的 action 方法return data}}})
// src/store/app.tsexport const useAppStore = defineStore({"app",state:()=>{userData: null},actions: {setData(data) {this.userData = data}}})
