六维教程

Vue3 状态管理入门

前面学过用 props 传、用 provide/inject 注入、用组合式函数复用逻辑,但有个场景它们都不顺手,就是购物车

购物车页要加减商品,顶部导航栏要显示总数,结算页要读出清单。

  • 这三个组件分属不同路由页面,互相不是父子,props 传不过去。
  • 组合式函数每次调用是独立副本,购物车页改的数量,导航栏那份看不到,也共享不了。
  • provide/inject 也能传,在根组件 provide 一份,所有页面都是它的后代,都能 inject 到,配 TypeScript 能约束类型,连修改函数也可以和数据一起 provide(Vue3 依赖注入 的读写分离写法),小项目这么写够用,但逻辑靠各组件自觉组织,项目一大,谁 provide 的、改动从哪来,只能靠约定撑着,没有统一结构,也没有工具能看能查。

这份”所有页面共同需要、且谁都能改”的数据,需要更正经的全局方案,这种情况交给全局状态管理,Vue 生态的现代方案是 Pinia,它上手简单、类型友好、比老的方案轻量得多,是 Vue 官方推荐的默认选择。

安装与接入全局

npm install pinia

main.js 中创建并挂载:

// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()

createApp(App).use(pinia).mount('#app')

定义第一个 Store

先别管 Store 这个新名词,写购物车逻辑你早就会了,Vue3 组合式函数 教过,把 ref、computed、函数装进一个函数再 return,开头的购物车照这个套路写,清单是 ref、总数是 computed、加减是函数:

// src/composables/useCart.js
import { ref, computed } from 'vue'

export function useCart() {
  const items = ref([{ id: 1, name: '苹果', price: 5, count: 2 }])
  const totalCount = computed(() =>
    items.value.reduce((sum, item) => sum + item.count, 0)
  )
  const addItem = (goods) => {
    const found = items.value.find(item => item.id === goods.id)
    if (found) found.count++
    else items.value.push({ ...goods, count: 1 })
  }
  const removeItem = (id) => {
    const found = items.value.find(item => item.id === id)
    if (found && found.count > 1) found.count--
  }

  return { items, totalCount, addItem, removeItem }
}

但它有个局限,Vue3 组合式函数 结尾也提醒过,每次调用 useCart() 函数体都重新跑一遍,items 是新建的一份,导航栏和购物车页各调一次,各拿各的,改不到一起去。

Pinia 的解法,就是给这个组合式函数包一层 defineStore,函数体一个字不用改,只是换个外壳,包完之后里面的数据变成全局唯一一份,谁调用都拿到同一个:

// src/stores/useCartStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // 函数体原样搬进来
  const items = ref([{ id: 1, name: '苹果', price: 5, count: 2 }])
  const totalCount = computed(() =>
    items.value.reduce((sum, item) => sum + item.count, 0)
  )
  const addItem = (goods) => {
    const found = items.value.find(item => item.id === goods.id)
    if (found) found.count++
    else items.value.push({ ...goods, count: 1 })
  }
  const removeItem = (id) => {
    const found = items.value.find(item => item.id === id)
    if (found && found.count > 1) found.count--
  }

  return { items, totalCount, addItem, removeItem }
})

useCart() 比,函数体一行没改,只变了两处外壳。function useCart() 换成 const useCartStore = defineStore('cart', () => ...),多出来的 'cart' 是 store 的名字,全局唯一,多个 store 靠它区分。

还是熟悉的 ref、computed、函数,Pinia 只是给它们起了三个角色名,方便统一称呼:

角色 写法 就是原来的 对应购物车
state ref 响应式数据 清单 items
getters computed 派生计算 总数 totalCount
actions 普通函数 修改逻辑 addItem / removeItem

明白这层对应,Store 就不神秘了,本质就是包了 defineStore 的全局组合式函数

在组件里使用

Store 写好了,组件里用起来就三步,import 进来、useCartStore() 拿实例、模板里直接用。

导航栏要显示总数,只读不写:

<!-- NavBar.vue -->
<script setup>
import { useCartStore } from '../stores/useCartStore'

const cart = useCartStore()
</script>

<template>
  <span>购物车({{ cart.totalCount }} 件)</span>
</template>

购物车页要改清单,点按钮调 action:

<!-- CartPage.vue -->
<script setup>
import { useCartStore } from '../stores/useCartStore'

const cart = useCartStore()
</script>

<template>
  <ul>
    <li v-for="item in cart.items" :key="item.id">
      {{ item.name }} × {{ item.count }}
      <button @click="cart.removeItem(item.id)">-</button>
    </li>
  </ul>
  <button @click="cart.addItem({ id: 1, name: '苹果', price: 5 })">+ 苹果</button>
</template>

读和写都走同一个 cart,ref 的 .value 规则、模板自动解包这些之前学的照用,不需要额外记新规则。

解构要小心

useCartStore() 返回一个对象,直接解构会丢失响应式(和 reactive 解构一个道理):

<script setup>
import { storeToRefs } from 'pinia'
import { useCartStore } from '../stores/useCartStore'

const cart = useCartStore()
// 要保住响应式,state/getters 用 storeToRefs 包着解构
const { items, totalCount } = storeToRefs(cart)
// actions 是普通函数,正常解构即可
const { addItem, removeItem } = cart
</script>

速查卡片

场景 写法
安装/接入 npm i pinia + app.use(createPinia())
定义 store defineStore('名字', () => { ... })
state ref(...) → 返回
getters computed(...) → 返回
actions 普通函数 → 返回
组件中使用 const s = useXxxStore()
响应式解构 state/getters 用 storeToRefs,actions 直接解构
上一篇
Vue3 路由进阶
下一篇
Vue3 Pinia 实战