主题
概述
Pinia 是 Vue 3 官方推荐的状态管理库,是 Vuex 的继任者。它提供了更简洁的 API、完整的 TypeScript 支持和开发工具集成。
何时使用 Pinia?
并非所有状态都需要 Pinia。遵循以下原则:
| 场景 | 推荐方案 |
|---|---|
| 组件内部状态 | ref() / reactive() |
| 父子组件共享 | props / emit / provide / inject |
| 服务端数据缓存 | TanStack Query (composables) |
| 只在组件树中共享 | Composables |
| 需要在非组件环境访问 | Pinia |
| 全局共享 + DevTools | Pinia |
必须使用 Pinia 的场景
用户认证状态
认证状态需要在路由守卫、HTTP 拦截器等非组件环境中访问,必须使用 Pinia:
stores/auth.ts
typescript
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { StorageSerializers, useStorage } from '@vueuse/core'
import { authApi } from '@/api/auth'
export interface User {
id: number
name: string
email: string
role?: string
}
export const useAuthStore = defineStore('auth', () => {
// State - 使用 useStorage 持久化到 localStorage
const user = useStorage<User | null>('user', null, undefined, {
serializer: StorageSerializers.object,
})
const token = useStorage<string | null>('token', null)
// Getters
const isAuthenticated = computed(() => !!user.value && !!token.value)
const isAdmin = computed(() => user.value?.role === 'admin')
// Actions
const login = async (email: string, password: string) => {
const response = await authApi.login(email, password)
user.value = response.user
token.value = response.access_token
}
const register = async (name: string, email: string, password: string) => {
const response = await authApi.register(name, email, password)
user.value = response.user
token.value = response.access_token
}
const logout = () => {
user.value = null
token.value = null
}
const fetchCurrentUser = async () => {
if (!token.value) return null
try {
const currentUser = await authApi.getCurrentUser()
user.value = currentUser
return currentUser
} catch {
logout()
return null
}
}
return {
// State
user,
token,
// Getters
isAuthenticated,
isAdmin,
// Actions
login,
register,
logout,
fetchCurrentUser,
}
})在非组件环境使用
api/client.ts
typescript
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'
const httpClient = axios.create({
baseURL: '/api',
})
// 请求拦截器:自动注入 Token
httpClient.interceptors.request.use((config) => {
const authStore = useAuthStore()
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
})
// 响应拦截器:处理 401
httpClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
const authStore = useAuthStore()
authStore.logout()
window.location.href = '/auth/login'
}
return Promise.reject(error)
}
)router/index.ts
typescript
import { createRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
// ...
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next('/auth/login')
return
}
if (to.meta.permissions) {
const requiredPerms = to.meta.permissions as string[]
const userRole = authStore.user?.role
if (!requiredPerms.includes(userRole || '')) {
next('/403')
return
}
}
next()
})Store 定义方式
Setup Store(推荐)
Composition API 风格
typescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
// getters
const doubleCount = computed(() => count.value * 2)
// actions
const increment = () => {
count.value++
}
const incrementAsync = async () => {
await new Promise(resolve => setTimeout(resolve, 1000))
count.value++
}
return {
count,
doubleCount,
increment,
incrementAsync,
}
})Options Store
Options API 风格
typescript
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
},
},
})在组件中使用
使用 Store
vue
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'
import { storeToRefs } from 'pinia'
const authStore = useAuthStore()
// 方式一:直接使用(响应式)
// 注意:直接解构会丢失响应式!
const user = authStore.user // ✅ 响应式
const { user } = authStore // ❌ 丢失响应式
// 方式二:使用 storeToRefs 解构(保持响应式)
const { user, isAuthenticated } = storeToRefs(authStore)
// actions 直接解构即可
const { login, logout } = authStore
const handleLogin = async () => {
await login('admin@example.com', 'password')
}
</script>
<template>
<div v-if="isAuthenticated">
欢迎,{{ user?.name }}
<Button @click="logout">退出</Button>
</div>
<div v-else>
<Button @click="handleLogin">登录</Button>
</div>
</template>解构响应式
直接解构 store 会丢失响应式,必须使用 storeToRefs():
typescript
// ❌ 丢失响应式
const { user, isAuthenticated } = authStore
// ✅ 保持响应式
const { user, isAuthenticated } = storeToRefs(authStore)状态持久化
使用 VueUse 的 useStorage 实现持久化:
持久化状态
typescript
import { useStorage, StorageSerializers } from '@vueuse/core'
export const useSettingsStore = defineStore('settings', () => {
// 简单类型
const theme = useStorage<'light' | 'dark'>('theme', 'light')
const locale = useStorage('locale', 'zh-CN')
// 复杂类型需要 serializer
const preferences = useStorage<{ fontSize: number; sidebar: boolean }>(
'preferences',
{ fontSize: 14, sidebar: true },
undefined,
{ serializer: StorageSerializers.object }
)
return { theme, locale, preferences }
})组合多个 Store
Store 组合
typescript
import { defineStore } from 'pinia'
import { useAuthStore } from './auth'
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
// 使用其他 store
const authStore = useAuthStore()
const addItem = async (productId: string) => {
if (!authStore.isAuthenticated) {
throw new Error('请先登录')
}
// 添加到购物车...
}
return { items, addItem }
})最佳实践
Pinia 使用建议
- 最小化使用:能用 composables/TanStack Query 就不用 Pinia
- 合理拆分:每个 store 只负责一个领域
- 持久化:需要持久化的状态使用
useStorage - 类型安全:使用 TypeScript,定义好所有类型
- 命名规范:store 名称使用
use[Feature]Store
不推荐使用 Pinia 的场景
| 场景 | 替代方案 |
|---|---|
| 服务器数据缓存 | TanStack Query |
| 只在几个组件间共享的状态 | Composables |
| 表单状态 | TanStack Form |
| 临时 UI 状态(弹窗开关等) | 组件本地 ref |
使用 Composable 替代
typescript
// ✅ 使用 composable 而非 pinia
// composables/useCart.ts
export function useCart() {
const items = ref<CartItem[]>([])
const addItem = (item: CartItem) => {
items.value.push(item)
}
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0)
)
return { items, addItem, total }
}