Skip to content
On this page

Pinia

学习目标

  • 【了解】Pinia基本介绍
  • 【掌握】Pinia快速入门
  • 【掌握】Pinia核心概念
  • 【掌握】Pinia实现购物车案例

一句话概述一下Pinia,可以将它视作VueX的V5版本。

1.Pinia基本介绍

1.1 基本介绍

一直以来VueX都是官方为Vue提供的状态管理库,但是在Vue3的版本中,尽管VueX4也能提供组合式API,但是自身存在的很多缺陷让官方重新编写了一个新的库,也就是我们今天要学习的Pinia,结果发现这个新的状态库比起VueX要更加实用,它已经正式成为了Vue的官方推荐,而VueX仍然可以使用,但不会继续维护了,显而易见,Pinia才是未来的主流,所以必须学习它,它的设计思路和VueX非常类似,所以对于已经熟练掌握了VueX的开发者而言,学习Pinia非常轻松!

1.2 Pinia vs VueX

Pinia和VueX设计思想是一致的,但Pinia要简洁的多,它对于TypeScript的支持非常好,总结一下它们的区别:

  • Pinia没有mutations。修改状态更加简便。
  • 没有模块嵌套,Pinia支持扁平结构,以更好的方式支持store之间的结合。
  • 完美支持typescript。
  • 移除命名空间。Pinia的定义方式内置了命名空间。
  • 无需动态添加stores。

2.Pinia快速入门

Pinia对于Vue2和Vue3是都支持的,我们以Vue3为例来讲解它的使用:

2.1 Pinia安装

通过命令在Vue3项目中安装Pinia,最新的版本是V2版本。

npm i pinia

然后在入口中使用Pinia

js
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
createApp(App).use(pinia).mount('#app')

2.2 快速入门

和VueX类似,使用Pinia同样需要创建store,找到src目录,创建一个store。

通过defineStore方法创建一个store,这个方法接受2个参数,第一个参数是store的唯一id,第二个参数是store的核心配置,这里和VueX几乎一样,但是没有了mutations,而且注意state必须写成箭头函数的形式。defineStore方法返回的是一个函数,通过调用这个返回值函数可以得到具体的store实例。

js
// store/index.js
import { defineStore } from 'pinia' 
const useMainStore = defineStore('main', {
  state: () => {
    return {
      count: 0,
    }
  },
  getters: {},
  actions: {},
})
export { useMainStore }

在组件中可以引入store中导出的函数来得到store实例,从而访问其中的状态:

vue
<template>
    {{ mainStore.count }}
</template>

<script setup>
  import { useMainStore } from './store'
  const mainStore = useMainStore()
</script>

<style></style>

3.Pinia核心概念

3.1 状态修改

VueX是规定修改状态必须通过mutation实现,Pinia中移除了mutation概念,那应该如何访问和修改状态呢,一起来看看。

首先在state中初始化一些状态:

js
const useMainStore = defineStore('main', {
  state: () => {
    return {
      count: 0,
      name: '张三',
      hobby: ['抽烟', '喝酒', '烫头'],
    }
  },
  getters: {},
  actions: {},
})

然后在组件中来访问和修改状态,如果希望通过解构的方式来访问状态里的数据,可以调用storeToRefs方法来保持数据的响应性。

vue
<template>
<p>{{ count }}</p>
<p>{{ name }}</p>
<p>{{ hobby }}</p>
<button>修改数据</button>
</template>

<script setup>
  import { storeToRefs } from 'pinia'
  import { useMainStore } from './store'
  const mainStore = useMainStore()
  const { count, name, hobby } = storeToRefs(mainStore)
</script>

点击button按钮后,如果希望修改store中数据,Pinia提供了以下几种方法:

  • 直接修改 但是这样做从性能层面不够好

    vue
    <template>
      <p>{{ count }}</p>
      <p>{{ name }}</p>
      <p>{{ hobby }}</p>
      <button @click="changeStore">修改数据</button>
    </template>
    
    <script setup>
      import { storeToRefs } from 'pinia'
      import { useMainStore } from './store'
      const mainStore = useMainStore()
      const { count, name, hobby } = storeToRefs(mainStore)
      const changeStore = () => {
        mainStore.count++
        mainStore.name = '李四'
        mainStore.hobby.push('敲代码')
      }
    </script>
  • 通过$patch方法传递一个对象 利用这个方法改变数据可以优化数据更新的性能

    js
    const changeStore = () => {
        mainStore.$patch({
          count: mainStore.count + 1,
          name: '李四',
          hobby: [...mainStore.hobby, '敲代码'],
        })
      }
  • 通过$patch方法传递一个函数 可以优化传递对象的代码

    js
    mainStore.$patch((state) => { //state即是store中的状态
          state.count++
          state.name = '李四'
          state.hobby.push('敲代码')
    })
  • 通过触发action中的方法来修改数据 可以进行参数传递 推荐使用这个方法

    js
    //store
    actions: {
        changeState(n) { //action中可以通过this访问到store实例
          this.$patch((state) => {
            state.count+=n
            state.name = '李四'
            state.hobby.push('敲代码')
          })
        },
      },
    js
    //组件中
    mainStore.changeState(10)

3.2 getters

Pinia中的getters几乎和VueX的概念一样,可以视作是Pinia的计算属性,具有缓存功能。

js
getters: {
   bigCount(state) { //接受的第一个参数即是state
     return state.count + 100
   },
},

4.购物车案例

4.1 案例准备

重新创建一个目录初始化一个Vue3项目,编写2个基本的组件(商品列表组件 购物车结算组件),模拟一个接口数据。

vue
商品列表组件
<template>
  <ul>
    <li>
      商品名称-商品价格
      <br />
      <button>添加到购物车</button>
    </li>
    <li>
      商品名称-商品价格
      <br />
      <button>添加到购物车</button>
    </li>
    <li>
      商品名称-商品价格
      <br />
      <button>添加到购物车</button>
    </li>
  </ul>
</template>
vue
购物车结算组件
<template>
    <div class="cart">
        <h2>购物车</h2>
        <p>
            <i>请添加一些商品到购物车</i>
        </p>
        <ul>
            <li>商品名称-商品价格 * 商品数量</li>
            <li>商品名称-商品价格 * 商品数量</li>
            <li>商品名称-商品价格 * 商品数量</li>
        </ul>
        <p>商品总价:XXX</p>
        <p>
            <button>结算</button>
        </p>
        <p>结算成功/失败</p>
    </div>
</template>
vue
App.vue
<template>
  <h1>Pinia-购物车</h1>
  <hr>
  <h2>商品列表</h2>
  <ProductionList></ProductionList>
  <hr>
  <ShoppingCart></ShoppingCart>
</template>

<script setup >
  import ProductionList from './components/ProductionList.vue'
  import ShoppingCart from './components/ShoppingCart.vue';
</script>

启动项目后可以看到基本的页面结构

准备完毕页面结构后,需要模拟一个接口请求,可以在src目录下新建一个api目录创建一个模拟的数据。

js
api/product.js
const products = [
  {
    id: 1,
    title: 'iphone 13',
    price: 5000,
    inventory: 3,
  },
  {
    id: 2,
    title: 'xiaomi 12',
    price: 3000,
    inventory: 20,
  },
  {
    id: 3,
    title: 'macbook air',
    price: 9900,
    inventory: 8,
  },
]
export const getProducts = async () => {
  await wait(100)
  return products
}
export const buyProducts = async () => {
  await wait(100)
  return Math.random() > 0.5
}
function wait(delay) {
  return new Promise((res) => {
    setTimeout(res, delay)
  })
}

4.2 渲染商品列表

创建商品的store,在actions中定义初始化列表的逻辑。

js
//store/product.js
import { defineStore } from 'pinia'
import { getProducts } from '../api/product.js'
export const useProductStore = defineStore('product', {
  state: () => {
    return {
      lists: [],
    }
  },
  getters: {},
  actions: {
    async getProductLists() {
      const productLists = await getProducts()
      this.lists = productLists
    },
  },
})

在列表组件中触发action,然后渲染数据:

vue
<template>
  <ul>
    <li v-for="item in store.lists" :key="item.id">
      {{ item.title }}-{{ item.price }}
      <br />
      <button>添加到购物车</button>
    </li>
  </ul>
</template>

<script setup>
  import { useProductStore } from '../store/product.js'
  const store = useProductStore()
  store.getProductLists()
</script>

在浏览器中确认页面正确渲染

4.3 购物车添加商品

购物车组件需要创建专属的store,actions中也需要完成添加商品的逻辑,这里也是编码的重点,可以先把大体框架完成。

js
store/shopCart.js
import { defineStore } from 'pinia'

defineStore('shopCart', {
  state: () => {
    return {
      lists: [],
    }
  }, 
  getters: {},
  actions: {
    addProduct(product) {
      //这里书写添加商品的逻辑
    },
    decrement(prodcut) { //减少库存数量
    const res = this.lists.find((item) => item.id === prodcut.id)
    res.inventory--
    },
  },
})

现在完成添加商品的逻辑,它应该包括这些要求:

  • 添加商品时检查是否有库存 如果没有需要终止函数

  • 添加商品时检查购物车列表中是否有该商品

    • 如果有 商品数量+1
    • 如果没有 添加到购物车列表中
  • 添加完成后库存数量-1

然后用代码实现:

js
addProduct(product) {
      //这里书写添加商品的逻辑
      if (product.inventory < 1) {
        return
      }
      const res = this.lists.find((item) => item.id === product.id)
      if (res) {
        res.number++
      } else {
        this.lists.push({
          id: product.id,
          title: product.title,
          price: product.price,
          number: 1,
        })
      }
     const pStore = useProductStore()
      pStore.decrement(product)
    },

store中的逻辑完成后,在购物车和商品列表组件中渲染视图:

vue
购物车
<template>
  <ul>
    <li v-for="item in store1.lists" :key="item.id">
      {{ item.title }}-{{ item.price }}
      <br />
        <!-- 通过库存状态给按钮绑定禁用逻辑 -->
      <button @click="store2.addProduct(item)" :disabled="!item.inventory">
    </li>
  </ul>
</template>

<script setup>
  import { useProductStore } from '../store/product.js'
  import { useShopCartStore } from '../store/shopCart'
  const store1 = useProductStore()
  store1.getProductLists()
  const store2 = useShopCartStore()
</script>

4.4 总价和结算

完成添加商品的核心逻辑后,再来完成总价计算和结算结果的显示。

总价使用getter计算得来:

js
store/shopCart.js
getters: {
    sum(state) {
      return state.lists.reduce((total, item) => {
        return total + item.price * item.number
      }, 0)
    },
  },

点击结算按钮,发送请求来获得结算结果,在购物车store中定义一个action来处理结算结果:

js
store/shopCart.js
state: () => {
    return {
      ...
      result: '',
    }
  },
actions: {
    ...
    async getResult() {
      const res = await buyProducts()
      this.result = res ? '成功' : '失败'
      if (res) { //结算成功后清空购物车列表
        this.lists = []
      }
    },
  },

所有购物车的基本功能都实现了,Pinia帮我们把所有的业务逻辑都提炼到了store中处理,大家有兴趣可以用VueX再来将这个案例完成一次,通过比较你会很快发现Pinia要比VueX更加方便。

版权声明 鄂ICP备2022000216号-2