Pinia 高级状态管理技巧:跨模块通信、缓存策略与性能优化

HTMLPAGE 团队
17 分钟阅读

从模块化设计、跨 store 通信、缓存与过期策略、到插件系统与性能监控,系统讲清如何把 Pinia 从"数据仓库"用成"智能状态引擎",支撑复杂应用的状态治理。

#Pinia #State Management #Store #Architecture #Performance

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

更好的策略:按 DomainFeature 划分,每个 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
  • 高频访问的数据,定期检查是否存在性能瓶颈

8. 内链与延伸阅读