Appearance
Pinia State管理
State是Pinia Store的核心组成部分,用于存储应用的数据。本章将详细介绍如何有效地管理和操作State,包括响应式、持久化、批量更新等高级主题。
State基础概念
State定义
在Pinia中,State是一个返回初始状态的函数,这确保了每个组件实例都能获得状态的独立副本。
typescript
import { defineStore } from 'pinia'
// 基础State定义
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo',
isAdmin: false
})
})
State类型声明
typescript
// 使用TypeScript进行类型声明
interface UserInfo {
id: string
name: string
email: string
avatar: string
}
interface Preferences {
theme: 'light' | 'dark'
language: string
notifications: boolean
}
export const useUserStore = defineStore('user', {
state: (): {
userInfo: UserInfo | null,
preferences: Preferences,
loading: boolean
} => ({
userInfo: null,
preferences: {
theme: 'light',
language: 'en',
notifications: true
},
loading: false
})
})
State响应式原理
Vue响应式系统集成
Pinia的State完全集成Vue的响应式系统,这意味着当State改变时,使用这些状态的组件会自动更新。
typescript
import { defineStore } from 'pinia'
export const useTodosStore = defineStore('todos', {
state: () => ({
todos: [] as Todo[],
filter: 'all' as 'all' | 'active' | 'completed'
}),
getters: {
filteredTodos: (state) => {
switch (state.filter) {
case 'active':
return state.todos.filter(todo => !todo.completed)
case 'completed':
return state.todos.filter(todo => todo.completed)
default:
return state.todos
}
},
activeCount: (state) => {
return state.todos.filter(todo => !todo.completed).length
}
},
actions: {
addTodo(text: string) {
const newTodo: Todo = {
id: Date.now().toString(),
text,
completed: false
}
this.todos.push(newTodo) // 响应式更新
},
toggleTodo(id: string) {
const todo = this.todos.find(todo => todo.id === id)
if (todo) {
todo.completed = !todo.completed // 响应式更新
}
}
}
})
interface Todo {
id: string
text: string
completed: boolean
}
在组件中使用响应式State
vue
<template>
<div>
<h2>Todos ({{ todosStore.activeCount }} active)</h2>
<!-- 添加新任务 -->
<form @submit.prevent="addTodo">
<input
v-model="newTodo"
placeholder="Add new todo..."
:disabled="todosStore.loading"
/>
<button type="submit" :disabled="!newTodo || todosStore.loading">
Add
</button>
</form>
<!-- 过滤器 -->
<div class="filters">
<button
@click="setFilter('all')"
:class="{ active: todosStore.filter === 'all' }"
>
All
</button>
<button
@click="setFilter('active')"
:class="{ active: todosStore.filter === 'active' }"
>
Active
</button>
<button
@click="setFilter('completed')"
:class="{ active: todosStore.filter === 'completed' }"
>
Completed
</button>
</div>
<!-- 任务列表 -->
<ul>
<li
v-for="todo in todosStore.filteredTodos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
/>
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">Remove</button>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useTodosStore } from '@/stores/todos'
const todosStore = useTodosStore()
const newTodo = ref('')
const addTodo = () => {
if (newTodo.value.trim()) {
todosStore.addTodo(newTodo.value.trim())
newTodo.value = ''
}
}
const toggleTodo = (id: string) => {
todosStore.toggleTodo(id)
}
const removeTodo = (id: string) => {
todosStore.todos = todosStore.todos.filter(todo => todo.id !== id)
}
const setFilter = (filter: 'all' | 'active' | 'completed') => {
todosStore.filter = filter
}
</script>
<style scoped>
.completed {
text-decoration: line-through;
opacity: 0.6;
}
.filters button {
margin-right: 8px;
}
.filters button.active {
font-weight: bold;
color: blue;
}
</style>
State修改方式
直接修改State
在Pinia的Actions中,可以直接修改State:
typescript
export const useProfileStore = defineStore('profile', {
state: () => ({
profile: {
name: '',
email: '',
bio: '',
avatar: ''
} as UserProfile,
editing: false
}),
actions: {
// 直接修改整个对象
setProfile(profile: UserProfile) {
this.profile = profile
},
// 直接修改属性
setName(name: string) {
this.profile.name = name
},
setEmail(email: string) {
this.profile.email = email
},
setBio(bio: string) {
this.profile.bio = bio
},
// 批量更新
updateProfile(updates: Partial<UserProfile>) {
this.profile = { ...this.profile, ...updates }
},
// 使用Object.assign
assignProfile(updates: Partial<UserProfile>) {
Object.assign(this.profile, updates)
},
// 开始编辑
startEdit() {
this.editing = true
},
// 完成编辑
finishEdit() {
this.editing = false
}
}
})
interface UserProfile {
name: string
email: string
bio: string
avatar: string
}
使用$patch方法
$patch方法允许你批量更新State,这对于复杂的更新操作非常有用:
typescript
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
total: 0,
discount: 0,
shipping: 0,
tax: 0,
currency: 'USD' as Currency
}),
actions: {
// 使用对象进行批量更新
updateCart(updates: Partial<CartState>) {
this.$patch(updates)
},
// 使用函数进行复杂的批量更新
applyDiscountAndShipping(discount: number, shipping: number) {
this.$patch((state) => {
state.discount = discount
state.shipping = shipping
state.total = this.calculateTotal()
})
},
// 添加商品到购物车
addToCart(item: CartItem) {
const existingItem = this.items.find(i => i.id === item.id)
if (existingItem) {
this.$patch((state) => {
existingItem.quantity += item.quantity
state.total = this.calculateTotal()
})
} else {
this.$patch((state) => {
state.items.push(item)
state.total = this.calculateTotal()
})
}
},
// 更新商品数量
updateItemQuantity(id: string, quantity: number) {
if (quantity <= 0) {
this.removeItem(id)
return
}
this.$patch((state) => {
const item = state.items.find(i => i.id === id)
if (item) {
item.quantity = quantity
state.total = this.calculateTotal()
}
})
},
// 移除商品
removeItem(id: string) {
this.$patch((state) => {
state.items = state.items.filter(item => item.id !== id)
state.total = this.calculateTotal()
})
},
// 清空购物车
clearCart() {
this.$patch({
items: [],
total: 0,
discount: 0
})
},
// 内部辅助方法
calculateTotal(): number {
const subtotal = this.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
)
return Math.max(0, subtotal - this.discount + this.shipping + this.tax)
}
}
})
interface CartItem {
id: string
name: string
price: number
quantity: number
}
type Currency = 'USD' | 'EUR' | 'CNY'
type CartState = ReturnType<typeof useCartStore>['$state']
State持久化
使用pinia-plugin-persistedstate
首先安装持久化插件:
bash
npm install pinia-plugin-persistedstate
然后在main.js中配置:
javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.mount('#app')
基础持久化配置
typescript
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
id: null as string | null,
name: '',
email: '',
preferences: {
theme: 'light' as 'light' | 'dark',
language: 'en',
notifications: true
},
lastLogin: null as string | null
}),
// 启用持久化
persist: true
})
高级持久化配置
typescript
export const useSettingsStore = defineStore('settings', {
state: () => ({
general: {
theme: 'light' as 'light' | 'dark',
language: 'en',
dateFormat: 'MM/DD/YYYY'
},
notifications: {
email: true,
push: false,
sound: true
},
privacy: {
shareData: false,
marketing: false
},
advanced: {
autoSave: true,
offlineMode: false,
experimentalFeatures: false
}
}),
persist: [
// 持久化整个store
{
key: 'app_settings',
storage: localStorage,
// 只持久化general和notifications部分
pick: ['general', 'notifications']
},
// 只持久化privacy部分,使用sessionStorage
{
key: 'privacy_settings',
storage: sessionStorage,
pick: ['privacy']
}
],
actions: {
updateGeneralSettings(settings: Partial<Settings['general']>) {
this.general = { ...this.general, ...settings }
},
updateNotificationSettings(settings: Partial<Settings['notifications']>) {
this.notifications = { ...this.notifications, ...settings }
},
resetToDefaults() {
this.$patch({
general: {
theme: 'light',
language: 'en',
dateFormat: 'MM/DD/YYYY'
},
notifications: {
email: true,
push: false,
sound: true
},
privacy: {
shareData: false,
marketing: false
}
})
}
}
})
interface Settings {
general: {
theme: 'light' | 'dark'
language: string
dateFormat: string
}
notifications: {
email: boolean
push: boolean
sound: boolean
}
privacy: {
shareData: boolean
marketing: boolean
}
advanced: {
autoSave: boolean
offlineMode: boolean
experimentalFeatures: boolean
}
}
自定义持久化逻辑
typescript
export const useSessionStore = defineStore('session', {
state: () => ({
userId: null as string | null,
sessionId: null as string | null,
expiresAt: null as number | null,
permissions: [] as string[],
lastActivity: Date.now()
}),
persist: {
key: 'user_session',
storage: localStorage,
serializer: {
// 自定义序列化方法
serialize: (state) => {
// 在序列化前处理敏感数据
const serialized = { ...state }
// 不序列化某些敏感字段
if (serialized.sessionId) {
// 可以对敏感数据进行加密
serialized.sessionId = btoa(serialized.sessionId) // 简单示例
}
return JSON.stringify(serialized)
},
// 自定义反序列化方法
deserialize: (value) => {
const state = JSON.parse(value)
// 在反序列化后处理数据
if (state.sessionId) {
state.sessionId = atob(state.sessionId) // 简单解密示例
}
return state
}
},
// 自定义存储逻辑
write: (state, storage, key) => {
// 检查会话是否过期
if (state.expiresAt && Date.now() > state.expiresAt) {
// 会话过期,清除存储
storage.removeItem(key)
return
}
// 更新最后活动时间
state.lastActivity = Date.now()
storage.setItem(key, JSON.stringify(state))
},
// 自定义读取逻辑
read: (storage, key) => {
const value = storage.getItem(key)
if (!value) return undefined
const state = JSON.parse(value)
// 检查会话是否过期
if (state.expiresAt && Date.now() > state.expiresAt) {
storage.removeItem(key)
return undefined
}
return state
}
},
getters: {
isExpired: (state) => {
return state.expiresAt ? Date.now() > state.expiresAt : true
},
isValid: (state) => {
return !state.isExpired && state.sessionId !== null
}
},
actions: {
createSession(userId: string, permissions: string[] = []) {
this.userId = userId
this.sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
this.expiresAt = Date.now() + 24 * 60 * 60 * 1000 // 24小时后过期
this.permissions = permissions
this.lastActivity = Date.now()
},
extendSession() {
if (this.sessionId) {
this.expiresAt = Date.now() + 24 * 60 * 60 * 1000 // 延长24小时
this.lastActivity = Date.now()
}
},
clearSession() {
this.userId = null
this.sessionId = null
this.expiresAt = null
this.permissions = []
}
}
})
State变更监听
使用Store订阅
typescript
import { watch } from 'vue'
export const useLoggerStore = defineStore('logger', {
state: () => ({
logs: [] as LogEntry[],
maxLogs: 1000
}),
actions: {
addLog(level: LogLevel, message: string, data?: any) {
const logEntry: LogEntry = {
id: Date.now().toString(),
timestamp: new Date().toISOString(),
level,
message,
data
}
this.logs.push(logEntry)
// 限制日志数量
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs)
}
},
clearLogs() {
this.logs = []
},
error(message: string, data?: any) {
this.addLog('error', message, data)
},
warn(message: string, data?: any) {
this.addLog('warn', message, data)
},
info(message: string, data?: any) {
this.addLog('info', message, data)
}
}
})
// 在Store定义之外设置监听
export function setupStoreListeners() {
const loggerStore = useLoggerStore()
// 监听状态变化
const counterStore = useCounterStore()
const unsubscribe = counterStore.$subscribe((mutation, state) => {
loggerStore.info(`Counter changed: ${mutation.storeId}.${mutation.type}`, {
newValue: state.count,
mutation
})
}, {
detached: true // 不受组件卸载影响
})
// 监听Store的任何变化
const userStore = useUserStore()
userStore.$subscribe((mutation, state) => {
if (mutation.type === 'direct') {
loggerStore.info(`User state changed`, {
mutation,
state
})
}
})
return unsubscribe
}
type LogLevel = 'info' | 'warn' | 'error'
interface LogEntry {
id: string
timestamp: string
level: LogLevel
message: string
data?: any
}
深度监听State
typescript
export const useDeepWatchStore = defineStore('deepWatch', {
state: () => ({
complexData: {
users: [] as User[],
settings: {
filters: {
age: { min: 0, max: 100 },
department: [] as string[],
active: true
},
sorting: {
field: 'name',
order: 'asc' as 'asc' | 'desc'
}
},
metadata: {
lastUpdated: new Date(),
version: '1.0.0'
}
}
}),
actions: {
// 初始化监听器
initWatchers() {
// 监听特定嵌套属性的变化
this.$subscribe((mutation, state) => {
// 检查是否是filters的变更
if (mutation.type === 'direct' &&
(mutation.events as any).key?.includes('filters')) {
this.handleFiltersChange()
}
})
},
handleFiltersChange() {
console.log('Filters changed, updating user list...')
// 根据新的过滤器重新计算用户列表
this.applyFilters()
},
applyFilters() {
// 应用过滤器逻辑
const { min, max } = this.complexData.settings.filters.age
const departments = this.complexData.settings.filters.department
const activeOnly = this.complexData.settings.filters.active
// 这里实现具体的过滤逻辑
console.log('Applied filters:', { min, max, departments, activeOnly })
},
updateFilterAge(min: number, max: number) {
this.complexData.settings.filters.age = { min, max }
},
updateFilterDepartments(departments: string[]) {
this.complexData.settings.filters.department = departments
},
updateFilterActive(active: boolean) {
this.complexData.settings.filters.active = active
}
}
})
interface User {
id: string
name: string
age: number
department: string
active: boolean
}
State性能优化
避免不必要的重渲染
typescript
export const useOptimizedStore = defineStore('optimized', {
state: () => ({
// 大量数据
largeDataset: [] as DataItem[],
// 计算结果缓存
computedResults: new Map<string, ComputedResult>(),
// UI状态(不需要响应式)
uiState: {
sidebarOpen: false,
modalVisible: false,
currentTab: 'overview'
}
}),
getters: {
// 使用computed包装昂贵的计算
expensiveCalculation: (state) => {
return computed(() => {
// 这里的计算只在依赖变化时重新执行
return state.largeDataset
.filter(item => item.active)
.reduce((sum, item) => sum + item.value, 0)
})
},
// 分页数据
getPaginatedData: (state) => {
return (page: number, pageSize: number) => {
const start = (page - 1) * pageSize
return state.largeDataset.slice(start, start + pageSize)
}
}
},
actions: {
// 批量更新大数据集
batchUpdateData(updates: DataUpdate[]) {
// 使用$patch进行批量更新,减少重渲染次数
this.$patch((state) => {
for (const update of updates) {
const item = state.largeDataset.find(d => d.id === update.id)
if (item) {
Object.assign(item, update.changes)
}
}
})
},
// 更新UI状态(非响应式)
updateUiState(newState: Partial<UiState>) {
// 对于纯UI状态,可以考虑不使用响应式
// 但这取决于具体需求
this.uiState = { ...this.uiState, ...newState }
},
// 清理缓存
clearComputedCache() {
this.computedResults.clear()
}
}
})
interface DataItem {
id: string
value: number
active: boolean
category: string
}
interface ComputedResult {
id: string
result: any
timestamp: number
}
interface DataUpdate {
id: string
changes: Partial<DataItem>
}
interface UiState {
sidebarOpen: boolean
modalVisible: boolean
currentTab: string
}
使用可选的响应式
typescript
import { shallowRef, triggerRef } from 'vue'
export const useShallowStore = defineStore('shallow', {
state: () => ({
// 使用浅层响应式存储大型对象
largeObject: shallowRef({} as LargeObject),
// 普通响应式数据
metadata: {
loaded: false,
lastUpdate: null as number | null
}
}),
actions: {
// 更新大型对象而不触发深层响应
updateLargeObject(partialUpdate: Partial<LargeObject>) {
const current = this.largeObject.value
this.largeObject.value = { ...current, ...partialUpdate }
// 如果需要强制更新,使用triggerRef
// triggerRef(this.largeObject)
},
// 批量更新大型对象
batchUpdateLargeObject(updates: Array<Partial<LargeObject>>) {
let current = this.largeObject.value
for (const update of updates) {
current = { ...current, ...update }
}
this.largeObject.value = current
},
markAsLoaded() {
this.metadata.loaded = true
this.metadata.lastUpdate = Date.now()
}
}
})
interface LargeObject {
data: any[]
settings: Record<string, any>
cache: Map<string, any>
// ... 其他大型数据结构
}
State测试
State单元测试
typescript
// stores/__tests__/counter.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../counter'
describe('Counter Store', () => {
beforeEach(() => {
// 创建新的pinia实例
setActivePinia(createPinia())
})
it('should initialize with default count of 0', () => {
const counter = useCounterStore()
expect(counter.count).toBe(0)
})
it('should increment count', () => {
const counter = useCounterStore()
counter.increment()
expect(counter.count).toBe(1)
})
it('should decrement count', () => {
const counter = useCounterStore()
counter.count = 5
counter.decrement()
expect(counter.count).toBe(4)
})
it('should compute double count', () => {
const counter = useCounterStore()
counter.count = 4
expect(counter.doubleCount).toBe(8)
})
it('should handle async increment', async () => {
const counter = useCounterStore()
await counter.incrementAsync()
expect(counter.count).toBe(1)
})
})
// stores/__tests__/user.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '../user'
import { vi, describe, it, beforeEach, expect } from 'vitest'
describe('User Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should handle user login', async () => {
const userStore = useUserStore()
// Mock API response
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({
id: '123',
name: 'John Doe',
email: 'john@example.com'
})
} as Response)
)
await userStore.login({ email: 'test@example.com', password: 'password' })
expect(userStore.user).toEqual({
id: '123',
name: 'John Doe',
email: 'john@example.com'
})
expect(userStore.isAuthenticated).toBe(true)
})
it('should handle login error', async () => {
const userStore = useUserStore()
// Mock API error response
global.fetch = vi.fn(() =>
Promise.resolve({
ok: false,
status: 401
} as Response)
)
await expect(
userStore.login({ email: 'test@example.com', password: 'wrong' })
).rejects.toThrow()
expect(userStore.error).toBeDefined()
})
})
最佳实践
1. State结构设计
typescript
// 推荐:扁平化状态结构
export const useRecommendedStore = defineStore('recommended', {
state: () => ({
// 将相关数据组织在一起
user: {
profile: null as UserProfile | null,
preferences: {} as UserPreferences,
sessions: [] as Session[]
},
// 分离UI状态和业务状态
ui: {
loading: false,
error: null as string | null,
notifications: [] as Notification[]
},
// 使用ID引用关联数据
entities: {
posts: {} as Record<string, Post>,
comments: {} as Record<string, Comment>
},
relations: {
userPosts: {} as Record<string, string[]> // userId -> postIds
}
})
})
// 避免:过度嵌套的状态结构
export const useAvoidStore = defineStore('avoid', {
state: () => ({
complexNestedData: {
level1: {
level2: {
level3: {
actualData: 'hard to access'
}
}
}
}
})
})
2. State初始化
typescript
export const useInitializedStore = defineStore('initialized', {
state: () => ({
// 从环境变量或配置初始化
apiUrl: process.env.VUE_APP_API_URL || 'https://api.example.com',
// 从本地存储初始化
theme: localStorage.getItem('theme') as 'light' | 'dark' || 'light',
// 从URL参数初始化
locale: new URLSearchParams(window.location.search).get('lang') || 'en',
// 从设备特性初始化
prefersDark: window.matchMedia('(prefers-color-scheme: dark)').matches
}),
actions: {
initializeFromStorage() {
const storedTheme = localStorage.getItem('user-theme')
if (storedTheme) {
this.theme = storedTheme as 'light' | 'dark'
}
}
}
})
3. 状态验证
typescript
export const useValidatedStore = defineStore('validated', {
state: () => ({
email: '',
age: 0,
preferences: {
theme: 'light' as 'light' | 'dark',
newsletter: false
}
}),
actions: {
setEmail(newEmail: string) {
if (!this.isValidEmail(newEmail)) {
throw new Error('Invalid email format')
}
this.email = newEmail
},
setAge(newAge: number) {
if (newAge < 0 || newAge > 150) {
throw new Error('Age must be between 0 and 150')
}
this.age = newAge
},
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
}
})
总结
State管理是Pinia的核心功能,通过本章的学习,你应该掌握:
- State的基础定义和类型声明
- 响应式系统的集成和使用
- 多种State修改方式:直接修改和$patch
- State持久化的实现
- State变更监听机制
- 性能优化技巧
- State测试方法
- 最佳实践原则
有效的State管理是构建可维护、高性能Vue应用的关键,确保遵循这些原则来组织你的应用状态。