第 12 章:Pinia 进阶
学习目标
- 用
pinia-plugin-persistedstate持久化 - 在路由守卫中读取 store 做权限
- store 之间互相调用
- 写单元测试
一、持久化插件
bash
npm i pinia-plugin-persistedstatets
// src/main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)ts
// store 中开启
export const useUserStore = defineStore('user', {
state: () => ({ token: '', name: '' }),
persist: true, // 默认 localStorage
})精确控制:
ts
persist: {
key: 'myapp-user',
storage: localStorage,
paths: ['token'], // 只持久化 token
}💡
persist写在 store 配置对象的字段里,前提是装了pinia-plugin-persistedstate插件(不装插件,persist字段被忽略,不生效)。三步配置:
① 装插件
bashnpm i pinia-plugin-persistedstate② main.ts 注册
tsimport { createPinia } from 'pinia' import piniaPersist from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPersist) // ← 必须注册,不然 persist 字段不生效③ 写在 store 里(Options 风格)
ts// src/stores/user.ts export const useUserStore = defineStore('user', { state: () => ({ token: '', userInfo: { name: '', avatar: '' }, }), persist: { key: 'myapp-user', // ① localStorage 里的 key 名 storage: localStorage, // ② 默认就是 localStorage,可以不写 paths: ['token'], // ③ 只持久化 token(不写 = 全 state 都存) }, })⚠️ Setup 风格不能用 persist——语法不支持。要持久化就用 Options 风格,Setup 风格只用在组合式逻辑多时。
常用配置项:
字段 干啥 keylocalStorage 里的 key 名(默认是 store id) storage默认 localStorage,可换sessionStoragepaths只持久化哪些字段,支持点路径(如 'settings.theme')beforeRestore/afterRestore从 localStorage 读回前后钩子 serializer自定义序列化(默认 JSON) 啥时候用:
场景 配 persist token / 用户信息 ✅ 必须,刷新不掉登录 购物车 / 草稿 ✅ 用户体验好 全局 UI 偏好(主题 / 语言) ✅ 推荐 纯计算状态 / 临时数据 ❌ 不持久化
⚠️ 坑 1:
persist: true默认会用JSON.stringify,不要存函数 / Symbol / 循环引用。
二、store 之间互相调用
ts
export const useOrderStore = defineStore('order', () => {
const cart = useCartStore()
const user = useUserStore()
const checkout = async () => {
if (!user.token) throw new Error('未登录')
const res = await api.createOrder(cart.items)
cart.clear()
return res
}
return { checkout }
})三、路由守卫中使用 store
ts
// src/router/index.ts
import { useUserStore } from '@/stores/user'
router.beforeEach((to) => {
const user = useUserStore()
if (to.meta.requiresAuth && !user.token) {
return { path: '/login' }
}
})⚠️ 坑 2:在
router.beforeEach里调useStore必须 Pinia 已经app.use(pinia),否则会报错。
四、getter 互相引用
ts
getters: {
fullName: (s) => `${s.first} ${s.last}`,
greeting(): string {
return this.fullName + ' 你好' // ⚠️ 必须用 this,不能用 state.
},
}五、单元测试
bash
npm i -D vitest @vue/test-utilsts
// src/stores/__tests__/cart.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from '../cart'
import { beforeEach, expect, test } from 'vitest'
beforeEach(() => setActivePinia(createPinia()))
test('add item', () => {
const cart = useCartStore()
cart.add({ id: 1, name: 'A' })
cart.add({ id: 1, name: 'A' })
expect(cart.totalQty).toBe(2)
})⚠️ 坑 3:每个测试前
setActivePinia(createPinia()),否则 store 状态会串。
六、热更新
ts
// vite HMR 支持
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
}这样 store 改动后,页面状态不会丢。
七、组合式风格 vs 选项式
| 风格 | 优点 | 适用 |
|---|---|---|
| Setup | 灵活,可复用 useXxx | 复杂业务 |
| Options | 直观,IDE 提示好 | 简单业务 |
⚠️ 坑 4:同一项目统一风格,混用维护成本高。
八、本章小结
| 能力 | 关键 |
|---|---|
| 持久化 | persist: true + 插件 |
| 跨 store | 在 action 中 useXStore() |
| 测试 | setActivePinia(createPinia()) |
| HMR | acceptHMRUpdate(useStore, import.meta.hot) |
动手练习
- 持久化:token 用
persist持久化,刷新页面后仍登录 - 订单 store:加 OrderStore,在 checkout 中调用 cart + user
- 单元测试:给 cart store 写 3 个 case