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 函数。