一直以来VueX都是官方为Vue提供的状态管理库,但是在Vue3的版本中,尽管VueX4也能提供组合式API,但是自身存在的很多缺陷让官方重新编写了一个新的库,也就是我们今天要学习的Pinia,结果发现这个新的状态库比起VueX要更加实用,它已经正式成为了Vue的官方推荐,而VueX仍然可以使用,但不会继续维护了,显而易见,Pinia才是未来的主流,所以必须学习它,它的设计思路和VueX非常类似,所以对于已经熟练掌握了VueX的开发者而言,学习Pinia非常轻松!
Pinia和VueX设计思想是一致的,但Pinia要简洁的多,它对于TypeScript的支持非常好,总结一下它们的区别:
Pinia对于Vue2和Vue3是都支持的,我们以Vue3为例来讲解它的使用:
通过命令在Vue3项目中安装Pinia,最新的版本是V2版本。
npm i pinia
然后在入口中使用Pinia
- import { createApp } from 'vue'
- import App from './App.vue'
- import { createPinia } from 'pinia'
- const pinia = createPinia() // 调用方法,创建pinia实例,将pinia引入项目中
- createApp(App).use(pinia).mount('#app')
和VueX类似,使用Pinia同样需要创建store,找到src目录,创建一个store。
通过defineStore方法创建一个store,这个方法接受2个参数,第一个参数是store的唯一id,第二个参数是store的核心配置,这里和VueX几乎一样,但是没有了mutations,而且注意state必须写成箭头函数的形式。defineStore方法返回的是一个函数,通过调用这个返回值函数可以得到具体的store实例。
- // store/index.js 创建store(pinia创建的是小store
- import { defineStore } from 'pinia'
- // 接收函数/方法,用use开头
- const useMainStore = defineStore('main', { // 起名,配置对象
- state: () => {
- return {
- count: 0,
- }
- },
- getters: {},
- actions: {},
- })
- export { useMainStore }
在组件中可以引入store中导出的函数来得到store实例,从而访问其中的状态:
- <template>
- {{ mainStore.count }}
- </template>
- <script setup>
- import { useMainStore } from './store'
- const mainStore = useMainStore()
- </script>
-
- <style></style>
其中,mainStore.$state.count === mainStore.count为true
- import useMainStore from './store'
- const mainStore = useMainStore()
- console.log(mainStore.$state.count === mainStore.count)
在pinia中,如果希望在Store中访问store实例的话,可以通过this
VueX是规定修改状态必须通过mutation实现,Pinia中移除了mutation概念,那应该如何访问和修改状态呢,一起来看看。
首先在state中初始化一些状态:
- const useMainStore = defineStore('main', {
- state: () => {
- return {
- count: 0,
- name: '张三',
- hobby: ['抽烟', '喝酒', '烫头'],
- }
- },
- getters: {},
- actions: {},
- })
然后在组件中来访问和修改状态,如果希望通过解构的方式来访问状态里的数据,可以调用storeToRefs方法来保持数据的响应性。
- <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中数据,vuex要求修改数据必须通过触发Mutation
pinia要求不那么严格,可以直接在组件中修改数据,但不推荐这么做:
- <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>
- const changeStore = () => {
- mainStore.$patch({
- count: mainStore.count + 1,
- name: '李四',
- hobby: [...mainStore.hobby, '敲代码'],
- })
- }
- mainStore.$patch((state) => { //state即是store中的状态
- state.count++
- state.name = '李四'
- state.hobby.push('敲代码')
- })
- //store
- actions: {
- changeState(n) { //action中可以通过this访问到store实例
- this.$patch((state) => {
- state.count+=n
- state.name = '李四'
- state.hobby.push('敲代码')
- })
- },
- },
- //组件中
- mainStore.changeState(10)
- import { defineStore } from 'pinia'
- const useMainStore = defineStore('main', {
- actions: {
- addCount(number) {
- this.count += number
- }
- },
- });
- export default useMainStore
- <template>
- <div>
- <button @click="add">{{ mainStore.count }}</button>
- {{ mainStore.count10 }}
- </div>
- </template>
- <script setup>
- import useMainStore from './store'
- const mainStore = useMainStore()
- function add() {
- mainStore.addCount(10)
- }
- </script>
不能使用解构的写法来在响应式对象中简化掉mainStore.的内容,否则会导致响应性缺失
如果希望简化写法,需要用storeToRefs(mainStore)来包裹mainStore,即可简化不写mainStore.
- <template>
- <div>
- <button @click="add">{{ count }}</button>
- {{ count10 }}
- <p>{{ name }}</p>
- <p v-for="(item, index) in friends" :key="index">{{ item }}</p>
- </div>
- </template>
- <script setup>
- import useMainStore from './store'
- import { storeToRefs } from 'pinia'
- const mainStore = useMainStore()
- const { count, name, friends, count10 } = storeToRefs(mainStore)
- const useMainStore = defineStore('main', { // 起名,配置对象
- state: () => {
- return {
- count: 1,
- name:'peiqi',
- friends: ['qiaozhi','suxi','peideluo']
- }
- },
- actions: {
- addCount(number) {
- // this.count += number
- // this.name = 'xiaozhu'
- // this.friends.push('lingyangfuren')
- // $patch有两种传参形式,一种传参数,一种传函数
- // this.$patch({
- // count: this.count+number,
- // name:'xiaozhu',
- // friends:[...this.friends,"lingyangfuren"]
- // })
- this.$patch((state) => {
- state.count += number
- state.name = 'xiaozhu'
- state.friends.push("lingyangfuren")
- })
- })
- }
- },
- });
Pinia中的getters几乎和VueX的概念一样,可以视作是Pinia的计算属性,具有缓存功能。
- getters: {
- bigCount(state) { //接受的第一个参数即是state
- return state.count + 100
- },
- },
上面写的是类似于vue2的配置选项写法,以下写法为类似于vue3的setup语法糖写法,可以选择自己喜欢的语法
- const useMainStore = defineStore('main', () => {
- let count = ref(1)
- let name = ref('peiqi')
- let friends = ref(['qiaozhi','suxi','peideluo'])
- let count10 = computed(() => {
- return count.value *10
- })
- function addCount() {
- count.value++
- name.value = 'xiaozhu'
- friends.value.push('lingyangfuren')
- }
- function addAsync() {
- setTimeout(() => {
- this.count += 5
- }, 3000);
- }
- return{
- count, name, friends, count10, addCount, addAsync
- }
- })
- const useLoginStore = defineStore('login', {})
- export {useMainStore, useLoginStore}
重新创建一个目录初始化一个Vue3项目,编写2个基本的组件(商品列表组件 购物车结算组件),模拟一个接口数据。
- 商品列表组件
- <template>
- <ul>
- <li>
- 商品名称-商品价格
- <br />
- <button>添加到购物车</button>
- </li>
- <li>
- 商品名称-商品价格
- <br />
- <button>添加到购物车</button>
- </li>
- <li>
- 商品名称-商品价格
- <br />
- <button>添加到购物车</button>
- </li>
- </ul>
- </template>
- 购物车结算组件
- <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>
- 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目录创建一个模拟的数据。
- 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)
- })
- }
创建商品的store,在actions中定义初始化列表的逻辑。
- //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,然后渲染数据:
- <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>
在浏览器中确认页面正确渲染
购物车组件需要创建专属的store,actions中也需要完成添加商品的逻辑,这里也是编码的重点,可以先把大体框架完成。
- 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--
- },
- },
- })
现在完成添加商品的逻辑,它应该包括这些要求:
然后用代码实现:
- 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中的逻辑完成后,在购物车和商品列表组件中渲染视图:
- 购物车
- <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>
完成添加商品的核心逻辑后,再来完成总价计算和结算结果的显示。
总价使用getter计算得来:
- store/shopCart.js
- getters: {
- sum(state) {
- return state.lists.reduce((total, item) => {
- return total + item.price * item.number
- }, 0)
- },
- },
点击结算按钮,发送请求来获得结算结果,在购物车store中定义一个action来处理结算结果:
- 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更加方便。