前言
在Vue.js
的世界里,每一次更新都是一次进化,Vue3
携带着更强大的性能与灵活性翩然而至。而在这场技术盛宴中,Pinia
以一种优雅而革命性的方式,重新定义了状态管理的体验。如果说Vuex
是Vue2
时代的王者,那么Pinia
无疑是Vue3
新时代的璀璨明星,它不仅简化了状态管理的复杂度,还极大地提升了开发者的幸福感。接下来,让我们携手踏上这场探索Pinia魅力的魔法之旅。
如果想了解更全面的vue3,Vue3+TS+Vite+Pinia最全总结>>>
Pinia:Vue3的灵魂伴侣
Vue3
引入了Composition API
,为组件的状态和逻辑提供了更清晰、更模块化的组织方式。Pinia
,作为Vue3
的官方状态管理库,完美地融入了这一新体系,它不仅仅是Vuex
的简单升级,而是在设计理念上的一次革新。
- 直观简洁:告别繁复的
actions
和mutations
,Pinia
提倡直接操作状态,让你的代码更加直观易懂。 TypeScript
友好:原生支持TypeScript
,为你的状态管理提供强类型检查,减少错误,提升开发效率。- 插件生态:尽管年轻,
Pinia
已经拥有丰富的插件生态,无论是持久化状态、调试,还是更复杂的业务需求,都能找到合适的解决方案。
pinia
为什么取代了vuex
官方的说法:
- 取消
mutations
。因为在大部分开发者眼中,mutations
只支持 同步修改状态数据,而actions
虽然支持 异步,却依然要在内部调用mutations
去修改状态,无疑是非常繁琐和多余的。 - 所有的代码都是
TypeScript
编写的,并且所有接口都尽可能的利用了TypeScript
的 类型推断,而不像Vuex
一样需要自定义TS
的包装器来实现对TypeScript
的支持 - 不像
Vuex
一样需要在实例Vue
原型上注入状态依赖,而是通过直接引入状态模块、调用getter/actions
函数来完成状态的更新获取;并且因为自身对TypeScript
的良好支持和类型推断,开发者可以享受很优秀的代码提示 - 不需要预先注册状态数据,默认情况下都是根据代码逻辑自动处理的;并且可以在使用中随时注册新的状态
- 没有
Vuex
的modules
嵌套结构,所有状态都是扁平化管理的。也可以理解为pinia
注册的状态都类似vuex
的module
,只是pinia
不需要统一的入口来注册所有状态模块 - 虽然是扁平化的结构,但是依然支持 每个状态之间的互相引用和嵌套
- 不需要
namespace
命名空间,得利于扁平化结构,每个状态在注册时即使没有声明状态模块名称,pinia
也会默认对它进行处理
总结一下就是:Pinia
在实现Vuex
全局状态共享的功能情况下,改善了状态存储结构,优化了使用的方式,简化API
设计与规范,并且基于TypeScript
的类型推断,为开发者提供了良好的TypeScript
支持与代码提示。
pinia
学习实践
一、安装Pinia
cd
进入项目根目录,执行安装pinia
:
npm install pinia@2.1.4
二、全局中蹙额pinia
创建pinia
实例并传递给应用,注册 pinia
实例到全局,在main.ts
中创建pinia
实例并传递给应用。
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'`在这里插入代码片`
// 导入pinia实例方法
import { createPinia } from 'pinia'
// 创建pinia实例并挂载到应用上
const app = createApp(App).use(createPinia())
app.mount('#app')
三、 创建一个 Store
,定义和修改Store状
态state
Store
(如:Pinia
)是一个保存状态和业务逻辑的实体,不与组件树绑定。它承载着全局状态(整个应用中访问的数据),每个引入它的组件都可以对其进行读写操作。Store
中有三个类似组件的概念:state(data)
、getter(computed)
、action(methods)
。- 需要注意的是,并不是所有的数据都要放到
Store
中处理,像购物车、用户登录状态等这些需要快速处理跨组件之间的状态,使用Store
更加便捷;而像控制元素是否可见这种组件内状态,则不需要Store
了。
通过
defineStore()
方法定义Store(全局状态数据)
,其中参数和返回值说明:
- 参数1:应用中 Store 的唯一标识字符串Id,不可重复。
- 参数2:接受Option对象或 Setup 函数形式。
- 返回值: defineStore() 的返回值进行任意命名,但最好使用 store 的名字,同时以 use 开头且以 Store 结 尾。
- use开头是一个符合组合式函数风格的约定。(比如 useUserStore , useCartStore ,useProductStore )。
四、创建一个简单的购物车数据状态管理文件
/src/stores/cart.ts
import { defineStore } from "pinia";
interface Goods{
id:number;
name:string;
num:number;
}
// 参数二,采用option对象形式
export const useCartStore = defineStore('cart',{
// state用于定义一个返回初始状态的函数
state:()=>{
return {
name:"wuyong",
age:18,
goodsList:[{id:1,name:'手机',num:1}] as Goods[]
}
}
})
五、结构state
状态组件中使用
组件中获取和修改状态数据,解构出来的state
状态,不是响应式的,使用 storeToRefs
方法将状态转成ref
后,解构出来的状态才是响应式的,通过store
实例操作状态是 响应式 的,通过 $patch
可同时修改多个状态属性。
如下表示:
<script setup lang="ts">
import { useCartStore } from './stores/cart';
import { storeToRefs } from 'pinia';
const cartStore = useCartStore();
// 通过store实例获取state状态
console.log("获取state状态", cartStore.$state, cartStore.name);
// 直接结构出来state状态,不是响应式的
//let { age } = cartStore;
// 使用storeToRefs方法将状态转成ref后,结构出来的状态是响应式的
const { age } = storeToRefs(cartStore);
console.log("age", age.value)
//修改state状态值
function handleAddAge() {
// age ++; // 直接解构是`非响应式`的
// age.value++; // 转成ref是`响应式`的
// cartStore.age++;
// 4. 通过 $patch 方法接受一个对象,可同时修改多个状态属性
// cartStore.$patch({
// name: 'wuqi',
// age: cartStore.age+1,
// });
// $patch 方法接受一个带state参数的回调函数,state来操作状态
cartStore.$patch((state) => {
state.name = 'wuqi';
state.age = 24;
state.goodsList.push({ id: state.goodsList.length + 1, name: '电脑'+state.goodsList.length + 1, num: 2 });
})
}
</script>
<template>
<div>
<p>用户名:{{ cartStore.name }}</p>
<p>
年龄:{{ age }}
<button @click="handleAddAge">+1</button>
</p>
<ul>
<li v-for="(item, index) in cartStore.goodsList" :key="index">
商品ID: {{ item.id }},名称:{{ item.name }},数量:{{ item.num }}
</li>
</ul>
<button @click="handleAddAge">年龄+1</button>
</div>
</template>
<style scoped></style>
效果:
六、重置状态:通过调用 store
的 $reset()
方法,将 state
状态重置为初始值
<script setup lang="ts">
import { useCartStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
const cartStore = useCartStore();
function handleResetState() {
// 将 state 状态重置为初始值
cartStore.$reset();
}
</script>
<template>
<div>
<button @click="handleResetState">重置状态</button>
</div>
</template>
<style scoped>
</style>
效果:
Getters
派生属性
Getter 介绍
- 有时候我们需要从 Store 中的 state 中派生出一些状态。例如:基于上面代码,增加一个userType 属性,当 age 值小于18,则 userType 值为 未成年人 ; 大于等 于18 , 则 userType 值为 成年人 。这时我们就需要用到 Getter 为我们解决。
- Getter 相当于组件中的计算属性(computed)。可以通过 defineStore() 中的 getters 属性来定义它们。 推荐使用箭头函数,并且它将接收 state 作为第一个参数。
- Getter中通过返回一个函数,该函数可以接受调用方传递任意参数。
总结:Getter
完全等同于 store
的 state
的计算值。可以通过 defineStore()
中的 getters
属性来定义它们。推荐使用箭头函数,并且它将接收 stat
e 作为第一个参数
Getter 实操作
一、定义状态(state
)、派生属性(getters
),创建一个根据商品ID查找商品的getGoodsById
,然后通过查找函数find
从商品列表中匹配指定ID
的商品并返回。
import { defineStore } from "pinia";
interface Goods{
id:number;
name:string;
num:number;
}
// 参数二,采用option对象形式
export const useCartStore = defineStore('cart',{
// state用于定义一个返回初始状态的函数
state:()=>{
return {
name:"wuyong",
age:18,
goodsList:[{id:1,name:'手机',num:1}] as Goods[]
}
},
//定义派生属性(等同于计算属性,根据state状态值得到新的一个状态值)
getters:{
//接受state作为第一个参数,使用建通函数声明:箭头函数方法不能使用this
userType:(state)=>{
return state.age<18?'未成年人':'成年人'
},
//使用普通函数声明,方法体可以使用this访问整个store实例
getGoodsById(state){
//通过this获取上面getter
console.log("getter普通函数",this.userType);
return (id:number)=>this.goodsList.find(item=>item.id===id)
}
}
})
二、在组件中获取Getter
<p>Getter 派生属性:{{ cartStore.userType }}</p>
<p>向 Getter 派生属性传递参数:{{ cartStore.getGoodsById(1) }}</p>
actions属性
一、actions
介绍
actions
属性扮演着关键角色,它们用于封装业务逻辑,执行异步操作,并且能够修改Store
中的状态。与直接在state
中修改不同,actions
提供了更好的组织结构和可维护性,同时也便于跟踪状态变更的来源。下面是对actions
属性的详细说明:
Action
是一个在Pinia Store
中定义的方法,用于处理包含副作用或异步逻辑的操作。- 它们是
Side Effect(副作用)
的安全场所,比如调用API
、修改状态(通过this.$patch
或直接修改状态对象)、延时操作等。 Action
函数可以返回Promise
,因此支持异步操作,这在处理网络请求时特别有用。- 与
Vue
的组件类似,Actions
内部可以使用Composition API
的函数,如ref、computed
等。
二、如何定义Actions
export const useCartStore = defineStore('cart', {
// ... state 和 getters
actions: {
// 添加商品到购物车的Action
async addToCart(product: Goods) {
// 模拟异步操作,如调用后端API
await new Promise(resolve => setTimeout(resolve, 1000));
// 使用this访问状态并修改
this.goodsList.push({ ...product, num: 1 });
},
// 清空购物车的Action
clearCart() {
this.goodsList = []; // 直接修改状态数组
}
}
});
三、使用Actions
在组件内,通过从Store
中解构出Action
方法,可以直接在组件的方法或生命周期钩子中调用它们。
<script setup>
import { useCartStore } from './cart.store';
const cartStore = useCartStore();
async function onAddToCart(product) {
await cartStore.addToCart(product);
}
function clearCart() {
cartStore.clearCart();
}
</script>
Action购物车使用实例
将商品加入购物车 goodsList 状态中。
import { defineStore } from "pinia";
interface Goods{
id:number;
name:string;
num:number;
}
// 参数二,采用option对象形式
export const useCartStore = defineStore('cart',{
// state用于定义一个返回初始状态的函数
state:()=>{
return {
name:"wuyong",
age:15,
goodsList:[{id:1,name:'手机',num:1}] as Goods[]
}
},
//定义派生属性(等同于计算属性,根据state状态值得到新的一个状态值)
getters:{
//接受state作为第一个参数,使用建通函数声明:箭头函数方法不能使用this
userType:(state)=>{
return state.age<18?'未成年人':'成年人'
},
//使用普通函数声明,方法体可以使用this访问整个store实例
getGoodsById(state){
//通过this获取上面getter
console.log("getter普通函数",this.userType);
return (id:number)=>this.goodsList.find(item=>item.id===id)
}
},
// 定义行为,类似method
actions:{
//新增商品
addGoods(goods:Goods){
if(goods.num){
//将goods.nums的值除1转成数值
goods.num = goods.num/1
//查询购物车中是否存在此商品,存在则数量累加,不存在则追加商品到数组中
const target = this.goodsList.find(item=>item.id === goods.id)
if(target){
target.num+=goods.num
}else{
this.goodsList.push(goods)
}
}
}
}
})
组件中使用
<script setup lang="ts">
import { useCartStore } from './stores/cart';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
//加入购物车逻辑
const goodsId = ref<number>(1)
const goodsName = ref<string>('')
const goodsNum = ref<number>(1)
function handleAddCart() {
const goods = { id: goodsId.value, name: goodsName.value, num: goodsNum.value }
//触发store中的action
cartStore.addGoods(goods)
}
const cartStore = useCartStore();
// 通过store实例获取state状态
console.log("获取state状态", cartStore.$state, cartStore.name);
// 使用storeToRefs方法将状态转成ref后,结构出来的状态是响应式的
const { age } = storeToRefs(cartStore);
console.log("age", age.value)
//将state状态重置为初始值
function handleResetState() {
cartStore.$reset();
}
//修改state状态值
function handleAddAge() {
// $patch 方法接受一个带state参数的回调函数,state来操作状态
cartStore.$patch((state) => {
state.name = 'wuqi';
state.age = ++state.age;
state.goodsList.push({ id: state.goodsList.length + 1, name: '电脑' + state.goodsList.length + 1, num: 2 });
})
cartStore.$subscribe((mutation, state) => {
console.log("mutation, state", mutation, state)
// 获取组件标识id(和 cartStore.$id 一样)
const { storeId } = mutation;
console.log('storeId', storeId); // 'cart'
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('cart', JSON.stringify(state))
})
}
</script>
<template>
<div>
<p>用户名:{{ cartStore.name }}</p>
<p>
年龄:{{ age }}
</p>
<ul>
<li v-for="(item, index) in cartStore.goodsList" :key="index">
商品ID: {{ item.id }},名称:{{ item.name }},数量:{{ item.num }}
</li>
</ul>
<input v-model="goodsId" placeholder="商品ID">
<input v-model="goodsName" placeholder="商品名称">
<input v-model="goodsNum" placeholder="商品数量">
<button @click="handleAddCart">加入购物车</button>
<!-- <p>Getter 派生属性:{{ cartStore.userType }}</p>
<p>向 Getter 派生属性传递参数:{{ cartStore.getGoodsById(1) }}</p> -->
</div>
</template>
<style scoped></style>
最终效果:
注意事项
Action内部应当尽量避免直接修改状态,推荐使用this.$patch或返回一个新的状态对象来更新状态,这样可以更好地利用Vue的响应式系统。
对于异步操作,务必确保正确处理异常,比如使用try-catch块来捕获错误并进行适当处理。
Pinia
的Setup
函数形
一、Pinia
的Setup
函数形式提供了一种更加灵活和现代的方式来组织和管理应用的状态,尤其适合那些喜欢Vue 3
新特性的开发者。
二、使用场景
- 当你偏好使用Vue 3的Composition API风格编码时。
- 当你需要在Store内部使用其他Composition API功能,如watch、onMounted等。
- 当你希望利用Vue的响应式系统来更精细地控制状态变化时。
三、示例代码解释
import {defineStore} from 'pinia'
import {ref,computed} from 'vue'
/**
* defineStore 参数2:采用 Setup 函数形式
* ref() 就是 state 属性
* computed() 就是 getters
* function() 就是 actions
*/
export const useCounterStore = defineStore('counter',()=>{
// 定义store的state属性
const count = ref(0)
// 使用 computed 来定义派生状态(getters)
const doubleCount = computed(()=>{
count.value
})
function addCount(){
count.value++
}
return {count,doubleCount,addCount}
})
组件中调用使用
<script setup lang="ts">
import {useCounterStore} from './stores/counter'
const counterStore = useCounterStore()
const { doubleCount} = cartStore;
</script>
<template>
<h2>Setup Store</h2>
<p>count:{{ doubleCount }}</p>
<button @click="counterStore.addCount">+1</button>
</template>
<style scoped></style>
完结,更多vue3学习总结>>>>