前端状态管理 Pinia 使用指南
前端状态管理 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
结论与建议
最佳实践总结
-
统一使用组合式风格:与 Vue 3 的
<script setup>保持一致,获得更好的 TypeScript 类型和代码组织。 -
合理使用 storeToRefs:解构 State 和 Getters 必须使用
storeToRefs,Actions 可以直接解构。 -
选择性持久化:只持久化必要的数据(如 token、用户偏好),避免将大量临时数据写入 Storage。
-
Store 职责单一:每个 Store 负责一个业务领域,避免"上帝Store"。跨 Store 调用时直接引用对应 Store。
-
避免循环依赖:Store A 引用 Store B,Store B 又引用 Store A 会导致循环依赖,应通过事件或中间层解耦。
-
敏感数据不持久化:权限列表等敏感数据不应持久化到 Storage,应在每次登录后从服务端获取。