基本概念
Pinia 是一个专为 Vue.js 设计的状态管理库,特别是针对 Vue 3 进行了优化,完美支持Vue3的Composition api 以及TypesCcript语法。它提供了一种更加简单、直观且可扩展的方式来组织和访问应用程序的状态。目前Pinia已经取代Vuex成为vue官方文档生态的一部分。
与vuex相比,Pinia主要有以下优点:
- Pinia 的 API 采用Vue 3的Composition API风格,设计更加简洁明了,易于理解和使用,状态管理更加直观。
- Pinia构建得更加轻量级,其体积比Vuex小,这有助于减少应用程序的加载时间和提高性能。
- Pinia 支持 TypeScript,提供了类型安全的 API ,有助于在开发过程中捕获错误和进行静态类型检查。
- Pinia支持创建多个store全局实例,每个store都可以视为一个独立的状态管理模块。
- Pinia 不需要嵌套模块,符合Vue3的Composition api ,让代码更加扁平化。
官方文档:Pinia | The intuitive store for Vue.js (vuejs.org)
使用步骤
1.安装Pinia
npm install pinia
2.main.js中引入pinia
import { createApp } from "vue";
import { createPinia } from "pinia"; //引入pinia
import App from "./App.vue";
const pinia = createPinia(); //创建pinia实例
const app = createApp(App);
app.use(pinia); //挂载实例
app.mount("#app");
3.创建仓库Option Store:store.js文件(后面会介绍Setup Store)
import { defineStore } from "pinia";
export const useMainStore = defineStore("main", {
state: () => ({}),
getters: {},
actions: {},
});
- defineStore的第一个参数:是容器的唯一标识,不能重复
- defineStore的第二个参数:是对容器仓库的配置说明(对象)
- state 属性: 存储全局状态数据data
- getters属性:计算属性,有缓存机制
- actions属性: 对state里数据变化的业务逻辑,简单说就是修改state全局状态数据
4.在组件中使用:需要引入和实例化
虽然我们前面定义了一个 store,但在我们使用
<script setup>
调用useStore()
(或者使用setup()
函数,像所有的组件那样) 之前,store 实例是不会被创建的:
<script setup>
import { useMainStore } from "../stores/index";//引入
const store= useMainStore();//获取实例
// 可以在组件中的任意位置访问 `store` 变量 ✨
console.log(store);
</script>
核心概念
1. state
1).定义state
在大多数情况下,state 都是 store 的核心。在 Pinia 中,state 被定义为一个返回初始状态的函数。这使得 Pinia 可以同时支持服务端和客户端。
//index.js
export const useMainStore = defineStore("main", {
state: () => ({
name: "Jack",
age: 18,
})
});
2).访问state数据
//组件script中
import { useMainStore } from "../stores/index";//引入
const store = useMainStore();
//方式一:
console.log(store.name,store.age);//输出 Jack 18
//方式二:
const { name, age} = store //解构构赋值
console.log(name,age);//输出Jack 18
//template模板中
{{store.name + store.age}}或解构的{{name + age}}
注意:虽然在数据较多的时候,方式二解构赋值相较于store.[‘变量名’]的方式更加简洁明了。但使用这种解构赋值法会丢失state数据的响应性。因为store
是一个用reactive
包裹的对象。
解决方式是:使用官方提供的storeToRefs()方法。它将为每一个响应式属性创建引用。
import { storeToRefs } from "pinia"//引入
const { name, age} = storeToRefs(store)//使用
补充:其实在Vuex中,直接解构数据也是不可以的。
3).修改state数据
方式一:直接修改,通过对store.[属性],对访问的属性重新赋值
function changeState(){
store.name = '小明'
store.age = '666'
}
方式二:$patch方法修改,它允许你用一个 state
的补丁对象在同一时间更改多个属性(修改多条数据时,性能更高)
function changeState(){
store.$patch({
name: "王小明",
age: 666,
});
}
不过,用这种语法的话,有些变更真的很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice
操作)都需要创建一个新的集合。因此,$patch
方法也接受一个函数来组合这种难以用补丁对象实现的变更。
//index.js
export const useMainStore = defineStore("main", {
state: () => ({
items: [],
hasChanged: false,
}),
});
//组件内 修改
function changeState(){
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
}
4).重置state
整个state的属性值都会变成定义时的初始值
function resetState(){
store.$reset()
}
2. getter
1).定义getter
Getter 完全等同于 store 的 state 的计算值。推荐使用箭头函数,并且它将接收 state
作为第一个参数:
export const useMainStore = defineStore("main", {
state: () => ({
name: "Jack",
age: 18,
}),
getters: {
doubleAge: (state) => state.age * 2,
//在getter中 使用另一个getter this指向当前存储库
addOneAge() {
return this.doubleAge + 1;
},
//或使用箭头函数但要将getters作为参数传入。(this指向问题)
addTowAge: (getters) => getters.doubleAge + 2,
}
});
2).使用getter
//组件script中
import { useMainStore } from "../stores/index";
const store = useMainStore();
//方式一:
console.log(store.doubleAge);
//方式二:解构赋值
import { storeToRefs } from "pinia"
const { doubleAge, addOneAge , addTowAge} = storeToRefs(store)
//.value访问计算属性的值
console.log(doubleAge.value,addOneAge.value, addTowAge.value);
//temlate模板中
<p>{{store.doubleAge}}</p>
<p>{{store.addOneAge}}</p>
<p>{{store.addTowAge}}</p>
getter相当于state的计算属性,具有缓存机制。它的值会基于state依赖被缓存。仅会在state依赖被更新时才重新计算。只要依赖的state数据不变,无论访问多少次计算结果,它都会立即返回先前的计算结果,而不用重复执行 getter 函数。
3. action
1).定义action
Action 相当于组件中的 method。它们是定义业务逻辑的完美选择。类似 getter,action 也可通过 this 访问整个 store 实例。不同的是,action 可以是异步的,你可以在它里面 await 调用任何 API,以及其他 action!
export const useCounterStore = defineStore("main", {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++;
},
//模拟异步请求函数
randomizeCounter() {//1 s后但会一个值未随机数字的promise对象
return new Promise((resolve, reject) => {
setTimeout(() => {
let number = Math.round(100 * Math.random());
resolve(number);
}, 1000);
});
},
//异步操作
async changeCount() {
const res = await this.randomizeCounter();//解析promise
this.count = res;
//调用其他action
this.increment();
},
},
});
2).使用action
//script中
const store = useCounterStore();
//方式一:
// 将 action 作为 store 的方法进行调用
store.changeCount()
//方式二:
//解构赋值,直接使用
const { changeCount , increment }= store
changeCount()
increment()
//template模板中
<button @click="store.changeCount()">changeCount</button>
注意:作为 action 的 方法可以直接解构
其他补充
1. Setup Store
这是另一种定义 store 的语法。与 Vue 组合式 API 的 setup 函数相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。
import { defineStore } from "pinia";
import { ref } from "vue";
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
});
在 Setup Store 中:ref() 就是 state 属性,computed() 就是 getters,function() 就是 actions
注意,要让 pinia 正确识别
state
,你必须在 setup store 中返回state
的所有属性。这意味着,你不能在 store 中使用私有属性。不完整返回会影响 SSR ,开发工具和其他插件的正常运行。相比于上面介绍的Option Store,Setup store拥有更高的灵活性,因为你可以在store内创建侦听器,并自由地使用任何组合式函数。
Setup store 也可以依赖于全局提供的属性,比如路由。任何应用层面提供的属性都可以在 store 中使用 inject() 访问,就像在组件中一样:
import { inject } from 'vue'
import { useRoute } from 'vue-router'
import { defineStore } from 'pinia'
export const useSearchFilters = defineStore('search-filters', () => {
const route = useRoute()
// 这里假定 `app.provide('appProvided', 'value')` 已经调用过
const appProvided = inject('appProvided')
// ...
return {
// ...
}
})
总之,两种语法都有各自的优势和劣势,Option Store更容易使用,而Setup Store更灵活和强大。
2. pinia持久化存储
pinia
和 vuex
一样,数据是短时的,只要一刷新页面,数据就会恢复成初始状态,为了避免这个问题,可以对其采用持久化保存方法。
持久化保存的原理是在 pinia
中数据更新时,同步保存到 localStorage
或 sessionStorage
中,刷新后从本地存储中读取数据。
1).安装插件
官方文档:开始使用 |Pinia 插件持续存在
npm i pinia-plugin-persistedstate
2).引入使用插件:main.js文件
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia"; //引入pinia
import persist from 'pinia-plugin-persistedstate'//引入插件
const pinia = createPinia(); //创建pinia实例
pinia.use(persist)//使用插件
const app = createApp(App);
app.use(pinia); //挂载pinia到vue实例
app.mount("#app");
3).使用插件
方式一:默认保存,将当前模块中的所有数据都进行持久化存储,保存在localStorage 或sessionStorage 中,刷新页面不需要手动读取数据,插件会自动读取。
//setup store
defineStore(
"counter",
() => {
return { };
},
{ persist: true }// true 表示开启持久化保存
);
//option store
defineStore("main", {
state: () => ({}),
actions: {},
persist: true,
});
方式二:传递一个对象给 Store 的 persist
属性来配置持久化。
persist: {
key: "counterStore", //存储名称
storage: sessionStorage, // 存储方式
paths: ["count"],
//paths指定 state 中哪些数据需要被持久化。
//[] 表示不持久化任何状态,undefined 或 null 表示持久化整个 state
},
效果如图:
3. 封装pinia
封装的目的是pinia独立维护,减少main.js文件的冗余度。同时因为Pinia支持创建多个store全局实例,封装pinia可更加方便统一管理这些仓库。举例说明:
1).创建仓库userStore:user.js文件
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => ({
name: "Jack",
age: 18,
}),
getters: {
doubleAge: (state) => state.age * 2,
},
actions: {
increment() {
this.age++;
},
},
persist: true,
});
2).创建仓库counterStore:counter.js文件
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useCounterStore = defineStore(
"counter",
() => {
//数据state
const count = ref(1);
//getter
const doubleCount = computed(() => count.value * 2);
//action
function increment() {
this.count++;
}
return {
count,
doubleCount,
increment,
};
},
{
persist: true,
}
);
3).封装pinia
import { createPinia } from "pinia"; //引入pinia
import persist from "pinia-plugin-persistedstate"; //引入持久化插件
const pinia = createPinia(); //创建pinia实例
pinia.use(persist); //使用插件
export default pinia; //导出pinia用于main.js注册
//统一导出,可以控制各个仓库是否可用
// import { useUserStore } from '@/stores/modules/user.js'
// export { useUserStore }
// import { useCounterStore } from '@/stores/modules/counter.js'
// export { useCounterStore }
// 简写
export * from "./modules/user";
export * from "./modules/counter";
4).在mian.js中注册pinia
import { createApp } from "vue";
import App from "./App.vue";
import pinia from "./stores/index";
const app = createApp(App);
app.use(pinia); //挂载pinia到vue实例
app.mount("#app");
文件结构:
至此pinia封装完成,由于pinia具有较强的可扩展性。如果需要添加其他插件扩展便可一并放入index.js文件独立维护。参考官方文档:插件 | Pinia (vuejs.org)
5).在组件中使用:只需要从index.js文件中导入,减少代码复杂度
import { useCounterStore } from '../stores/index'
const counterStore = useCounterStore()
若有错误或描述不当的地方,烦请评论或私信指正,万分感谢 😃