Pinia 状态管理完全指南:Store 设计、持久化、SSR 兼容与生产级实践

Pinia 是 Vue 官方推荐的状态管理库,作为 Vuex 的继任者。本文完整覆盖:Pinia vs Vuex 对比、Store 定义与组织、State/Getter/Action 模式、插件系统、持久化方案、SSR 兼容性、Devtools 调试,以及与 Vue Router 的组合使用。

Pinia 是 Vue 核心团队开发的状态管理库,作为 Vuex 的官方继任者。它比 Vuex 更轻量、更 TypeScript 友好、API 更直觉——没有 mutations,直接通过 action 修改 state,支持 Composition API 原生。Vue 官方在 Vue 3 时代明确推荐 Pinia 作为新项目的状态管理方案。


一、为什么选 Pinia?

1.1 Pinia vs Vuex:核心差异

维度PiniaVuex 4
API 风格直觉(store.count++样板多(mutation → action)
TypeScript⭐ 原生支持,类型推导完美需要模块声明
体积⭐ ~1KB~1.5KB
Devtools✅ 时间旅行、热更新✅ 时间旅行
SSR✅ 原生支持需要配置
插件✅ 简单✅ 复杂
模块化自动(每个 store 独立)需要手动命名空间
学习曲线⭐ 低

1.2 Pinia vs 全局状态库(Zustand 等)

虽然理论上任何 JS 状态库都能与 Vue 配合,但 Pinia 的优势在于:

  • Vue 响应式集成:Pinia 的 state 就是 Vue 的 reactive/ref,天然与组件响应式系统打通
  • SSR 兼容:服务端渲染时自动处理 hydration
  • Devtools:与 Vue DevTools 深度集成
  • 插件生态:持久化、缓存、日志等插件丰富

二、Store 定义与基础用法

2.1 定义 Store

// stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 命名规范:use + Store 名
export const useCounterStore = defineStore('counter', () => {
  // ========== State ==========
  const count = ref(0)
  const items = ref<string[]>([])

  // ========== Getters ==========
  const doubleCount = computed(() => count.value * 2)
  const itemCount = computed(() => items.value.length)

  // ========== Actions ==========
  function increment() {
    count.value++
  }

  function addItem(item: string) {
    items.value.push(item)
  }

  function $reset() {
    count.value = 0
    items.value = []
  }

  return { count, items, doubleCount, itemCount, increment, addItem, $reset }
})

注意:次参数 'counter' 是 store 的唯一 ID,全局不能重复。

2.2 在组件中使用

<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const store = useCounterStore()

// 解构保持响应性(必须用 storeToRefs)
const { count, doubleCount } = storeToRefs(store)

// 可以直接修改 state(不需要 mutation)
const increase = () => {
  store.increment()
  // 或:store.count++
}

// $reset
const reset = () => store.$reset()
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increase">+1</button>
    <button @click="reset">Reset</button>
  </div>
</template>

2.3 Options 风格定义(兼容 Vuex 用户)

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0, items: [] as string[] }),
  getters: {
    doubleCount: (state) => state.count * 2,
    itemCount: (state) => state.items.length,
  },
  actions: {
    increment() {
      this.count++
    },
    addItem(item: string) {
      this.items.push(item)
    },
  },
})

推荐:新项目中使用 Setup 风格(Composition API),类型推导更完整。


三、Pinia Store 组织策略

3.1 按领域拆分 Store

stores/
├── index.ts           # store 导出聚合
├── auth.ts            # 认证状态
├── user.ts            # 用户信息
├── cart.ts            # 购物车
├── product.ts         # 商品数据
├── ui.ts              # UI 状态(主题/侧边栏/模态框)
└── settings.ts        # 用户设置

3.2 Store 间互相调用

// stores/cart.ts
import { useAuthStore } from './auth'

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])

  async function checkout() {
    const auth = useAuthStore()  // 在其他 store 中获取
    if (!auth.isLoggedIn) {
      throw new Error('Please login first')
    }
    await api.checkout({ items: items.value, userId: auth.user?.id })
    items.value = []
  }

  return { items, checkout }
})

3.3 聚合导出

// stores/index.ts
export { useAuthStore } from './auth'
export { useUserStore } from './user'
export { useCartStore } from './cart'
export { useUIStore } from './ui'

四、插件与持久化

4.1 Pinia Plugin 开发

// plugins/piniaStorage.ts
import { PiniaPluginContext } from 'pinia'

export function piniaStoragePlugin(context: PiniaPluginContext) {
  const { store } = context

  // 从 localStorage 恢复
  const saved = localStorage.getItem(store.$id)
  if (saved) {
    store.$patch(JSON.parse(saved))
  }

  // 订阅变化并保存
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}

// main.ts
import { createPinia } from 'pinia'
import { piniaStoragePlugin } from './plugins/piniaStorage'

const pinia = createPinia()
pinia.use(piniaStoragePlugin)

4.2 使用 pinia-plugin-persistedstate

npm install pinia-plugin-persistedstate
// main.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
pinia.use(piniaPluginPersistedstate)

// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
  // ...
}, {
  persist: {
    storage: localStorage,           // 默认 localStorage
    paths: ['token', 'user'],        // 只持久化这些字段
  },
})

五、SSR 兼容

// Nuxt.js / SSR 项目
// 使用 $fetchState 或 ensure data is fetched

export const useProductStore = defineStore('product', () => {
  const products = ref<Product[]>([])
  const loading = ref(false)

  async function fetchProducts() {
    if (products.value.length) return  // SSR 时已获取则跳过
    loading.value = true
    products.value = await $fetch('/api/products')  // Nuxt 的 $fetch
    loading.value = false
  }

  return { products, loading, fetchProducts }
})

// 在页面中
<script setup>
const productStore = useProductStore()
await productStore.fetchProducts()  // await 确保 SSR 完成后再渲染
</script>

常见问题(FAQ)

为什么不用全局 ref/composable?

全局 ref 或跨文件 composable 可以共享状态,但缺少:

  • Devtools 支持(时间旅行、状态快照)
  • SSR 兼容性(hydration 处理)
  • 插件生态(持久化、缓存)
  • 命名冲突管理

Pinia 提供了生产级状态管理的完整解决方案。

Pinia 支持异步 Action 吗?

export const useStore = defineStore('store', () => {
  const loading = ref(false)

  async function fetchData() {
    loading.value = true
    try {
      const data = await api.fetch()
      return data
    } finally {
      loading.value = false
    }
  }

  return { loading, fetchData }
})

// 使用
const data = await store.fetchData()

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章