前言
在开始学习pinia之前你需要知道什么是pinia?
Pinia 是 Vue.js 的轻量级状态管理库
说到vue的状态管理库,除了pinia之外我们最多用到的估计是vuex,关于他们两之间的区别,pinia的官网给出了解释,这里简单摘录一段
Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法。最终,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分内容,并决定实现它 取而代之的是新的建议。
与 Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的规范,提供了 Composition-API 风格的 API,最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持。
通过以上我们就能有一个大致的认识:
- pinia的使用相比vuex更简单(不再有mutations、模块更加友好灵活)
- pinia的使用风格上更贴近于vue3的Composition-API 风格
- pinia对于ts的支持性更好,有着更可靠的类型推导
- 最重要一点pinia将会是vuex的未来形态(早晚都要学,早学早解脱)
看到了以上的描述,至少从开发者体验上来讲,如果你对vuex的mutations和分模块命名空间冗杂的调用方式感到厌烦,如果你想获得更好的ts支持和类型推导、如果你想使用一个语法更加简洁的vue状态管理库,那么你无法拒绝学习pinia,尤其是在使用Composition-API 风格风格下
在这里我要提前说的是pinia的中文官方文档挺友好的,因为pinia本身的api并不多,使用起来相对于vuex要简单,因此我建议你去通读官方文档,获得最权威的指引,但如果你和我一样(懒批),上手新技术时看到长篇文档就头疼,又想快速上手pinia,那么本篇新手司机指南也够用了
基础使用
在介绍pinia的使用之前,先声明文中所有的例子,都是基于vue3的实例展示,如果你不熟悉vue3,那么建议你先去熟悉一下vue3的setup及其语法糖
安装和使用
安装比较简单,根据你使用npm或者yarn自己选择安装就行,这没什么好说的,注意安装时是生产依赖不是开发依赖
//npm管理
yarn add pinia
//yarn管理
npm i pinia
基础使用直接上代码展示
main.js
// 在main.js中注册pinia
import { createPinia } from 'pinia'
app.use(createPinia())
这样就成功在项目中注册了pinia,接下来让我们定义一个store,在pinia中每一个store都是通过defineStore方法创建的,该方法第一个参数是store的唯一id,第二个参数是一个配置对象,具体配置我们在核心概念中讲解,现在你可以先体会这种书写方式
store/counter.js
import { defineStore } from 'pinia'
// 第一个参数counter是该store的唯一id标示,第二个参数是一个配置对象
export const useCounter = defineStore('counter', {
state: () => {
return {
counter: 0,
name:"COUNTER"
}
}
})
注意:你并不一定要命名为useCounter,你可以在store下定义多个.ts或者.js文件,用以划分模块,比如你还有一个用户信息store模块,你可以创建一个userInfo.js文件,同样的格式然后命名为useUserInfo,以此类推你可以划分更多的storeModule来满足你的业务需要
在组件中使用:
components/Demo.vue
<script setup>
import useCounterStore from './store/counter'
const counterStore = useCounterStore() //这样我们就可以在组件中使用该store了
</script>
<template>
<h1>{{ counterStore.count }}</h1>
<h1>{{ counterStore.name }}</h1>
</template>
以上就是pinia的基础安装及引入使用,下面我们看下pinia的核心概念及其使用方法
核心概念
pinia同vuex有5大核心不同(state、getters、mutations、actions、modules),它的只有3大核心(state、getters、actions),下面分别介绍一下:
核心一:state
一般情况下state是整个pinia创建的store的核心,在 Pinia 中,state被定义为返回初始状态的函数
import { defineStore } from 'pinia'
export const useCounter = defineStore('counter', {
// state是一个返回初始值的函数
state: () => {
return {
counter: 0,
name:"COUNTER"
}
}
})
根据上面的例子,在组件中使用,我们可以接在store中访问其值并读写
import useCounterStore from './store/counter'
const counterStore = useCounterStore()
//对!没错!不再需要通过mutations,你可以直接进行修改store中state的值,且这种修改时响应式的会触发ui的更新
counterStore.counter++ // counter:1
当然你可以重置
这个state
counterStore.$reset() // counter:0
批量修改
的方式
counterStore.$patch({
counter: counterStore.counter + 1,
name: 'Abalam',
})
// 当然还有另一种处理复杂逻辑的方式
counterStore.$patch((state) => {
// state是当前store的索引,你可以函数中处理更复杂的逻辑,并修改这个state
state.counter = state.counter + 1
state.name = 'Abalam'
})
即使你只改变一个属性,你依然可以使用批量修改的方式,对于批量修改和单独读写修改的方式的使用建议是,如果你一次性修改多个值或者有复杂逻辑需要处理,那么建议你使用$patch,相比单个逐一修改性能更高,反之建议你单独读写修改
替换state
(注意使用这个需要慎重,这个方法将直接替换掉你原有的state)
counterStore.$state = { counter: 666, name: 'Paimon' }
关于解构
的问题
请注意,store 是一个用reactive
包裹的对象,这意味着不需要在getter 之后写.value
,但是,就像setup
中的props
一样,我们不能对其进行解构:
const { name, counter } = counterStore // 这样是不行的,会导致name、couter失去响应式
但是就想我们以前解决解构reactive数据使用的toRefs
一样,pinia也给我们提供了一个方法storeToRefs
const { name, counter } = storeToRefs(counterStore)
通过这种方式我们就可以保持解构出来的name、counter依旧保持响应式,但是注意storeToRefs会跳过action
和其他非响应式的数据
,只会为响应式数据创建refs
订阅状态
的方法
可以通过store.$subscribe()
来订阅state的变化,当然使用watch也可以做到这点,使用 $subscribe()
的优点是 subscriptions 只会在 patches 之后触发一次
比如我们想在store的state发生改变时,将其存入localStorage
counterStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct'(直接赋值修改) | 'patch object'($patch传入对象修改) | 'patch function($patch传入函数修改)'
mutation.storeId // 'counter' 创建store时的id
// 仅适用于 mutation.type === 'patch object'
mutation.payload // 就是通过$patch(object)这种方式传入的对象
// 每当它发生变化时,将整个状态持久化到本地存储
localStorage.setItem('counter', JSON.stringify(state))
})
当然你也可以使用watch,但是如果是单纯监听store的state改变,更建议你使用订阅的方式
watch(
demoStore.$state,
(state) => {
// 每当它发生变化时,将整个状态持久化到本地存储
localStorage.setItem('counter', JSON.stringify(state))
},
{ deep: true }
)
核心二:getters
这个相对来说比较简单,如果你使用过vuex,我相信你一定不陌生这个属性,如果你没有使用过也没关系,getters 是幕后的 computed 属性,对于computed你一定很熟悉,getters就是这个效果,就是将state的数据经过加工处理后再返回,下面我一次展示完所有的基础用法
store/demo.js
import { defineStore } from "pinia";
import { useOtherStore } from './other-store' //这是在store目录下定义的另一个store
export const useDemo = defineStore("demo", {
state: () => {
return {
counter: 0,
name: "demo",
users: [],
};
},
getters: {
// 第一种方式 state会当做参数传入
doubleCount(state) {
return state.counter * 2;
},
// 第二种方式 this就是当前的store可以通过this访问当前store中属性
doubleplusOne(): number {
//注意:如果你使用了ts那么返回值类型必须定义,如果你没有ts这里返回类型不需要写
return this.counter * 2 + 1;
},
// 第三种 箭头函数 你必须使用参数的形式,和第一种其实属于一种
// doubleCount: (state) => {
// return state.counter * 2;
// },
// 使用其他的getters
doubleplusTwo(): number {
return this.doubleCount + 2;
},
//传递参数的getters,和computed一样,如果想要getters传入参数就必须返回一个函数,但是该函数的值并不会被缓存
getUserByName(state) {
const activeUsers = state.users.filter((user) => user.active);
return (userId) => activeUsers.find((user) => user.id === userId);
},
//使用其他模块的state 这里我们已经在上面通过import引入了otherStore
otherGetter(state) {
const otherStore = useOtherStore()
return state.counter + otherStore.data
},
},
});
在组件的使用上同state的使用方式一样,都是可以直接通过store的属性访问,这里就不做演示了,但是注意getters是不能够赋值的
核心三:actions
终于来到pinia的第三个核心actions了,actions就相当于组件中的methods,我们可以在actions中定义一些业务处理逻辑,包裹请求接口、修改state等,actions是支持异步的,你可以使用async await操作,具体使用展示如下
import { defineStore } from "pinia";
import { useAuthStore } from "./auth-store";
export const useMain = defineStore("main", {
state: () => {
return {
preferences: [],
counter: 0,
users: [],
};
},
actions: {
// 基础用法一: 修改state中的值
increment() {
this.counter++;
},
// 基础用法二:在actions中使用异步操作
async initUsers() {
this.users = await fetchUsers(); //伪代码 假设我们有一个请求方法
},
// 基础用法三: 在actions中使用别的store
async fetchUserPreferences() {
const auth = useAuthStore(); //可以在当前store中引入别的store
if (auth.isAuthenticated) {
this.preferences = await fetchPreferences(); //伪代码 假设我们有一个请求方法
} else {
throw new Error("User must be authenticated");
}
},
},
});
在组件中使用
<script setup>
import { useMain } from './store/main'
const mainStore = useMain()
</script>
<template>
<h1>{{ mainStore.counter }}</h1>
<button @click="mainStore.increment">+1</button>
</template>
订阅
Acitons的方法
订阅actions可以通过store.$onActions方法,传入一个回调函数,该函数会在action之前执行,如果你想在action之后处理一些操作,可以在after内传入一个回调函数,具体用法如下
const unsubscribe = someStore.$onAction(
({
name, // action 的名字
store, // store 实例
args, // 调用这个 action 的参数
after, // 在这个 action 执行完毕之后,执行这个函数
onError, // 在这个 action 抛出异常的时候,执行这个函数
}) => {
// 如果 action 成功并且完全运行后,after 将触发。
// 它将等待任何返回的 promise
after((result) => {
console.log('action执行成功后返回的promise值',result)
});
// 如果 action 抛出或返回 Promise.reject ,onError 将触发
onError((error) => {
console.log("出错了", error);
});
}
);
// 手动移除订阅
unsubscribe();
默认情况下pinia的所有订阅包裹state的订阅都不需要手动去移除,除非你的业务逻辑需要进行这样的处理,因为订阅默认都绑定到了添加他们组件上,如果组件被销毁那么订阅也会随着销毁,如果你不想这么做,那么在订阅函数的第二个参数中传入true,那么订阅即使在组件销毁后也会依然保持
Plugins插件
到目前pinia的基础使用已经介绍完了,如果你只想了解pinia的基础使用,后面的内容就可以略过了,如果你想涉及一些pinia的进阶内容,那么可以继续向下,首先我们先来介绍一下Plugins插件
Plugins是pinia提供的一个底层api,通过插件我们可以拓展pinia store的功能,具体操作列表官方提供如下
- 向 Store 添加新属性
- 定义 Store 时添加新选项
- 为 Store 添加新方法
- 包装现有方法
- 更改甚至取消操作
- 实现本地存储等副作用
- 仅适用于特定 Store
首先以一个为pinia添加一个全局属性开始
import { createPinia } from 'pinia'
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
pinia.use(SecretPiniaPlugin) //通过use的方式,这样其他store都会获得一个secret属性
// 在另一个文件中
const store = useStore()
store.secret // 'the cake is a lie'
以上这里例子就是给pinia添加了一个全局属性,但是你需要注意属性和状态并不是一个概念,我们这里添加的是全局pinia的属性,pinia的state虽然也是通过store.
的方式访问,但是这里不是一个概念,需要注意区分
下面简单介绍下pinia的插件格式:
pinia的插件是一个函数,可以返回需要添加到store的属性,其内部接受一个context参数
function myPiniaPlugin(context){
context.pinia // 使用 `createPinia()` 创建的 pinia
context.app // 使用 `createApp()` 创建的当前应用程序(仅限 Vue 3)
context.store // 插件正在扩充的 store
context.options // 定义存储的选项对象传递给`defineStore()`
}
下面介绍一些基本的使用方式
// 使用方式一:扩充store-添加全局属性或方法
pinia.use(()=>{key:value}) //伪代码 需要填加的属性方法直接作为对象返回就可以
// 当然你也可以通过另一种方式 (解构context获取store,然后直接在store定义)
pinia.use(({store})=>{
store.key = value
})
// 注意:
// 1. 如果你是想添加外部静态属性,则应该在添加前使用markRaw()包装对象
// 2. 如果你的项目启用了ts,那么你需要使用Typing插件(具体可在官网查找用法,比较简单这里不做展示了)
// 使用方式二: 添加新的状态
// 注意如果你是添加新的状态,那么必须在store和store.$state上同时添加
const globalSecret = ref('secret')
pinia.use(({store})=>{
store.$state.secret = globalSecret
store.secret = globalSecret
})
// 使用方式三: 插件中调用订阅(相当于添加全局订阅事件)
pinia.use(({ store }) => {
store.$subscribe(() => {
// 在存储变化的时候执行
})
store.$onAction(() => {
// 在 action 的时候执行
})
})
还有一种最重要的使用方式,这里单独展示,那就是添加新选项
,添加新选项允许我们在定义store时额外配置选项,比如官网的防抖配置的案例
defineStore('search', {
actions: {
searchContacts() {
// ...
},
},
// 稍后将由插件读取
debounce: {
// 将动作 searchContacts 防抖 300ms
searchContacts: 300,
},
})
然后插件可以读取该自定义选项,然后完成特殊的逻辑处理
// 使用任何防抖库
import debounce from 'lodash/debunce'
pinia.use(({ options, store }) => {
if (options.debounce) {
// 我们正在用新的action覆盖这些action
return Object.keys(options.debounce).reduce((debouncedActions, action) => {
// 这里简单解释一下,以防一些同学迷糊
// 通过keys获取到所在 debounce 配置中的 keys ,而这个keys就是我们在acitons中的方法名,对应的values就是其各自设置的防抖的时间
// 然后调用reduce一次从头遍历所由在debounce配置了防抖的函数名,生成一个新的防抖actions返回,最终新的actions会被添加到当前的store上,从而覆盖掉原来的actions,完成防抖配置
debouncedActions[action] = debounce(
store[action],
options.debounce[action]
)
return debouncedActions
}, {})
}
})
以上就是pinia插件的相关知识点了,当然初次接触后,你可能不知道插件到底在什么样的情况下使用,笔者刚看到这块的时候也很疑惑,正好下面有一个地方就可以使用到这个插件的知识点了,别着急慢慢来
模块化思路
关于pinia的模块化,pinia其实本身就是按模块定义store的,笔者的个人理解就是它是天生自带模块化的,因为它不需要像vuex那样再去声明modules,一个stroe文件就是一个模块
但是一般项目中我们都习惯统一从一个store文件当做仓库的入口,笔者就提供以下两种方式,大家看着选择,也可提供自己的思路
假设现在 src/store 文件下我们定义了 2 个模块 user模块、counter模块
// src/store/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => {
return {
name:"jian",
age: 12
}
},
})
// src/store/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return {
}
},
})
方式一:
// src/store/index.js
import { useUserStore } from './user'
import { useCounterStore } from './counter'
export default function useStore() {
return {
userStore: useUserStore(),
counterStore: useCounterStore(),
}
}
组件中使用
<script setup>
import { storeToRefs } from 'pinia'
import useStore from './store'
const { userStore } = useStore()
const { name, age } = storeToRefs(userStore)
</script>
方式二: 使用Es6的模块化语法引出并导出
// src/store/index.js
export { useUserStore } from './user'
export { useCounterStore } from './counter'
组件中使用
<script setup>
import { useUserStore } from './store'
const userStore = useUserStore()
const { name, age } = storeToRefs(userStore)
</script>
持久化思路
所有的状态管理库无论是,vuex、pinia、redux,都绕不开一个问题,页面刷新后的数据持久化问题,说到持久化存储前端一般都会想到利用sessionstorage和localstorage,那么现在我们就通过localstorage实现pinia的数据持久化
首先我们需要指定pinia中的哪些数据需要被持久化,在哪里指定比较合适呢,很显然是在定义store时比较合适,我们在定义store的时候,就指定哪些数据是要被持久化的
如下案例我们想持久存储用户的token信息,最好的办法是,在定义store的时候,就添加一个配置,指定token需要被持久化
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => {
return {
name:"jian",
age: 12,
token:'xxxxxxx'
}
},
// 持久存储配置
persist:{
enabled: true //是否启用持久存储
strategies: [ // 存储策略 可以指定多个策略
{
key:"user", // 存储的key
storage: localstorage, // 使用什么storage进行存储
paths:["token"] // 具体要存储该store中的哪些值
}
]
}
})
如上述的配置就算是比较详细的了,我们在定义store的时候传入一个控制持久存储的配置persist,在这个配置中我们可以控制是否开启持久存储,和指定具体的存储策略,包括存储哪些值,存储当前store对应的key,以及利用什么进行存储,这里需要注意 storage对象
如果是用户可以指定的话,那我们就要对其进行类型的限制,传入的storage我们不在乎其具体实现,但是要有setItem、getItem方法,因为后续我们需要手动去调用这些方法写入和读取值
现在配置添加好了,下一步就是去如何处理这些配置,之前我们有提到过,pinia给我提供了一种自定义配置的方法,那就是plugins,现在正好我们可以使用这个方法了,这也整是插件的使用场景之一自定义配置
下面我们开始实现这个插件
function persistencePlugin({ options, store }) {
// 判断当前的store有没有配置persist
if (options.persist?.enabled) {
//定义一个默认的存储策略
const defaultStrat = [{
key: store.$id, // 默认存储的key前缀
storage: sessionStorage //默认存储方式用sessionStorage
}]
// 判断用户是否指定了持久化策略,如果指定了就使用用户的否则就使用默认的
const strategies = options.persist?.strategies?.length ? options.persist?.strategies : defaultStrat
strategies.forEach(strategy => {
const storage = strategy.storage || sessionStorage
const storeKey = strategy.key || store.$id
const storageResult = storage.getItem(storeKey)
// 如果我们配置的key,在持久化的storage中有对应的value值,那么就将这个值存入当前store中(刷新也不丢失的核心处理就在这)
if (storageResult) {
store.$patch(JSON.parse(storageResult))
updateStorage(strategy, store)
}
});
// 设置全局订阅事件,如果用户指定了开启持久化,那么当用户修改当前的state时,就按照指定策略更新storage(updateStorage单独抽取封装)
store.$subscribe(() => {
strategies.forEach((strategy) => {
updateStorage(strategy, store)
})
})
}
}
先整理出插件的轮廓后,最后实现更新storage的方法
function updateStorage(strategy, store) {
const storage = strategy.storage || sessionStorage
const storeKey = strategy.key || store.$id
// 默认是全部全部更新持久化,如果用户指定了paths,那么只更新用户指定的
if (strategy.paths) {
const partialState = strategy.paths.reduce((finalObj, key) => {
finalObj[key] = store.$state[key]
return finalObj
}, {})
storage.setItem(storeKey, JSON.stringify(partialState))
} else {
storage.setItem(storeKey, JSON.stringify(store.$state))
}
}
通过以上方法我们就实现了利用插件完成pinia的数据持久化配置,这里只是展示pinia数据持久化的实现思路,当然这方面已经有完善的插件了,在项目中除非其不能满足你的业务需要,否则也没有必要重复造轮子,开发中你可以直接使用,下面就介绍一个pinia的持久化插件
pinia-plugin-persistedstate
安装方式
根据你自己项目选择的包管理工具进行安装
pnpm : pnpm i pinia-plugin-persistedstate
npm : npm i pinia-plugin-persistedstate
yarn : yarn add pinia-plugin-persistedstate
引入
import App from "./App.vue";
import { createApp } from "vue";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createPinia } from "pinia";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate)
createApp(App).use(pinia);
使用
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => {
return {
token: 'xxxx',
userInfo: {
name: 'zh',
},
}
},
// 如果你想当前store的state所有数据都持久化,就直接配置为true
// persist: true,
// 如果你想自定义持久化策略
persist: {
// 自定义在存储中的key值,默认是当前store的id,当前例子如果此处不指定,默认就是main
key: 'customKey',
// 指定存储的storage,默认为localStorage
storage: window.sessionStorage,
// 指定持久化的路径,不传默认存储所有,传 [ ] 则什么都不存储
paths: ['userInfo.name',"token"],
},
})
当然该插件还支持更多的配置功能,如序列化配置、afterRestore钩子等,这些笔者也没有用过,这里简单的提一嘴,有需要的同学可以直接查看pinia-plugin-persistedstate的官方文档,先说好该文档么得中文翻译......
总结
pinia体积更小只有1kb,相较于vuex语法更加简化,对于typescript的支持性更好,更适用于vue3的组合式api风格,如果你项目是vue3+typescript,那么在状态管理库上可以考虑选择pinia(虽然我也没在公司项目中真实落地过,但是demo项目体验下来,真香~)