关于 Pinia
可能你不太了解Pinia
是啥,但是VueX
肯定知道,他是vue全家桶
成员之一,目前版本迭代到了VueX 4
。根据当下的形势VueX 5
是不会出来了,因为Vue官方的状态管理库已经更改为了 Pinia
。
Pinia
可以理解为VueX 5
,但是(Pinia舍弃了mutation和module两部分
),既然官方把名字都改了,肯定是做了改动和升级,使用起来更加方便。
Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。
想要管理状态,首先要在项目中,创建一个数据仓库,将需要管理的状态,放在数据仓库中,一般创建的数据仓库名为store
。
安装 Pinia
安装命令:
ts
yarn add pinia
// 或者
npm install pinia
安装完成后,我们需要将pinia挂载到Vue应用中,也就是我们需要创建一个根存储传递给应用程式。我们需要修改main.js,引入pinia提供的cteatePinia方法:
vue3的安装引入
ts
// 基于vue3的安装引入
import { createApp } from 'vue';
import { createPinia } from 'pinia';
//构建
const pinia = createPinia();
const app = createApp(App);
// 将Pinia挂载到Vue应用中
app.use(pinia).mount('#app');
vue2的安装引入
在Vue 2中使用,还需要安装组合 API:
@vue/composition-api
js
import Vue from 'vue'
import vueCompositionApi from '@vue/composition-api'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
import App from './App.vue'
// piniaPersist是Pinia的辅助插件,要挂载到Pinia上,Pinia在挂载到Vue上
const pinia = createPinia()
pinia.use(piniaPersist)
// 注意下面的执行顺序,要先是组合式api,才可挂载Pinia
Vue.use(vueCompositionApi)
Vue.use(pinia)
new Vue({
pinia,
render: h => h(App),
}).$mount('#app')
store 数据仓库
把store
理解为一个公共组件,只不过该公共组件只存放数据,这些数据在其它所有的组件都能够访问或修改。
四个概念,state 、getters 、actions 、persist,
- state用于存放数据的地方
- getters定义计算关系的地方
- actions定义方法的地方
- persist设置数据持久化的属性
1、使用pinia提供的defineStore()
方法来创建一个store数据仓库
ts
// src/store/formInfo.js
import { defineStore } from 'pinia';
// 第一个参数是应用程序中 store 的唯一 id(给创建的store一个名字,方便组件使用)
const useFormInfoStore = defineStore('formInfo', {
// 数据
state: () => {
return {
name:'hello word'
}
},
// 计算
getters: {
},
// 方法
actions: {
setToken (value: string) {
this.name = value
}
},
persist: {
enabled: true,
......
}
})
export default useFormInfoStore;
defineStore接收两个参数:
- name:一个字符串,必传项,该store的唯一id。
- options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。
可以根据项目情况定义任意数量的store存储不同功能模块的数据,一个store就是一个函数,它和Vue3的实现思想也是一致的。
2、使用store
可以在任意组件中引入定义的store
来进行使用,在使用前要进行实例化
ts
<script setup>
// 引入定义
import useFormInfoStore from '@/store/formInfo';
// 调用方法,返回store实例
const formInfoStore = useFormInfoStore();
</script>
store 被实例化后,你就可以直接在 store 上访问 state
、getters
和 actions
中定义的任何属性。
3、解构使用store
store
是一个用reactive
包裹的对象,这意味着不需要在getter 之后写.value
,但是,就像setup
中的props
一样,我们不能对其进行解构 ,如果我们想要提取store中的属性同时保持其响应式的话,我们需要使用Pinia中的storeToRefs()
方法,它将为响应式属性创建refs。
ts
<script setup>
import { storeToRefs } from 'pinia';
// 引入定义
import useFormInfoStore from '@/store/formInfo';
// 调用方法,返回store实例
const formInfoStore = useFormInfoStore();
const { name, age } = formInfoStore; // ❌ 此时解构出来的name和age不具有响应式
const { name, age } = storeToRefs(formInfoStore); // ✅ 此时解构出来的name和age是响应式引用
</script>
状态管理器持久化
状态管理器就是defineStore()
创建的一个store仓库
不持久化带来的问题
在VueX中管理的状态都是静态的,只要页面刷新,使用到该状态值的地方,都会变回初始值。
pinia所管理的数据其实也相当于静态数据,它定义在前端代码中,在页面中某处展示时,如果用户修改获取到的state中的值并刷新页面,发现数据会变回到它最开始定义的状态。
如果想要实现,修改的值代替原来定义的值,并且刷页面也不会再回到定义的状态,就需要开启持久化操作,将数据保存到浏览器中。
持久化的实现方法
Pinia
配套插件pinia-plugin-persist
安装pinia-plugin-persist
ts
yarn add pinia-plugin-persist
// 或者
npm i pinia-plugin-persist
引入pinia-plugin-persist
ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
// 挂载到 pinia 上
const pinia = createPinia()
pinia.use(piniaPersist)
createApp({})
.use(pinia)
.mount('#app')
在文件 tsconfig.ts
中配置
ts
// tsconfig.ts
{
"compilerOptions": {
"types": [
"pinia-plugin-persist"
]
},
}
在创建store数据仓库时,可以配置 persist 属性
,来进行控制存储的状态。
persist中有4个配置属性:
enabled属性:
为true时,对整个store中的state内容进行存储,默认存储方式为sessionStorage
storage属性:
指定存储方式,可以为localStorage或sessionStorage,默认为后者paths属性:
对state 中的字段,按组进行打包储存key属性:
自定义存储的 key,默认是 store.$id
基础用法
ts
persist: {
enabled: true
},
进阶用法,除了 enabled 属性
,都是进阶用法,并且在strategies数组
配置
ts
import { defineStore } from "pinia"
export const useStore = defineStore("YourStore", () => {
const foo = ref("foo")
const bar = ref("bar")
return { foo, bar }
}, {
persist: {
enabled: true,
strategies: [{
// 自定义存储的 key,默认是 store.$id
key: "custom storageKey",
// 可以指定任何 extends Storage 的实例,默认是 sessionStorage
storage: localStorage,
// state 中的字段名,按组打包储存
paths: ["foo", "bar"]
}],
}
})
举个例子:
ts
export const useUserStore = defineStore('storeUser', {
state () {
return {
firstName: 'S',
lastName: 'L',
accessToken: 'xxxxxxxxxxxxx',
}
},
persist: {
enabled: true,
strategies: [
{ storage: sessionStorage, paths: ['firstName', 'lastName'] }, // firstName 和 lastName字段用sessionStorage存储
{ storage: localStorage, paths: ['accessToken'] }, // accessToken字段用 localstorage存储
],
},
})
State
state
为保存store数据库
中数据的容器,defineStore传入的第二个参数options配置项里面,就包括state属性。
ts
// src/store/formInfo.js
import { defineStore } from 'pinia';
const useFormInfoStore = defineStore('formInfo', {
// 推荐使用 完整类型推断的箭头函数
state: () => {
return {
// 所有这些属性都将自动推断其类型
name: 'Hello World',
age: 18,
isStudent: false
}
}
// 还有一种定义state的方式,不太常见,了解即可
// state: () => ({
// name: 'Hello World',
// age: 18,
// isStudent: false
// })
})
export default useFormInfoStore;
默认情况下,您可以通过 store
实例来直接读取和写入状态:
ts
<script setup>
import useFormInfoStore from '@/store/formInfo';
const formInfoStore = useFormInfoStore();
console.log(formInfoStore.name); // 'Hello World'
formInfoStore.age++; // 19
</script>
操作state的方法
pinia还提供了方法,我们使用和操作state:$reset
、$patch
、$state
、$subscribe
- $reset 重置状态,将状态重置成为初始值
- $patch 支持对state对象的部分批量修改
- <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a t e 通过将其 state 通过将其 </math>state通过将其state 属性设置为新对象来替换 Store 的整个状态
- $subscribe 订阅store中的状态变化
ts
<script setup>
import useFormInfoStore from '@/store/formInfo';
const formInfoStore = useFormInfoStore();
console.log(formInfoStore.name); // 'Hello World'
// 直接修改state中的属性
formInfoStore.age++; // 19
// 1.$reset 重置状态,将状态重置成为初始值
formInfoStore.$reset();
console.log(formInfoStore.age); // 18
// 2.$patch 支持对state对象的部分批量修改
formInfoStore.$patch({
name: 'hello Vue',
age: 198
});
// 3.$state 通过将其 $state 属性设置为新对象来替换 Store 的整个状态
formInfoStore.$state = {
name: 'hello Vue3',
age: 100,
gender: '男'
}
// 4.$subscribe 订阅store中的状态变化
formInfoStore.$subscribe((mutation, state) => {
// 监听回调处理
}, {
detached: true // 💡如果在组件的setup中进行订阅,当组件被卸载时,订阅会被删除,通过detached:true可以让订阅保留
})
</script>
注意几种操作方法的问题
- 同直接修改state中的属性不同,通过$patch方法更新多个属性时,在devtools的timeline中,是合并到一个条目中的
- $state在进行替换时,
会更新已有的属性,新增原来state不存在的属性
,针对不在替换范围内的,则保持不变。
- <math xmlns="http://www.w3.org/1998/Math/MathML"> s u b s c r i b e 只会订阅到 p a t c h e s 引起的变化,即上面的通过 subscribe只会订阅到patches引起的变化,即上面的通过 </math>subscribe只会订阅到patches引起的变化,即上面的通过patch方法和$state覆盖时会触发到状态订阅,直接修改state的属性则不会触发。
Getters
pinia中的getters可以完全等同于Store状态的计算属性
,通常在defineStore中的getters属性中定义。同时支持组合多个getter,此时通过this
访问其他getter。
ts
import { defineStore } from 'pinia';
const useFormInfoStore = defineStore('formInfo', {
state: () => {
return {
name: 'Hello World',
age: 18,
isStudent: false,
gender: '男'
};
},
getters: {
// 仅依赖state,通过箭头函数方式
isMan: (state) => {
return state.gender === '男';
},
isWoman() {
// 通过this访问其他getter,此时不可以用箭头函数
return !this.isMan;
}
}
});
export default useFormInfoStore;
在使用时,我们可以直接在store实例上面访问getter:
ts
<template>
<div>The person is Man: {{ formInfoStore.isMan }} or is Woman: {{ formInfoStore.isWoman }}</div>
</tempalte>
<script setup>
import useFormInfoStore from '@/store/formInfo';
const formInfoStore = useFormInfoStore();
</script>
getter进阶用法
通常getter是不支持额外传参的,但是我们可以从getter返回一个函数的方式来接收参数:
ts
import { defineStore } from 'pinia';
const useFormInfoStore = defineStore('formInfo', {
state: () => {
return {
name: 'Hello World',
age: 18,
isStudent: false,
gender: '男'
};
},
getters: {
isLargeBySpecialAge: (state) => {
return (age) => {
return state.age > age
}
}
}
});
export default useFormInfoStore;
在组件中使用时即可传入对应参数,注意,在这种方式时,getter不再具有缓存性。
ts
<template>
<div>The person is larger than 18 years old? {{ formInfoStore.isLargeBySpecialAge(18) }}</div>
</tempalte>
<script setup>
import useFormInfoStore from '@/store/formInfo';
const formInfoStore = useFormInfoStore();
</script>
Actions
定义在defineStore中的actions属性内,常用于定义业务逻辑使用。action可以是异步的,可以在其中异步调用 任何 API 甚至其他操作。
ts
import { defineStore } from 'pinia';
const useFormInfoStore = defineStore('formInfo', {
state: () => {
return {
name: 'Hello World',
age: 18,
isStudent: false,
gender: '男'
};
},
getters: {
isMan: (state) => {
return state.gender === '男';
},
isWoman() {
return !this.isMan;
}
},
actions: {
incrementAge() {
this.age++;
},
async registerUser() {
try {
const res = await api.post({
name: this.name,
age: this.age,
isStudent: this.isStudent,
gender: this.gender
});
showTips('用户注册成功~');
} catch (e) {
showError('用户注册失败!');
}
}
}
});
export default useFormInfoStore;
使用
ts
<script setup>
import useFormInfoStore from '@/store/formInfo';
const formInfoStore = useFormInfoStore();
const registerUser = () => {
formInfoStore.registerUser();
}
</script>
$onAction()方法
使用 store.$onAction()
订阅 action 及其结果。传递给它的回调在 action 之前执行。 after
处理 Promise 并允许您在 action 完成后执行函数,onError
允许您在处理中抛出错误。
ts
const unsubscribe = formInfoStore.$onAction(
({
name, // action 的名字
store, // store 实例
args, // 调用这个 action 的参数
after, // 在这个 action 执行完毕之后,执行这个函数
onError, // 在这个 action 抛出异常的时候,执行这个函数
}) => {
// 记录开始的时间变量
const startTime = Date.now()
// 这将在 `store` 上的操作执行之前触发
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// 如果 action 成功并且完全运行后,after 将触发。
// 它将等待任何返回的 promise
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})
// 如果 action 抛出或返回 Promise.reject ,onError 将触发
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
// 手动移除订阅
unsubscribe()
和$subscribe类似,在组件中使用时,组件卸载,订阅也会被删除,如果希望保留的话,需要传入true作为第二个参数。
ts
<script setup>
import useFormInfoStore from '@/store/formInfo';
const formInfoStore = useFormInfoStore();
formInfoStore.$onAction(callback, true);
</script>
纯composition api方法,应用Pinia
举个例子:
ts
import { defineStore } from "pinia";
import { ref, computed } from 'vue'
export const useSettingStore = defineStore('setting', ()=> {
const name = ref('大哥')
const age = ref(18)
const NameDescribe = computed(() => `她的名字为${name.value}`)
function changeSettingAge() {
age.value = age.value == 18 ? 20 : 18
}
function setName(e: string) {
name.value = e
}
// 传递的函数要 return 返回出来才可以使用
return { name, age, NameDescribe, changeSettingAge, setName }
})
// 使用
// home.vue
<el-button @click="changeSetting">{{
settingStore.NameDescribe
}}</el-button>
import { useSettingStore } from "@/stores/setting";
const settingStore = useSettingStore();
const changeSetting = () => {
settingStore.changeSettingAge();
settingStore.setName("二弟");
};
// 另一种使用方法
<el-button @click="changeSetting">{{
titleDescribe
}}</el-button>
import { storeToRefs } from "pinia";
import { useSettingStore } from "@/stores/setting";
const { name, age, NameDescribe } = storeToRefs(useSettingStore())
const { changeSettingAge, setName } = useSettingStore()
const changeSetting = () => {
changeSettingAge();
setNmae("二弟");
console.log(name.value)
};
Pinia 与 Vuex 对比
VueX 的基本思想借鉴了Flux。Flux 是 Facebook 在构建大型 Web 应用程序时为了解决数据一致性问题而设计出的一种架构,它是一种描述状态管理的设计模式。绝大多数前端领域的状态管理工具都遵循这种架构,或者以它为参考原型。Vuex在它的基础上进行了一些设计上的优化:
Pinia舍弃了mutation和module两部分,这样我们在使用时就更加的便捷。