Chapter 05

Pinia 状态管理

告别 Vuex 的繁琐,拥抱 Pinia 的轻量与优雅。从 Store 定义到持久化,完整掌握 Vue 3 官方推荐的状态管理方案

5.1 为什么选择 Pinia?

Pinia 是 Vue 3 的官方推荐状态管理库(Vuex 5 的精神继承者),由 Vue 核心团队成员 Eduardo San Martin Morote 开发。与 Vuex 4 相比,Pinia 有以下优势:

特性Vuex 4Pinia
TypeScript类型推断差,需要大量类型声明原生 TS,完全自动推断
代码量需要 mutation + action,繁琐只有 state + getters + actions,简洁
模块化需要 modules,命名空间复杂天然多 Store,互相引用方便
DevTools支持支持(更好的可视化)
SSR支持更好的 SSR 支持
体积~10KB~1.5KB(微型)

5.2 安装与初始化

npm install pinia
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const pinia = createPinia()

createApp(App)
  .use(pinia)
  .use(router)
  .mount('#app')

5.3 Options Store(选项式风格)

与 Vue 的 Options API 类似,适合从 Vuex 迁移的开发者:

import { defineStore } from 'pinia'

// 惯例:Store ID 以 'use' 开头,以 'Store' 结尾
export const useCounterStore = defineStore('counter', {
  // state:返回初始状态的函数
  state() {
    return {
      count: 0,
      name: '计数器'
    }
  },

  // getters:相当于计算属性(自动缓存)
  getters: {
    doubleCount: (state) => state.count * 2,
    // 在 getter 中调用其他 getter
    doubleCountPlusOne(): number {
      return this.doubleCount + 1
    }
  },

  // actions:同步/异步操作(替代 Vuex 的 mutation + action)
  actions: {
    increment() {
      this.count++  // 直接修改 state!
    },
    reset() {
      this.count = 0
    },
    async fetchAndSet() {
      const data = await fetch('/api/count').then(r => r.json())
      this.count = data.count
    }
  }
})

5.4 Setup Store(组合式风格,推荐)

与 Vue 的 Composition API 一致,更灵活,TypeScript 支持更佳:

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user'
}

export const useUserStore = defineStore('user', () => {
  // ref 对应 state
  const currentUser = ref<User | null>(null)
  const isLoading   = ref(false)
  const token       = ref('')

  // computed 对应 getters
  const isLoggedIn = computed(() => !!currentUser.value)
  const isAdmin    = computed(() => currentUser.value?.role === 'admin')
  const userName   = computed(() => currentUser.value?.name ?? '游客')

  // function 对应 actions
  async function login(email: string, password: string) {
    isLoading.value = true
    try {
      const res = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password })
      })
      const data = await res.json()
      currentUser.value = data.user
      token.value = data.token
    } finally {
      isLoading.value = false
    }
  }

  function logout() {
    currentUser.value = null
    token.value = ''
  }

  // 必须 return 所有需要暴露的内容
  return { currentUser, isLoading, isLoggedIn, isAdmin, userName, login, logout }
})

5.5 在组件中使用 Store

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useCounterStore } from '@/stores/counter'

const userStore    = useUserStore()
const counterStore = useCounterStore()

// ✅ 使用 storeToRefs 解构 state 和 getters(保持响应性)
// 注意:actions 不需要解构,直接调用即可
const { isLoggedIn, userName, isLoading } = storeToRefs(userStore)

// 直接解构 actions(不影响响应性,因为 action 是函数)
const { login, logout } = userStore

// ❌ 错误:直接解构 state/getters 会丢失响应性
// const { isLoggedIn } = userStore  // isLoggedIn 变成普通布尔值
</script>

<template>
  <div v-if="isLoading">加载中...</div>
  <div v-else-if="isLoggedIn">
    欢迎, {{ userName }}
    <button @click="logout">退出</button>
  </div>
  <button v-else @click="login('a@b.com', '123456')">登录</button>
</template>

5.6 Store 间的互相引用

import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', () => {
  // 在 action/getter 内部调用其他 store(不要在 setup 顶层调用)
  async function checkout() {
    const userStore = useUserStore()
    if (!userStore.isLoggedIn) {
      throw new Error('请先登录')
    }
    // 继续结算逻辑...
  }
  return { checkout }
})

5.7 持久化插件

使用 pinia-plugin-persistedstate 将 Store 状态持久化到 localStorage/sessionStorage:

npm install pinia-plugin-persistedstate
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)  // 注册插件
export const useUserStore = defineStore('user', () => {
  const token = ref('')
  const user  = ref(null)
  return { token, user }
}, {
  // 持久化配置
  persist: {
    key: 'user-store',             // localStorage 键名
    storage: localStorage,          // 存储目标
    paths: ['token'],             // 只持久化 token,不存 user 对象
  }
})
$patch 批量修改

Pinia 提供了 $patch 方法批量修改多个 state,比多次单独赋值更高效(减少视图更新次数):counterStore.$patch({ count: 10, name: '新名称' })。也可以传入函数:store.$patch(state => { state.count++; state.list.push(item) })

$reset 重置状态

Options Store 自动拥有 $reset() 方法,可将 state 重置为初始值。Setup Store 需要手动实现 reset 函数。

5.6 Pinia 与 Vuex 的本质区别

Pinia 是 Vue 3 的官方状态管理库,完全替代了 Vuex 4。理解两者差异有助于选择正确的迁移路径和心智模型。

无 mutations
Vuex 强制通过 mutations 修改状态(同步),actions 处理异步。Pinia 合并了两者——直接在 actions 中修改 state,同步异步均可,代码更简洁。
扁平化模块
Vuex 使用嵌套模块(namespaced modules),命名空间容易造成混乱。Pinia 每个 Store 是独立的,通过 ID 标识,相互之间可以直接引用,无需命名空间。
完整 TypeScript 支持
Pinia 从设计之初就考虑 TypeScript,Store 的 state、getters、actions 类型全部自动推导,无需额外类型声明。Vuex 4 的类型支持需要大量样板代码。
Devtools 集成
Pinia 深度集成 Vue Devtools,支持时间旅行调试、state 快照、action 追踪,开发体验与 Vuex 持平甚至更好。

5.7 跨 Store 引用

在复杂应用中,多个 Store 之间常常需要相互引用。Pinia 允许在一个 Store 内直接导入并使用另一个 Store,没有 Vuex 命名空间的束缚。

// stores/cart.ts — 购物车 Store
import { useUserStore } from './user'   // 引用用户 Store

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

  async function checkout() {
    // 在 action 内部调用另一个 Store
    const userStore = useUserStore()   // 注意:在函数内调用,不在顶层

    if (!userStore.isLoggedIn) {
      throw new Error('请先登录')
    }

    await submitOrder({
      userId: userStore.userId,
      items: items.value
    })
    items.value = []   // 清空购物车
  }

  return { items, checkout }
})
常见误区:解构 Store 导致响应式丢失

直接解构 Pinia Store 会丢失响应性:const { count } = useCounterStore() 中的 count 不是响应式的。应使用 storeToRefs() 保持响应性:const { count } = storeToRefs(useCounterStore())。Actions 可以直接解构,因为它们是普通函数:const { increment } = useCounterStore()

本章小结

Pinia 是 Vue 3 推荐的状态管理方案,提供 Options Store(类 Vuex 风格)和 Setup Store(Composition API 风格)两种写法。核心优势:无 mutations、完整 TypeScript 支持、扁平化模块、轻量(约 1KB)。关键 API:defineStore() 定义 Store,storeToRefs() 保持解构后的响应性,$patch() 批量更新,持久化插件解决刷新丢失问题。Store 之间可以在 actions 内互相引用,实现跨模块数据共享。