Pinia 符合直觉的 Vue.js 状态管理库
文章目录
- Pinia 符合直觉的 Vue.js 状态管理库
- 1.简介
- 2.为什么要使用Pinia
- 3.安装
- 3.1 挂载pinia
- 4.创建一个store容器
- 4.1 Option 参数
- 4.2 Setup 参数
- 5.三个重要概念
- 5.1 State
- 5.2 Getter
- **5.3 Action**
- 6.购物车实例
- 6.1 商品列表组件
1.简介
官网
Pinia 起始于 2019 年 11 月左右的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。从那时起,我们就倾向于同时支持 Vue 2 和 Vue 3,并且不强制要求开发者使用组合式 API,我们的初心至今没有改变。除了安装和 SSR 两章之外,其余章节中提到的 API 均支持 Vue 2 和 Vue 3。虽然本文档主要是面向 Vue 3 的用户,但在必要时会标注出 Vue 2 的内容,因此 Vue 2 和 Vue 3 的用户都可以阅读本文档。
2.为什么要使用Pinia
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。如果你熟悉组合式 API 的话,你可能会认为可以通过一行简单的 export const state = reactive({})
来共享一个全局状态。对于单页应用来说确实可以,但如果应用在服务器端渲染,这可能会使你的应用暴露出一些安全漏洞。 而如果使用 Pinia,即使在小型单页应用中,你也可以获得如下功能:
- Devtools 支持
- 追踪 actions、mutations 的时间线
- 在组件中展示它们所用到的 Store
- 让调试更容易的 Time travel
- 热更新
- 不必重载页面即可修改 Store
- 开发时可保持当前的 State
- 插件:可通过插件扩展 Pinia 功能
- 为 JS 开发者提供适当的 TypeScript 支持以及自动补全功能。
- 支持服务端渲染
3.安装
yarn add pinia
# 或者使用 npm
npm install pinia
3.1 挂载pinia
在main.ts中挂载,使用createPinia()
创建pinia实例
import { createApp } from 'vue'
import './style.css'
import App from "./App.vue";
import {createPinia} from "pinia";
const pinia = createPinia()
let app = createApp(App);
app.use(pinia)
app.mount('#app')
4.创建一个store容器
Store 是用 defineStore()
定义的,它的第一个参数要求是一个独一无二的名字:
import { defineStore } from 'pinia'
// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useAlertsStore = defineStore('alerts', {
// 其他配置...
})
这个名字 ,也被用作 id ,是必须传入的, Pinia 将用它来连接 store 和 devtools。为了养成习惯性的用法,将返回的函数命名为 use… 是一个符合组合式函数风格的约定。
defineStore()
的第二个参数可接受两类值:Setup 函数或 Option 对象。
4.1 Option 参数
与 Vue 的选项式 API 类似,我们也可以传入一个带有 state
、actions
与 getters
属性的 Option 对象
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
你可以认为 state
是 store 的数据 (data
),getters
是 store 的计算属性 (computed
),而 actions
则是方法 (methods
)。
4.2 Setup 参数
也存在另一种定义 store 的可用语法。与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
在 Setup Store 中:
ref()
就是state
属性computed()
就是getters
function()
就是actions
Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR 变得更加复杂。
5.三个重要概念
5.1 State
在大多数情况下,state 都是你的 store 的核心。人们通常会先定义能代表他们 APP 的 state。在 Pinia 中,state 被定义为一个返回初始状态的函数。这使得 Pinia 可以同时支持服务端和客户端。
import { defineStore } from 'pinia'
const useStore = defineStore('storeId', {
// 为了完整类型推理,推荐使用箭头函数
state: () => {
return {
// 所有这些属性都将自动推断出它们的类型
count: 0,
name: 'Eduardo',
isAdmin: true,
items: [],
hasChanged: true,
}
},
})
使用state
默认情况下,你可以通过 store
实例访问 state,直接对其进行读写。
const mainStore = useMainStore()
mainStore.sum++
重置state
可以通过调用 store 的 $reset()
方法将 state 重置为初始值。
const store = useStore()
store.$reset()
变更state
除了用 store.count++
直接改变 store,你还可以调用 $patch
方法。它允许你用一个 state
的补丁对象在同一时间更改多个属性:
mainStore.$patch({
sum:mainStore.sum+1,
count:mainStore.count+1,
})
不过,用这种语法的话,有些变更真的很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice
操作)都需要你创建一个新的集合。因此,$patch
方法也接受一个函数来组合这种难以用补丁对象实现的变更。
mainStore.$patch(state => {
state.sum += 1;
state.count += 1
})
两种变更 store 方法的主要区别是,$patch()
允许你将多个变更归入 devtools 的同一个条目中。
5.2 Getter
Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore()
中的 getters
属性来定义它们。推荐使用箭头函数,并且它将接收 state
作为第一个参数:
export const useMainStore = defineStore("main", {
state: () => {
return {
sum: 1,
count: 2
}
},
/**
* 类似与组件的computed
* 具有缓存功能,当里面的值没有变化时,多次调用也只会执行一次
*/
getters: {
// 自动推断出返回类型是一个 number
douberSum(state) {
return 2 * state.sum;
},
// 返回类型**必须**明确设置
douberSumT():number{
// 整个 store 的 自动补全和类型标注 ✨
return 2* this.sum;
}
},
})
然后你可以直接访问 store 实例上的 getter 了:
<template>
<p>Double count is {{ store.douberSum }}</p>
</template>
<script setup>
import { useCounterStore } from './counterStore'
const store = useCounterStore()
</script>
Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:
export const useStore = defineStore('main', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})
并在组件中使用:
<script setup>
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 `getUserById.value` 来访问
// <script setup> 中的函数
</script>
<template>
<p>User 2: {{ getUserById(2) }}</p>
</template>
请注意,当这样做时,getter 将不再被缓存,它们只是一个被你调用的函数。不过,可以在 getter 本身中缓存一些结果,虽然这种做法并不常见,但有证明表明它的性能会更好
访问其他 store 的 getter
想要使用另一个 store 的 getter 的话,那就直接在 getter 内使用就好:
import { useOtherStore } from './other-store'
export const useStore = defineStore('main', {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
5.3 Action
Action 相当于组件中的 method。它们可以通过 defineStore()
中的 actions
属性来定义,并且它们也是定义业务逻辑的最好选择。
import {defineStore} from "pinia";
export const useMainStore = defineStore("main", {
state: () => {
return {
sum: 1,
count: 2
}
},
actions: {
changeState(number: number) {
this.sum += number;
this.count += number;
}
}
})
类似 getter,action 也可通过 this
访问整个 store 实例,并支持完整的类型标注(以及自动补全✨)。不同的是,action
可以是异步的,你可以在它们里面 await
调用任何 API,以及其他 action!
actions: {
async loadAllProduct() {
this.all = await getProducts();
},
decrementProduct(product: IProduct) {
const ret = this.all.find(item => item.id == product.id)
if (ret) {
ret.inventory--
}
}
}
访问其他 store 的 action
想要使用另一个 store 的话,那你直接在 action 中调用就好了:
actions: {
addProductToCart(product:IProduct){
console.log(product)
//减库存
const shopStore = useShopStore();
shopStore.decrementProduct(product)
},
}
6.购物车实例
本次使用的是TS语法
定义模拟数据
export interface IProduct {
id: number,
title: string,
price: number,
inventory: number //库存
}
const products: IProduct[] = [
{id: 1, title: "ipad 4 mini", price: 3250.5, inventory: 2},
{id: 2, title: "iphone 14 pro max", price: 9899, inventory: 1},
{id: 3, title: "macbook pro", price: 16000, inventory: 3},
]
//定义请求方法
export const getProducts = async () => {
await wait(100)
return products
}
async function wait(delay: number) {
return new Promise((resolve) => setTimeout(resolve, delay))
}
export const buyProducts = async (totalPrice: number) => {
console.log('结账金额:'+totalPrice)
await wait(100);
return Math.random() > 0.5
}
6.1 商品列表组件
useShopStore
import {defineStore} from "pinia";
import {getProducts, IProduct} from "../request/shop.ts";
export const useShopStore = defineStore("shop", {
state: () => {
return {
all: [] as IProduct[]
}
},
getters: {},
actions: {
async loadAllProduct() {
this.all = await getProducts();
},
decrementProduct(product: IProduct) {
const ret = this.all.find(item => item.id == product.id)
if (ret) {
ret.inventory--
}
}
}
})
ShopComp.vue
<template>
<h2>商品列表</h2>
<ul>
<li v-for="(item,index) in all" :key="index">
<h4>{{ item.title + ' - ¥' + item.price + ' 剩余数量:' + item.inventory }}</h4>
<button :disabled="item.inventory<=0" @click="addCar(item)">添加到购物车</button>
<br>
</li>
</ul>
</template>
<script setup lang="ts">
import {useShopStore} from "../../store/shop.ts";
import {useCarStore} from "../../store/car.ts";
import {IProduct} from "../../request/shop.ts";
import {storeToRefs} from "pinia";
const shopStore = useShopStore();
//获取所有数据
shopStore.loadAllProduct();
const {all} = storeToRefs(shopStore);
const carStore = useCarStore();
const addCar = (product: IProduct) => {
carStore.addProductToCart(product);
}
</script>
<style scoped>
* {
margin: 0;
padding: 0;
}
</style>
6.2 购物车列表组件
useCartStore
import {defineStore} from "pinia";
import {buyProducts, IProduct} from "../request/shop.ts";
import {useShopStore} from "./shop.ts";
type CartProduct = {
quantity:number//数量
}&IProduct
export const useCarStore = defineStore("car", {
state() {
return {
cartProducts: [] as CartProduct[]//购物车列表
}
},
getters: {
totalPrice():number{
return this.cartProducts.reduce((total,item)=>{
console.log(total,item,item.price,item.quantity)
return total+item.price*item.quantity
},0)
}
},
actions: {
addProductToCart(product:IProduct){
console.log(product)
//判断是否还有库存
if(product.inventory<1){
alert("已经没有库存了")
return
}
//有库存则将数据保存进去
//检查购物车是否已经存在该商品
const cartItem = this.cartProducts.find(item=>item.id===product.id);
if (cartItem){
cartItem.quantity+=1
}else {
this.cartProducts.push({...product,quantity:1})
}
//减库存
const shopStore = useShopStore();
shopStore.decrementProduct(product)
},
async settlementCart(){
let data = await buyProducts(this.totalPrice)
if (data){
this.cartProducts=[];
}
return data
}
}
})
CartComp.vue
<template>
<h2>你的购物车</h2>
<h5>请添加一些商品到购物车</h5>
<ul>
<li v-for="(item,index) in cartProducts" >
<h5>{{item.title +" - "+item.price+" * "+item.quantity}}</h5>
</li>
</ul>
<h5>商品总价:{{ '¥ '+totalPrice }}</h5>
<button @click="settlement">结算</button>
<h6 v-show="showFlag">{{msg}}</h6>
</template>
<script setup lang="ts">
import {useCarStore} from "../../store/car.ts";
import {storeToRefs} from "pinia";
import {ref} from "vue";
const carStore = useCarStore();
const {cartProducts,totalPrice } = storeToRefs(carStore)
let msg = ref("结算成功")
let showFlag = ref(false)
const settlement=()=>{
let res = carStore.settlementCart();
if(res){
msg=ref("结算成功")
showFlag.value=true;
setTimeout(()=>{
showFlag.value=false;
},1000)
}else {
msg=ref("结算失败")
showFlag.value=true;
setTimeout(()=>{
showFlag.value=false;
},1000)
}
}
</script>