Appearance
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方法传递一个对象 利用这个方法改变数据可以优化数据更新的性能
jsconst changeStore = () => { mainStore.$patch({ count: mainStore.count + 1, name: '李四', hobby: [...mainStore.hobby, '敲代码'], }) }
通过$patch方法传递一个函数 可以优化传递对象的代码
jsmainStore.$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更加方便。