前端状态管理 Pinia 使用指南

作者:忆笙智云官方 | 发布时间:2026-06-14 16:00 | 更新时间:2026-07-14 16:00

前端状态管理 Pinia 使用指南

引言

在 Vue 3 企业级项目中,状态管理是架构设计的核心环节。Pinia 作为 Vue 3 官方推荐的状态管理库,以其轻量、类型安全、支持组合式 API 的特性,取代 Vuex 成为新一代状态管理方案。相比 Vuex,Pinia 移除了 mutations 的概念,简化了 API 设计,并原生支持 TypeScript 类型推导。

本文将系统介绍 Pinia 在 Vue 3 企业项目中的最佳实践,涵盖 Store 定义规范、组合式 API 风格、持久化插件和 TypeScript 类型推导。

核心内容

一、Pinia 与 Vuex 对比

graph LR
    subgraph "Vuex架构"
        V1[State] --> V2[Mutations]
        V2 --> V3[Actions]
        V3 --> V4[Getters]
    end

    subgraph "Pinia架构"
        P1[State] --> P2[Actions]
        P2 --> P3[Getters]
    end

    style V2 fill:#ffcdd2
    style P2 fill:#c8e6c9
特性 Vuex Pinia
Mutations 必须 无(Actions直接修改)
TypeScript 支持较弱 原生支持
模块化 嵌套模块 扁平Store
组合式API 不支持 原生支持
代码分割 需手动配置 自动支持
体积 ~15KB ~1.5KB

二、Store 定义规范

1. 安装与注册

// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

2. 两种定义风格

Pinia 提供两种 Store 定义风格:选项式(Options API)和组合式(Setup/组合式 API)。企业项目推荐使用组合式风格,与 Vue 3 的 <script setup> 保持一致。

graph TD
    A[Store定义风格] --> B[选项式 Options API]
    A --> C[组合式 Setup API]

    B --> B1[类似Vuex结构]
    B --> B2[state/getters/actions]
    B --> B3[适合简单场景]

    C --> C1[类似script setup]
    C --> C2[ref/computed/function]
    C --> C3[适合企业项目]
    C --> C4[更好的类型推导]

    style C fill:#c8e6c9
    style C4 fill:#a5d6a7

选项式风格:

// stores/user.ts
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null as UserInfo | null,
    token: '',
    permissions: [] as string[]
  }),

  getters: {
    isLoggedIn: (state) => !!state.token,
    username: (state) => state.userInfo?.username ?? ''
  },

  actions: {
    async login(credentials: LoginDTO) {
      const res = await loginApi(credentials)
      this.token = res.data.token
      this.userInfo = res.data.user
      this.permissions = res.data.permissions
    },

    logout() {
      this.token = ''
      this.userInfo = null
      this.permissions = []
    }
  }
})

组合式风格(推荐):

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

export const useUserStore = defineStore('user', () => {
  // State —— 使用ref/reactive
  const userInfo = ref<UserInfo | null>(null)
  const token = ref('')
  const permissions = ref<string[]>([])

  // Getters —— 使用computed
  const isLoggedIn = computed(() => !!token.value)
  const username = computed(() => userInfo.value?.username ?? '')

  // Actions —— 普通函数
  async function login(credentials: LoginDTO) {
    const res = await loginApi(credentials)
    token.value = res.data.token
    userInfo.value = res.data.user
    permissions.value = res.data.permissions
  }

  function logout() {
    token.value = ''
    userInfo.value = null
    permissions.value = []
  }

  // 必须返回所有需要暴露的属性和方法
  return {
    userInfo,
    token,
    permissions,
    isLoggedIn,
    username,
    login,
    logout
  }
})

三、Store 目录组织规范

企业项目中 Store 数量较多,需要合理的目录组织:

src/stores/
├── index.ts              # Pinia实例创建
├── modules/
│   ├── user.ts           # 用户状态
│   ├── permission.ts     # 权限状态
│   ├── app.ts            # 应用全局状态
│   ├── dict.ts           # 字典状态
│   └── tags-view.ts      # 标签页状态
└── types/
    └── index.ts          # Store相关类型定义
// stores/modules/app.ts —— 应用全局状态
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useAppStore = defineStore('app', () => {
  /** 侧边栏折叠状态 */
  const sidebarCollapsed = ref(false)
  /** 主题模式 */
  const theme = ref<'light' | 'dark'>('light')
  /** 语言 */
  const locale = ref('zh-CN')
  /** 设备类型 */
  const device = ref<'desktop' | 'tablet' | 'mobile'>('desktop')

  function toggleSidebar() {
    sidebarCollapsed.value = !sidebarCollapsed.value
  }

  function setTheme(newTheme: 'light' | 'dark') {
    theme.value = newTheme
    document.documentElement.setAttribute('data-theme', newTheme)
  }

  function setDevice(newDevice: 'desktop' | 'tablet' | 'mobile') {
    device.value = newDevice
  }

  return {
    sidebarCollapsed,
    theme,
    locale,
    device,
    toggleSidebar,
    setTheme,
    setDevice
  }
})

四、持久化插件

Pinia 持久化插件可将 Store 数据自动同步到 localStorage/sessionStorage,解决页面刷新后状态丢失的问题。

安装与配置

// stores/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

export default pinia

持久化配置示例

// stores/modules/user.ts
export const useUserStore = defineStore('user', () => {
  const token = ref('')
  const userInfo = ref<UserInfo | null>(null)
  const permissions = ref<string[]>([])

  // ... actions

  return { token, userInfo, permissions, /* ... */ }
}, {
  persist: {
    // 指定持久化的key
    key: 'ys-user',
    // 只持久化部分字段
    paths: ['token', 'userInfo'],
    // 使用sessionStorage
    storage: sessionStorage,
    // 数据恢复后的回调
    afterRestore: (ctx) => {
      console.log('Store数据已恢复:', ctx.store.$id)
    }
  }
})
// stores/modules/app.ts —— 多存储策略
export const useAppStore = defineStore('app', () => {
  const sidebarCollapsed = ref(false)
  const theme = ref<'light' | 'dark'>('light')
  const locale = ref('zh-CN')

  // ...

  return { sidebarCollapsed, theme, locale, /* ... */ }
}, {
  persist: {
    key: 'ys-app',
    paths: ['theme', 'locale'],  // 只持久化主题和语言
    storage: localStorage
  }
})

五、TypeScript 类型推导

Pinia 的组合式风格天然支持 TypeScript 类型推导,无需手动声明类型。

类型定义

// stores/types/index.ts

/** 用户信息 */
export interface UserInfo {
  id: number
  username: string
  nickname: string
  email: string
  avatar: string
  roles: string[]
}

/** 登录参数 */
export interface LoginDTO {
  username: string
  password: string
  captchaCode?: string
}

/** 登录响应 */
export interface LoginResult {
  token: string
  user: UserInfo
  permissions: string[]
}

Store 间类型引用

// stores/modules/permission.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useUserStore } from './user'

export const usePermissionStore = defineStore('permission', () => {
  const routes = ref<RouteRecordRaw[]>([])
  const menuList = ref<MenuRecord[]>([])

  /** 动态路由是否已加载 */
  const routesLoaded = ref(false)

  /** 用户可访问的菜单 */
  const accessibleMenus = computed(() => {
    const userStore = useUserStore()
    if (!userStore.permissions.length) return []
    return filterMenusByPermission(menuList.value, userStore.permissions)
  })

  /** 生成动态路由 */
  async function generateRoutes() {
    const userStore = useUserStore()
    const asyncRoutes = await getRoutesApi(userStore.permissions)
    routes.value = asyncRoutes
    menuList.value = buildMenuList(asyncRoutes)
    routesLoaded.value = true
  }

  return { routes, menuList, routesLoaded, accessibleMenus, generateRoutes }
})

六、组件中使用 Store

<script setup> 中使用

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

const userStore = useUserStore()
const appStore = useAppStore()

// 使用storeToRefs解构保持响应性
const { userInfo, isLoggedIn, username } = storeToRefs(userStore)
const { theme, sidebarCollapsed } = storeToRefs(appStore)

// Actions可以直接解构(不需要storeToRefs)
const { login, logout } = userStore
const { toggleSidebar, setTheme } = appStore

// 登录处理
async function handleLogin() {
  try {
    await login({ username: 'admin', password: '123456' })
  } catch (error) {
    console.error('登录失败', error)
  }
}
</script>

<template>
  <div class="header">
    <span>{{ username }}</span>
    <el-button @click="toggleSidebar">
      {{ sidebarCollapsed ? '展开' : '收起' }}
    </el-button>
    <el-button @click="logout">退出</el-button>
  </div>
</template>

Store 的重置与批量更新

// 在组件中使用
const userStore = useUserStore()

// 重置Store到初始状态(仅选项式风格支持$reset)
// 组合式风格需要手动实现reset方法
export const useUserStore = defineStore('user', () => {
  const token = ref('')
  const userInfo = ref<UserInfo | null>(null)

  // 手动实现reset
  function $reset() {
    token.value = ''
    userInfo.value = null
  }

  return { token, userInfo, $reset }
})

// 批量更新($patch)
userStore.$patch({
  token: 'new-token',
  userInfo: { id: 1, username: 'admin', /* ... */ }
})

// 函数式批量更新(推荐,适用于复杂逻辑)
userStore.$patch((state) => {
  state.token = 'new-token'
  state.userInfo = { id: 1, username: 'admin', /* ... */ }
})

七、Store 订阅与调试

// 监听State变化
const userStore = useUserStore()

userStore.$subscribe((mutation, state) => {
  console.log('Store变更类型:', mutation.type)
  console.log('变更的StoreID:', mutation.storeId)
  console.log('最新状态:', state)
})

// 监听Action执行
userStore.$onAction(({ name, args, after, onError }) => {
  const startTime = Date.now()

  after((result) => {
    console.log(`Action [${name}] 执行成功,耗时: ${Date.now() - startTime}ms`)
  })

  onError((error) => {
    console.error(`Action [${name}] 执行失败:`, error)
  })
})

八、数据流架构

graph TD
    A[组件] -->|dispatch| B[Store Actions]
    B -->|调用| C[API Service]
    C -->|请求| D[后端API]
    D -->|响应| C
    C -->|返回数据| B
    B -->|更新| E[Store State]
    E -->|响应式绑定| A

    F[路由守卫] -->|读取| E
    G[权限指令] -->|读取| E
    H[其他Store] -->|跨Store调用| B

    style B fill:#e1f5fe
    style E fill:#c8e6c9
    style A fill:#fff3e0

结论与建议

最佳实践总结

  1. 统一使用组合式风格:与 Vue 3 的 <script setup> 保持一致,获得更好的 TypeScript 类型和代码组织。

  2. 合理使用 storeToRefs:解构 State 和 Getters 必须使用 storeToRefs,Actions 可以直接解构。

  3. 选择性持久化:只持久化必要的数据(如 token、用户偏好),避免将大量临时数据写入 Storage。

  4. Store 职责单一:每个 Store 负责一个业务领域,避免"上帝Store"。跨 Store 调用时直接引用对应 Store。

  5. 避免循环依赖:Store A 引用 Store B,Store B 又引用 Store A 会导致循环依赖,应通过事件或中间层解耦。

  6. 敏感数据不持久化:权限列表等敏感数据不应持久化到 Storage,应在每次登录后从服务端获取。

相关资源