六维教程

Vue3 Pinia 实战

Vue3 状态管理入门认识了 Pinia 的三个角色:state、getters、actions,这篇把它们放进真实场景,做一份”用户登录态 + 购物车”,把异步请求、参数化 getters、模块拆分、持久化这些实战功夫全用上。

购物车 store

先定义 data(state)、派生值(getters)、操作方法(actions):

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

export const useCartStore = defineStore('cart', () => {
  // ----- state -----
  const items = ref([])   // [{ id, name, price, count }]

  // ----- getters -----
  const totalCount = computed(() =>
    items.value.reduce((sum, it) => sum + it.count, 0)
  )
  const totalPrice = computed(() =>
    items.value.reduce((sum, it) => sum + it.count * it.price, 0)
  )

  // ----- actions -----
  const addItem = (goods) => {
    const found = items.value.find(it => it.id === goods.id)
    if (found) found.count++
    else items.value.push({ ...goods, count: 1 })
  }
  const removeItem = (id) => {
    items.value = items.value.filter(it => it.id !== id)
  }
  const clearCart = () => {
    items.value = []
  }

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

getters 内部直接访问同一个 store 的 state,自动追踪依赖、自动缓存——和单组件里的 computed 体验一致。

参数化 getters

购物车要”看某件商品的金额”,让 getter 接收参数(返回一个函数即可):

// store 内新增
const itemTotal = (id) => computed(() => {
  const it = items.value.find(i => i.id === id)
  return it ? it.price * it.count : 0
})

这么用:

<script setup>
import { useCartStore } from '../stores/useCartStore'
const cart = useCartStore()
</script>

<template>
  <p>这件商品金额:{{ cart.itemTotal(3) }}</p>
</template>

getters 还是组件内 computed?

getters 归”store 内的派生逻辑”,多组件共用就不重复算;只在单个组件里用一次的,组件内 computed 就行。判断标准是”这份派生值有多处要用吗”。

异步 action:登录流程

action 里可以 await。把”调登录接口 + 写 token + 存用户信息”收进一个 action,组件只管调用:

// src/stores/useUserStore.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const token = ref(localStorage.getItem('token') || '')

  const login = async (username, password) => {
    // 换成真实接口:const { data } = await axios.post('/api/login', { username, password })
    const res = await new Promise((resolve) => {
      setTimeout(() => resolve({ token: 'mock-token', name: '小明' }), 500)
    })
    token.value = res.token
    user.value = res
    localStorage.setItem('token', res.token)
  }

  const logout = () => {
    user.value = null
    token.value = ''
    localStorage.removeItem('token')
  }

  return { user, token, login, logout }
})

组件里调用并订阅登录态:

<script setup>
import { useUserStore } from '../stores/useUserStore'
const userStore = useUserStore()
</script>

<template>
  <p v-if="userStore.user">你好,{{ userStore.user.name }}</p>
  <button v-else @click="userStore.login('tom', '123456')">登录</button>
</template>

配合 Vue3 路由进阶 的路由守卫:router.beforeEachuseUserStore().token 判权限,一个”登录拦着才能进”的闭环就成了。

action 能调用另一个 store 的 action 吗?

可以,action 内 useXxxStore() 即可跨 store 协作(如”登录成功→跳转、下单→清空购物车”)。

模块化拆分

不同业务域各建一个文件、各是一个 store,按域组织而不是全塞一个

src/stores/
├── useUserStore.js    # 用户信息、登录退出
├── useCartStore.js    # 购物车
├── useProductStore.js # 商品列表、详情
└── useThemeStore.js   # 主题设置

组件里”用哪个取哪个”:

import { useCartStore } from '../stores/useCartStore'
import { useUserStore } from '../stores/useUserStore'

const cart = useCartStore()
const user = useUserStore()

持久化:刷新不丢

store 默认存内存。要刷新不掉,把关键 state 同步到 localStorage——可以订阅 store 变化($subscribe)自动保存:

// 在 main.js,store 使用前开启
import { createPinia } from 'pinia'

const pinia = createPinia()

// 全局持久化:state 每次变化都写进 localStorage(示例、可按 store 收窄)
pinia.use(({ store }) => {
  const key = `pinia-${store.$id}`
  const saved = localStorage.getItem(key)
  if (saved) store.$patch(JSON.parse(saved))
  store.$subscribe((_, state) => localStorage.setItem(key, JSON.stringify(state)))
})

提示:直接上插件(如 pinia-plugin-persistedstate)可以少写一半代码,这里先理解原理。注意只存可序列化的数据,别把函数/大量图片存进去。

持久化之后,守卫里还闪跳吗?

localStorage 同步读取是同步的,没有”闪跳”窗口;真正异步的持久化(如从 IndexedDB 读)才要处理 loading 状态。我们的登录 store 直接用 localStorage 同步读写,够用且简单。

速查卡片

场景 写法
getters 带参 const fn = (id) => computed(...) 返回函数
异步 action action 里 await 请求,组件只管调用
模块化 每个业务域一个 useXxxStore.js
响应式解构 storeToRefs 包 state/getters,actions 直解
持久化 $subscribe 同步 localStorage(或插件)
路由联动 beforeEach 里读 useUserStore().token

状态管理从入门到实战都齐了。最后一个大件:把这些年的知识组装成完整应用——下一篇 Vue3 项目实战,从空白脚手架做一个能上线的静态工具站。

上一篇
Vue3 状态管理入门
下一篇
Vue3 项目实战