Pinia 高级状态管理技巧:跨模块通信、缓存策略与性能优化
用 Pinia 一段时间后,很多团队会遇到这些问题:
- Store 之间怎么通信而不陷入"相互依赖地狱"?
- API 数据应该在 store 里缓存,什么时候更新,什么时候过期?
- 怎么监控 store 变化,防止"无意改动数据后到线上才出现 bug"?
- 何时用
storeToRefs,何时直接解构,该怎么选?
大多数团队对 Pinia 的理解停留在"定义 state → getter → action",但高级用法能让状态管理变成可维护、可调试、性能优化的系统。
1. Store 划分策略:避免"全能大 store"
最常见的坑是建立一个包罗万象的 Store:
// ❌ 坏做法
export const useLargeStore = defineStore('large', {
state: () => ({
user: {},
projects: [],
tasks: [],
notifications: [],
settings: {},
cache: {}
// ...20 个字段
})
})
这会导致:
- 难以追踪谁改了什么
- 频繁的修改触发不必要的组件重新渲染
- 测试每个功能都要 mock 整个 store
更好的策略:按 Domain 或 Feature 划分,每个 store 职责清晰:
// ✅ 按功能域拆分
export const useAuthStore = defineStore('auth', {
state: () => ({ user: null, token: '' })
// 只管用户认证
})
export const useProjectStore = defineStore('project', {
state: () => ({
current: null,
list: [],
filters: {}
})
// 只管项目管理
})
export const useNotificationStore = defineStore('notification', {
state: () => ({ messages: [] })
// 只管通知消息
})
拆分的好处:
- 订阅时精确控制(换项目时,只重新取项目数据,用户信息无需重新加载)
- 独立测试和维护
- 性能好(减少不必要的 rerender)
2. 跨 Store 通信:用 Action 而不是直接修改
不同 store 需要交互是常态,关键是怎么交互。
❌ 坏的写法:
// 在组件里直接改多个 store
const authStore = useAuthStore()
const projectStore = useProjectStore()
authStore.logout()
projectStore.$reset() // 直接调用别人的方法
notificationStore.clearAll()
这样做的问题是,如果流程改变(比如登出时需要先保存项目状态),就要改组件代码。
✅ 好的写法:用 action 来编排跨 store 操作:
export const useAuthStore = defineStore('auth', {
actions: {
async logout() {
// 负责编排所有登出相关的 store 操作
const projectStore = useProjectStore()
const notificationStore = useNotificationStore()
// 顺序很重要
projectStore.saveEdits() // 保存项目改动
notificationStore.clearAll() // 清通知
// 最后再登出
const res = await api.post('/logout')
this.user = null
this.token = ''
return res
}
}
})
// 组件里只需要调一个
const authStore = useAuthStore()
await authStore.logout()
规则:
- Store A 的 action 可以调用 Store B 的 getter 和 action
- 但禁止在 getter 里调用其他 store 的 action(会导致副作用不可预测)
- 复杂的多 store 编排,放在组件 or composable 里,而不是 store 里
3. 缓存与过期策略:让数据"自动"更新
API 数据应该什么时候从 store 重新取?常见的做法都不够优雅:
❌ 坏的方案:
// 每次用都重新请求(浪费请求)
const fetchUser = async () => {
this.user = await api.getUser()
}
// 永不更新(数据陈旧)
const cachedUser = computed(() => this.user)
✅ 好的方案:用缓存时间戳和自动失效机制:
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
lastFetch: 0,
CACHE_DURATION: 5 * 60 * 1000 // 5 分钟
}),
getters: {
isCacheExpired: (state) => {
return Date.now() - state.lastFetch > state.CACHE_DURATION
},
shouldFetch: (state) => {
return !state.user || state.isCacheExpired
}
},
actions: {
async fetchUserIfNeeded() {
if (!this.shouldFetch) return this.user
this.user = await api.getUser()
this.lastFetch = Date.now()
return this.user
},
// 显式刷新
async refreshUser() {
this.user = await api.getUser()
this.lastFetch = Date.now()
},
// 登出时清缓存
logout() {
this.user = null
this.lastFetch = 0
}
}
})
// 组件里使用
const userStore = useUserStore()
const user = computed(() => {
userStore.fetchUserIfNeeded()
return userStore.user
})
好处:
- 避免重复请求
- 数据自动过期,无需手动管理
- 在合适的时机(登出)清缓存
进阶:实现一个通用的缓存 mixin
const createCachedStore = (storeName, options) => {
return defineStore(storeName, {
state: () => ({
cache: {},
timestamps: {},
...options.state?.()
}),
getters: {
isCacheValid: (state) => (key) => {
const ts = state.timestamps[key]
if (!ts) return false
return Date.now() - ts < (options.cacheDuration || 5 * 60 * 1000)
}
},
actions: {
getCached(key, fetcher) {
if (this.isCacheValid(key)) {
return this.cache[key]
}
return fetcher().then((data) => {
this.cache[key] = data
this.timestamps[key] = Date.now()
return data
})
}
}
})
}
4. Pinia 插件系统:统一监控和日志
Pinia 提供了插件系统,可以在 store 级别做全局操作,比如:
- 异常监控
- 状态变化日志
- 性能追踪
- 状态持久化
// 插件:监控所有状态变化并打日志
export const createLoggingPlugin = () => {
return (context) => {
context.store.$subscribe((mutation, state) => {
console.log(`[Store ${context.store.$id}] ${mutation.type}`, {
mutation: mutation.payload,
newState: state
})
})
}
}
// 插件:性能监控
export const createPerformancePlugin = () => {
return (context) => {
const store = context.store
const originalAction = store.$onAction
store.$onAction(({ name, after, onError }) => {
const start = performance.now()
after(async () => {
const duration = performance.now() - start
if (duration > 100) {
console.warn(`[SLOW ACTION ${store.$id}.${name}] ${duration.toFixed(2)}ms`)
}
})
onError((error) => {
console.error(`[ERROR ${store.$id}.${name}]`, error)
})
})
}
}
// 注册插件
const app = createApp(App)
const pinia = createPinia()
pinia.use(createLoggingPlugin())
pinia.use(createPerformancePlugin())
app.use(pinia)
用途:
- 开发阶段:快速定位数据异常
- 生产环境:性能监控和错误上报
- 跨团队:统一日志规范
5. storeToRefs vs 直接解构:什么时候用哪个?
这是新手常犯的错误:
const authStore = useAuthStore()
// ❌ 如果直接解构,reactive 会丢失
const { user } = authStore // user 之后的变化不会更新 UI
规则很简单:
| 场景 | 方法 | 原因 |
|---|---|---|
| 访问状态(需要响应性) | storeToRefs() | 保留 ref,响应式 |
| 访问 getter 和 action | 直接解构 | getter/action 不需要响应性 |
| 在 template 中 | 不解构,直接 store.user | 自动 unwrap |
| 在 computed 中 | storeToRefs() | 依赖追踪 |
export default defineComponent({
setup() {
const authStore = useAuthStore()
// 状态:用 storeToRefs
const { user, token } = storeToRefs(authStore)
// Action 和 getter:直接解构
const { logout, isAdmin } = authStore
// template 中可以这样用
return {
user, // ref,响应式
isAdmin, // computed,是 ref
logout // function
}
}
})
6. 避免的反模式
反模式 1:在 store 的 getter 里产生副作用
// ❌ 坏做法
export const useDataStore = defineStore('data', {
getters: {
cachedData: (state) => {
if (!state.data) {
// 别这样!getter 每次访问都会请求
api.fetchData().then(d => state.data = d)
}
return state.data
}
}
})
getter 应该是纯函数。如果有副作用,改成 action。
反模式 2:过度使用 computed getters
// ❌ 如果只是在 state 基础上做简单转换
getters: {
userEmail: (state) => state.user?.email,
userName: (state) => state.user?.name
}
对于这种简单的字段访问,直接用 state 就够了。getter 适合复杂的派生逻辑。
反模式 3:Store 里塞逻辑,action 变得巨大
// ❌ store 不应该包含业务逻辑
actions: {
async processOrder(items) {
// 计算折扣
// 校验库存
// 生成订单号
// 发送邮件
// ...
}
}
这些应该在业务层或 composable 里。store 只负责"状态管理"。
7. 最佳实践清单
- 按功能域划分 store(
useAuthStore,useProjectStore等) - 复杂的跨 store 操作,用 store 的 action 或组件 composable 来编排
- 为 API 数据实现缓存和过期机制
- 用 storeToRefs 处理状态,直接解构处理 action/getter
- 定义 store 的类型和文档(让团队成员明确知道这个 store 的职责)
- 用插件实现日志、监控、持久化等横切功能
- getter 保持纯函数,副作用放 action
- 高频访问的数据,定期检查是否存在性能瓶颈


