Skip to content

Pinia 状态管理

大白话解释: Pinia 是 Vuex 的"升级版",更简单、更好用。如果说 Vuex 是"绕弯子"(修改数据要经过 Mutation),Pinia 就是"直来直去"(直接修改数据)。

为什么推荐 Pinia 替代 Vuex?

  • 没有 Mutation:直接修改 state,不用绕弯子写 mutation
  • TypeScript 支持更好:类型推导完美,写代码时有智能提示
  • 模块化更简单:每个 Store 天然独立,不用嵌套 modules
  • 体积更小:只有 ~1KB,Vuex 有 ~10KB

什么时候用 Pinia?

  • Vue 3 新项目(官方推荐)
  • 需要多个组件共享状态
  • 需要状态持久化(如用户登录信息)

Vue 3 官方推荐的状态管理库,是 Vuex 的继任者。API 更简洁,完美支持 TypeScript,没有 mutations 的概念。


与 Vuex 对比

特性PiniaVuex
mutations,直接修改 state需要 mutations
TypeScript原生支持,类型推导完美支持较弱
模块化天然独立,无需 modules需要 modules 嵌套
体积~1KB~10KB
DevTools完整支持完整支持
SSR支持支持
插件支持支持

安装与基本配置

bash
yarn add pinia
ts
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'  // 导入 createPinia
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()  // 创建 Pinia 实例

app.use(pinia)  // 安装 Pinia 插件
app.mount('#app')

创建 Store

Options API 风格

ts
// stores/counter.ts
import { defineStore } from 'pinia'  // 导入 defineStore

export const useCounterStore = defineStore('counter', {
  // 状态:返回一个对象,包含所有响应式状态
  state: () => ({
    count: 0,           // 计数器
    name: '计数器',      // 名称
    list: [] as string[], // 列表
  }),

  // 计算属性:基于 state 自动计算
  getters: {
    doubleCount: (state) => state.count * 2,  // 双倍计数

    // 使用其他 getter
    doubleCountPlusOne(): number {
      return this.doubleCount + 1  // 双倍 + 1
    },

    // 带参数的 getter(返回函数)
    findById: (state) => {
      return (id: number) => state.list.find((item) => item === String(id))
    },
  },

  // 方法(同步 + 异步):修改 state 的唯一方式
  actions: {
    increment() {
      this.count++  // 直接修改 state
    },

    decrement() {
      this.count--
    },

    reset() {
      this.count = 0
    },

    async fetchCount() {
      try {
        const res = await api.getCount()  // 异步请求
        this.count = res.data             // 更新 state
      } catch (err) {
        console.error('获取计数失败', err)
      }
    },
  },
})

Composition API 风格(推荐)

ts
// stores/user.ts
import { ref, computed } from 'vue'  // 导入 ref 和 computed
import { defineStore } from 'pinia'  // 导入 defineStore

export const useUserStore = defineStore('user', () => {
  // ---- state:用 ref 定义响应式状态 ----
  const name = ref('')          // 用户名
  const token = ref('')         // 登录 token
  const roles = ref<string[]>([])  // 角色列表
  const permissions = ref<string[]>([])  // 权限列表

  // ---- getters:用 computed 定义计算属性 ----
  const isLoggedIn = computed(() => !!token.value)  // 是否已登录
  const isAdmin = computed(() => roles.value.includes('admin'))  // 是否管理员
  const hasPermission = (perm: string) => permissions.value.includes(perm)  // 检查权限

  // ---- actions:普通函数,直接修改 state ----
  async function login(credentials: { username: string; password: string }) {
    const res = await api.login(credentials)  // 调用登录 API
    token.value = res.data.token              // 保存 token
    name.value = res.data.name                // 保存用户名
    roles.value = res.data.roles              // 保存角色
    permissions.value = res.data.permissions  // 保存权限
    return res.data                           // 返回数据供组件使用
  }

  async function getUserInfo() {
    if (!token.value) return  // 没有 token,跳过
    try {
      const res = await api.getUserInfo()  // 获取用户信息
      name.value = res.data.name
      roles.value = res.data.roles
      permissions.value = res.data.permissions
    } catch (err) {
      logout()  // token 失效,退出登录
    }
  }

  function logout() {
    token.value = ''         // 清空 token
    name.value = ''          // 清空用户名
    roles.value = []         // 清空角色
    permissions.value = []   // 清空权限
  }

  // 返回所有需要暴露的状态和方法
  return {
    name,
    token,
    roles,
    permissions,
    isLoggedIn,
    isAdmin,
    hasPermission,
    login,
    getUserInfo,
    logout,
  }
})

在组件中使用

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

const counterStore = useCounterStore()  // 获取 counter store 实例
const userStore = useUserStore()        // 获取 user store 实例

// ✅ state 和 getters 用 storeToRefs 解构(保持响应性)
const { count, doubleCount } = storeToRefs(counterStore)
const { name, isLoggedIn, isAdmin } = storeToRefs(userStore)

// ✅ actions 可以直接解构(不需要 storeToRefs)
const { increment, decrement, reset } = counterStore
const { login, logout } = userStore
</script>

<template>
  <div>
    <p>count: {{ count }}, double: {{ doubleCount }}</p>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
    <button @click="reset">重置</button>

    <div v-if="isLoggedIn">
      <p>欢迎,{{ name }}</p>
      <button @click="logout">退出</button>
    </div>
  </div>
</template>

⚠️ 直接解构 counterStore.count 会丢失响应性,必须用 storeToRefs


Store 之间互相调用

ts
// stores/order.ts
import { defineStore } from 'pinia'  // 导入 defineStore
import { useUserStore } from './user'  // 导入 user store

export const useOrderStore = defineStore('order', () => {
  const userStore = useUserStore()  // 获取 user store 实例

  const list = ref<any[]>([])  // 订单列表,需要类型标注

  async function createOrder(product: any) {
    // 使用其他 store 检查登录状态
    if (!userStore.isLoggedIn) {
      throw new Error('请先登录')
    }

    const res = await api.createOrder({
      userId: userStore.name,  // 使用 user store 的数据
      product,
    })
    list.value.push(res.data)  // 添加到订单列表
    return res.data
  }

  async function fetchOrders() {
    const res = await api.getOrders()  // 获取订单列表
    list.value = res.data              // 更新列表
  }

  return { list, createOrder, fetchOrders }
})

常用 API 详解

$reset —— 重置为初始值

ts
const store = useCounterStore()
store.count = 100      // 修改状态
store.name = '自定义'  // 修改名称

store.$reset() // 恢复为 state() 的初始值

⚠️ Composition API 风格的 store 没有 $reset,需要手动实现:

ts
export const useUserStore = defineStore('user', () => {
  const name = ref('')    // 用户名
  const token = ref('')   // token

  // 手动实现 $reset 方法
  function $reset() {
    name.value = ''   // 重置用户名
    token.value = ''  // 重置 token
  }

  return { name, token, $reset }
})

$patch —— 批量更新

ts
const store = useCounterStore()

// 对象形式:一次性更新多个字段
store.$patch({ count: 10, name: '新名字' })

// 函数形式(适合复杂更新,如数组操作)
store.$patch((state) => {
  state.count += 10         // 修改计数
  state.name = '更新'       // 修改名称
  state.list.push('new item')  // 添加数组项
})

$subscribe —— 监听状态变化

ts
const store = useCounterStore()

const unsubscribe = store.$subscribe(
  (mutation, state) => {
    // mutation.type: 'direct' | 'patch object' | 'patch function'
    // mutation.events: 具体的变更事件
    console.log('状态变化:', mutation.type, mutation.events)
    console.log('新状态:', state)
  },
  { detached: true } // detached: true 表示组件卸载后继续监听(默认 false,组件卸载自动取消)
)

// 取消监听
unsubscribe()

$onAction —— 监听 action 调用

ts
const store = useCounterStore()

// 监听所有 action 的调用
store.$onAction(({ name, args, after, onError }) => {
  console.log(`action "${name}" 被调用,参数:`, args)  // 记录调用

  after((result) => {
    console.log(`action "${name}" 完成,结果:`, result)  // action 成功
  })

  onError((error) => {
    console.error(`action "${name}" 失败:`, error)  // action 失败
  })
})

$state —— 替换整个状态

ts
const store = useCounterStore()

// 替换整个 state(谨慎使用,会丢失响应性引用)
store.$state = { count: 0, name: '初始', list: [] }

// 或获取当前 state 的快照
const currentState = store.$state

$dispose —— 销毁 store 实例

ts
const store = useCounterStore()

// 手动销毁,清除所有订阅
store.$dispose()

// 销毁后 store 不再响应式,也不能再使用
// 如需重新使用,需要重新调用 useCounterStore()

💡 $dispose 后 store 会从 Pinia 实例中移除。如果再次调用 useCounterStore(),会创建一个全新的实例。


插件系统

基本插件

ts
// plugins/logger.ts
import type { PiniaPluginContext } from 'pinia'  // 导入插件上下文类型

// 日志插件:记录所有状态变化和 action 调用
export function loggerPlugin({ store }: PiniaPluginContext) {
  // 监听状态变化
  store.$subscribe((mutation, state) => {
    console.log(`[Pinia] ${store.$id}`, mutation.type, state)
  })

  // 监听 action 调用
  store.$onAction(({ name, args }) => {
    console.log(`[Pinia] ${store.$id}.${name}()`, args)
  })
}
ts
// main.ts
import { createPinia } from 'pinia'
import { loggerPlugin } from './plugins/logger'  // 导入日志插件

const pinia = createPinia()
pinia.use(loggerPlugin)  // 注册插件
app.use(pinia)

持久化插件

ts
// plugins/persist.ts
import type { PiniaPluginContext } from 'pinia'  // 导入插件上下文类型

// 持久化插件:自动将状态保存到 localStorage
export function persistPlugin({ store }: PiniaPluginContext) {
  // 从 localStorage 恢复状态
  const key = `pinia-${store.$id}`  // 存储 key
  const saved = localStorage.getItem(key)
  if (saved) {
    try {
      store.$patch(JSON.parse(saved))  // 恢复状态
    } catch (e) {
      console.error('恢复状态失败', e)
    }
  }

  // 监听变化并自动保存
  store.$subscribe((mutation, state) => {
    localStorage.setItem(key, JSON.stringify(state))  // 保存到 localStorage
  })
}
bash
# 或使用社区插件
yarn add pinia-plugin-persistedstate
ts
// main.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'  // 导入持久化插件

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)  // 注册持久化插件
ts
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const token = ref('')   // token
  const name = ref('')    // 用户名
  return { token, name }
}, {
  persist: {
    key: 'user-store',           // 存储 key
    storage: localStorage,       // 存储方式
    paths: ['token'],            // 只持久化 token,不持久化 name
  },
})

给 store 添加全局属性

ts
// plugins/api.ts
import type { PiniaPluginContext } from 'pinia'

// 扩展 PiniaStore 类型,避免 TS 报错
declare module 'pinia' {
  export interface PiniaCustomProperties {
    $api: {
      get: (url: string) => Promise<any>
      post: (url: string, data: any) => Promise<any>
    }
  }
}

export function apiPlugin({ store }: PiniaPluginContext) {
  // 给每个 store 注入 $api
  store.$api = {
    get: (url: string) => fetch(url).then((r) => r.json()),
    post: (url: string, data: any) =>
      fetch(url, { method: 'POST', body: JSON.stringify(data) }).then((r) => r.json()),
  }
}
ts
// 使用
const store = useUserStore()
const data = await store.$api.get('/api/user')

在组件外使用 store

ts
// router/guards.ts
import { useUserStore } from '@/stores/user'

export function setupRouterGuards(router: any) {
  router.beforeEach((to: any) => {
    // ✅ 在 router guard 中使用 store(必须在 pinia 安装后调用)
    const userStore = useUserStore()
    if (to.meta.requiresAuth && !userStore.isLoggedIn) {
      return { name: 'Login' }
    }
  })
}

⚠️ 在组件外使用 store 时,必须确保 createPinia() 已经 app.use(pinia)


测试 Store

ts
// stores/__tests__/counter.test.ts
import { describe, it, expect } from 'vitest'  // 导入测试工具
import { setActivePinia, createPinia } from 'pinia'  // 导入 Pinia 工具
import { useCounterStore } from '../counter'  // 导入要测试的 store

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia()) // 每次测试重置 Pinia,避免状态污染
  })

  it('初始值为 0', () => {
    const store = useCounterStore()  // 创建 store 实例
    expect(store.count).toBe(0)     // 验证初始值
  })

  it('increment 正常工作', () => {
    const store = useCounterStore()
    store.increment()                // 调用 action
    expect(store.count).toBe(1)     // 验证结果
  })

  it('doubleCount 正确计算', () => {
    const store = useCounterStore()
    store.count = 5                  // 直接修改 state
    expect(store.doubleCount).toBe(10)  // 验证 getter
  })
})

常见坑点

1. storeToRefs 只用于 state 和 getters

ts
// ✅ 正确
const { count, doubleCount } = storeToRefs(store) // state + getters
const { increment } = store // actions 直接解构

// ❌ 错误 —— actions 不需要 storeToRefs
const { increment } = storeToRefs(store)

2. 组件外使用 store 忘记初始化

ts
// ❌ 在 pinia 安装前调用
const store = useUserStore() // 报错:getActivePinia was called with no active Pinia

// ✅ 确保在 app.use(pinia) 之后调用

3. 直接替换 reactive 对象

ts
// ❌ Composition API 风格中
const state = reactive({ count: 0 })
state = reactive({ count: 1 }) // 丢失引用

// ✅ 修改属性
state.count = 1

// ✅ 或用 ref
const state = ref({ count: 0 })
state.value = { count: 1 }

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准