您现在的位置是:首页 >技术交流 >抛弃Vuex,使用Pinia网站首页技术交流

抛弃Vuex,使用Pinia

LvhaoIT 2024-06-19 13:56:23
简介抛弃Vuex,使用Pinia

Pinia 符合直觉的 Vue.js 状态管理库


在这里插入图片描述

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 类似,我们也可以传入一个带有 stateactionsgetters 属性的 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>

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。