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.beforeEach 里 useUserStore().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 项目实战,从空白脚手架做一个能上线的静态工具站。