Skip to content
On this page

Pinia Actions

Actions是Pinia Store中的方法,用于改变State或执行业务逻辑。它们类似于传统Redux中的thunks或Vuex中的mutations和actions。本章将详细介绍Actions的使用方法、异步操作处理、错误处理和最佳实践。

Actions基础

定义和使用Actions

typescript
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    lastChanged: null as Date | null
  }),
  
  actions: {
    // 同步Action
    increment() {
      this.count++
      this.lastChanged = new Date()
    },
    
    decrement() {
      this.count--
      this.lastChanged = new Date()
    },
    
    incrementBy(amount: number) {
      this.count += amount
      this.lastChanged = new Date()
    },
    
    reset() {
      this.count = 0
      this.lastChanged = new Date()
    }
  }
})

在组件中使用Actions

vue
<template>
  <div>
    <p>Count: {{ counterStore.count }}</p>
    <p>Last Changed: {{ counterStore.lastChanged?.toLocaleString() }}</p>
    
    <button @click="counterStore.increment()">+1</button>
    <button @click="counterStore.decrement()">-1</button>
    <button @click="counterStore.incrementBy(5)">+5</button>
    <button @click="counterStore.reset()">Reset</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()
</script>

异步Actions

基础异步操作

typescript
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null as User | null,
    loading: false,
    error: null as string | null,
    lastFetchTime: null as number | null
  }),
  
  actions: {
    // 异步获取用户信息
    async fetchUser(userId: string) {
      this.loading = true
      this.error = null
      
      try {
        // 模拟API调用
        const response = await fetch(`/api/users/${userId}`)
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`)
        }
        
        const userData = await response.json()
        this.user = userData
        this.lastFetchTime = Date.now()
      } catch (error) {
        this.error = (error as Error).message
        console.error('Failed to fetch user:', error)
      } finally {
        this.loading = false
      }
    },
    
    // 异步更新用户信息
    async updateUser(userData: Partial<User>) {
      if (!this.user) {
        throw new Error('No user to update')
      }
      
      this.loading = true
      this.error = null
      
      try {
        const response = await fetch(`/api/users/${this.user.id}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(userData)
        })
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`)
        }
        
        const updatedUser = await response.json()
        this.user = { ...this.user, ...updatedUser }
        this.lastFetchTime = Date.now()
      } catch (error) {
        this.error = (error as Error).message
        console.error('Failed to update user:', error)
      } finally {
        this.loading = false
      }
    },
    
    // 异步删除用户
    async deleteUser() {
      if (!this.user) {
        throw new Error('No user to delete')
      }
      
      this.loading = true
      this.error = null
      
      try {
        const response = await fetch(`/api/users/${this.user.id}`, {
          method: 'DELETE'
        })
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`)
        }
        
        this.user = null
      } catch (error) {
        this.error = (error as Error).message
        console.error('Failed to delete user:', error)
      } finally {
        this.loading = false
      }
    }
  }
})

interface User {
  id: string
  name: string
  email: string
  avatar?: string
}

在组件中使用异步Actions

vue
<template>
  <div>
    <h2>User Profile</h2>
    
    <!-- 加载状态 -->
    <div v-if="userStore.loading" class="loading">
      Loading...
    </div>
    
    <!-- 错误状态 -->
    <div v-else-if="userStore.error" class="error">
      Error: {{ userStore.error }}
      <button @click="retry">Retry</button>
    </div>
    
    <!-- 用户信息 -->
    <div v-else-if="userStore.user" class="user-info">
      <img :src="userStore.user.avatar" alt="Avatar" v-if="userStore.user.avatar" />
      <h3>{{ userStore.user.name }}</h3>
      <p>Email: {{ userStore.user.email }}</p>
      
      <div class="actions">
        <button @click="editUser">Edit</button>
        <button @click="deleteUser" class="danger">Delete</button>
      </div>
    </div>
    
    <!-- 无用户状态 -->
    <div v-else class="no-user">
      No user loaded
      <button @click="loadUser">Load User</button>
    </div>
    
    <!-- 编辑表单 -->
    <div v-if="editing" class="edit-form">
      <h3>Edit User</h3>
      <form @submit.prevent="saveUser">
        <div>
          <label>Name:</label>
          <input v-model="editForm.name" />
        </div>
        <div>
          <label>Email:</label>
          <input v-model="editForm.email" type="email" />
        </div>
        <button type="submit">Save</button>
        <button type="button" @click="cancelEdit">Cancel</button>
      </form>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const editing = ref(false)
const editForm = reactive({
  name: '',
  email: ''
})

const loadUser = () => {
  userStore.fetchUser('123')
}

const retry = () => {
  if (userStore.user) {
    userStore.fetchUser(userStore.user.id)
  }
}

const editUser = () => {
  if (userStore.user) {
    editForm.name = userStore.user.name
    editForm.email = userStore.user.email
    editing.value = true
  }
}

const saveUser = async () => {
  await userStore.updateUser(editForm)
  editing.value = false
}

const cancelEdit = () => {
  editing.value = false
}

const deleteUser = async () => {
  if (confirm('Are you sure you want to delete this user?')) {
    await userStore.deleteUser()
  }
}
</script>

<style scoped>
.loading, .error, .no-user {
  padding: 20px;
  text-align: center;
}

.error {
  color: red;
}

.user-info {
  padding: 20px;
}

.actions {
  margin-top: 20px;
}

.danger {
  background-color: #dc3545;
  color: white;
}

.edit-form {
  margin-top: 20px;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.edit-form form div {
  margin-bottom: 10px;
}

.edit-form label {
  display: inline-block;
  width: 80px;
}
</style>

Actions中的状态管理

批量状态更新

typescript
export const useBatchUpdateStore = defineStore('batchUpdate', {
  state: () => ({
    items: [] as Item[],
    selectedItems: [] as string[],
    loading: false,
    error: null as string | null,
    lastAction: null as string | null
  }),
  
  actions: {
    // 批量更新项目
    async batchUpdateItems(updates: Array<{ id: string; changes: Partial<Item> }>) {
      this.loading = true
      this.error = null
      
      try {
        // 使用Promise.all并行处理更新
        const updatePromises = updates.map(update => 
          this.updateSingleItem(update.id, update.changes)
        )
        
        await Promise.all(updatePromises)
        
        this.lastAction = `Updated ${updates.length} items`
      } catch (error) {
        this.error = (error as Error).message
        throw error // 重新抛出错误供调用者处理
      } finally {
        this.loading = false
      }
    },
    
    // 更新单个项目(私有辅助方法)
    async updateSingleItem(id: string, changes: Partial<Item>): Promise<void> {
      const response = await fetch(`/api/items/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(changes)
      })
      
      if (!response.ok) {
        throw new Error(`Failed to update item ${id}: ${response.statusText}`)
      }
      
      // 更新本地状态
      const itemIndex = this.items.findIndex(item => item.id === id)
      if (itemIndex !== -1) {
        this.items[itemIndex] = { ...this.items[itemIndex], ...changes }
      }
    },
    
    // 批量删除
    async batchDeleteItems(ids: string[]) {
      this.loading = true
      this.error = null
      
      try {
        // 使用Promise.allSettled确保所有删除操作都执行
        const results = await Promise.allSettled(
          ids.map(id => this.deleteSingleItem(id))
        )
        
        // 检查是否有失败的操作
        const failed = results.filter(result => result.status === 'rejected')
        if (failed.length > 0) {
          throw new Error(`${failed.length} items failed to delete`)
        }
        
        this.lastAction = `Deleted ${ids.length} items`
      } catch (error) {
        this.error = (error as Error).message
        throw error
      } finally {
        this.loading = false
      }
    },
    
    // 删除单个项目
    async deleteSingleItem(id: string) {
      const response = await fetch(`/api/items/${id}`, {
        method: 'DELETE'
      })
      
      if (!response.ok) {
        throw new Error(`Failed to delete item ${id}: ${response.statusText}`)
      }
      
      // 从本地状态移除
      this.items = this.items.filter(item => item.id !== id)
      this.selectedItems = this.selectedItems.filter(selectedId => selectedId !== id)
    },
    
    // 选择/取消选择项目
    toggleItemSelection(id: string) {
      const index = this.selectedItems.indexOf(id)
      if (index > -1) {
        this.selectedItems.splice(index, 1)
      } else {
        this.selectedItems.push(id)
      }
    },
    
    // 选择所有/取消选择所有
    toggleSelectAll() {
      if (this.selectedItems.length === this.items.length) {
        this.selectedItems = []
      } else {
        this.selectedItems = this.items.map(item => item.id)
      }
    },
    
    // 添加新项目
    async addItem(itemData: Omit<Item, 'id'>) {
      this.loading = true
      this.error = null
      
      try {
        const response = await fetch('/api/items', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(itemData)
        })
        
        if (!response.ok) {
          throw new Error(`Failed to add item: ${response.statusText}`)
        }
        
        const newItem = await response.json()
        this.items.push(newItem)
        this.lastAction = 'Added new item'
      } catch (error) {
        this.error = (error as Error).message
        throw error
      } finally {
        this.loading = false
      }
    }
  }
})

interface Item {
  id: string
  name: string
  description: string
  status: 'active' | 'inactive' | 'pending'
  createdAt: string
}

Actions中的事务处理

typescript
export const useTransactionStore = defineStore('transaction', {
  state: () => ({
    accounts: [] as Account[],
    transactions: [] as Transaction[],
    balances: new Map<string, number>(), // account id -> balance
    processing: false,
    errors: [] as string[]
  }),
  
  actions: {
    // 转账操作 - 包含事务逻辑
    async transferFunds(fromAccountId: string, toAccountId: string, amount: number) {
      if (this.processing) {
        throw new Error('Another transaction is in progress')
      }
      
      if (amount <= 0) {
        throw new Error('Transfer amount must be positive')
      }
      
      if (fromAccountId === toAccountId) {
        throw new Error('Cannot transfer to the same account')
      }
      
      this.processing = true
      this.errors = []
      
      try {
        // 检查余额
        const fromBalance = this.balances.get(fromAccountId) || 0
        if (fromBalance < amount) {
          throw new Error('Insufficient funds')
        }
        
        // 验证账户存在
        const fromAccount = this.accounts.find(acc => acc.id === fromAccountId)
        const toAccount = this.accounts.find(acc => acc.id === toAccountId)
        
        if (!fromAccount || !toAccount) {
          throw new Error('One or both accounts do not exist')
        }
        
        // 记录转账前状态(用于回滚)
        const originalFromBalance = fromBalance
        const originalToBalance = this.balances.get(toAccountId) || 0
        
        // 执行转账
        const success = await this.executeTransfer(fromAccountId, toAccountId, amount)
        
        if (success) {
          // 更新本地状态
          this.balances.set(fromAccountId, originalFromBalance - amount)
          this.balances.set(toAccountId, originalToBalance + amount)
          
          // 记录交易
          const transaction: Transaction = {
            id: `txn_${Date.now()}`,
            from: fromAccountId,
            to: toAccountId,
            amount,
            timestamp: new Date().toISOString(),
            status: 'completed'
          }
          
          this.transactions.unshift(transaction)
        } else {
          throw new Error('Transfer failed at the backend')
        }
      } catch (error) {
        this.errors.push((error as Error).message)
        throw error
      } finally {
        this.processing = false
      }
    },
    
    // 执行转账(模拟API调用)
    async executeTransfer(fromId: string, toId: string, amount: number): Promise<boolean> {
      // 模拟网络请求
      await new Promise(resolve => setTimeout(resolve, 1000))
      
      // 模拟可能的失败情况
      if (Math.random() < 0.1) { // 10% 失败率
        return false
      }
      
      return true
    },
    
    // 获取账户余额
    getAccountBalance(accountId: string): number {
      return this.balances.get(accountId) || 0
    },
    
    // 刷新账户数据
    async refreshAccounts() {
      this.processing = true
      try {
        const response = await fetch('/api/accounts')
        if (!response.ok) throw new Error('Failed to fetch accounts')
        
        const accounts = await response.json()
        this.accounts = accounts
        
        // 重新计算余额
        this.recalculateBalances()
      } catch (error) {
        this.errors.push((error as Error).message)
      } finally {
        this.processing = false
      }
    },
    
    // 重新计算所有账户余额
    recalculateBalances() {
      // 初始化所有账户余额为0
      this.accounts.forEach(account => {
        this.balances.set(account.id, 0)
      })
      
      // 根据交易记录更新余额
      this.transactions.forEach(transaction => {
        if (transaction.status === 'completed') {
          const fromBalance = this.balances.get(transaction.from) || 0
          const toBalance = this.balances.get(transaction.to) || 0
          
          this.balances.set(transaction.from, fromBalance - transaction.amount)
          this.balances.set(transaction.to, toBalance + transaction.amount)
        }
      })
    }
  }
})

interface Account {
  id: string
  name: string
  type: 'checking' | 'savings' | 'credit'
  userId: string
}

interface Transaction {
  id: string
  from: string
  to: string
  amount: number
  timestamp: string
  status: 'pending' | 'completed' | 'failed'
}

Actions间的通信

调用其他Actions

typescript
export const useCompositeActionsStore = defineStore('compositeActions', {
  state: () => ({
    users: [] as User[],
    posts: [] as Post[],
    comments: [] as Comment[],
    loading: false,
    error: null as string | null
  }),
  
  actions: {
    // 设置加载状态
    setLoading(status: boolean) {
      this.loading = status
    },
    
    // 设置错误
    setError(error: string | null) {
      this.error = error
    },
    
    // 获取用户信息
    async fetchUser(userId: string): Promise<User | null> {
      try {
        const response = await fetch(`/api/users/${userId}`)
        if (!response.ok) throw new Error('User not found')
        
        const user = await response.json()
        // 检查用户是否已存在于本地状态
        const existingIndex = this.users.findIndex(u => u.id === user.id)
        if (existingIndex !== -1) {
          this.users[existingIndex] = user
        } else {
          this.users.push(user)
        }
        
        return user
      } catch (error) {
        this.setError((error as Error).message)
        return null
      }
    },
    
    // 获取用户的文章
    async fetchUserPosts(userId: string): Promise<Post[]> {
      try {
        const response = await fetch(`/api/users/${userId}/posts`)
        if (!response.ok) throw new Error('Failed to fetch posts')
        
        const posts = await response.json()
        
        // 更新本地状态
        posts.forEach(post => {
          const existingIndex = this.posts.findIndex(p => p.id === post.id)
          if (existingIndex !== -1) {
            this.posts[existingIndex] = post
          } else {
            this.posts.push(post)
          }
        })
        
        return posts
      } catch (error) {
        this.setError((error as Error).message)
        return []
      }
    },
    
    // 获取文章的评论
    async fetchPostComments(postId: string): Promise<Comment[]> {
      try {
        const response = await fetch(`/api/posts/${postId}/comments`)
        if (!response.ok) throw new Error('Failed to fetch comments')
        
        const comments = await response.json()
        
        // 更新本地状态
        comments.forEach(comment => {
          const existingIndex = this.comments.findIndex(c => c.id === comment.id)
          if (existingIndex !== -1) {
            this.comments[existingIndex] = comment
          } else {
            this.comments.push(comment)
          }
        })
        
        return comments
      } catch (error) {
        this.setError((error as Error).message)
        return []
      }
    },
    
    // 复合Action:获取用户及其相关内容
    async fetchUserWithContent(userId: string) {
      this.setLoading(true)
      this.setError(null)
      
      try {
        // 并行获取用户和文章
        const [user, posts] = await Promise.all([
          this.fetchUser(userId),
          this.fetchUserPosts(userId)
        ])
        
        if (!user) {
          throw new Error('User not found')
        }
        
        // 获取每篇文章的评论(限制并发数)
        const commentPromises = posts.slice(0, 5).map(post => 
          this.fetchPostComments(post.id)
        )
        
        await Promise.all(commentPromises)
        
        return { user, posts, comments: this.comments }
      } catch (error) {
        this.setError((error as Error).message)
        throw error
      } finally {
        this.setLoading(false)
      }
    },
    
    // 清理用户相关内容
    clearUserContent(userId: string) {
      // 移除该用户的文章
      this.posts = this.posts.filter(post => post.userId !== userId)
      
      // 移除该用户的评论
      this.comments = this.comments.filter(comment => comment.userId !== userId)
      
      // 移除用户
      this.users = this.users.filter(user => user.id !== userId)
    },
    
    // 删除用户及所有相关内容
    async deleteUser(userId: string) {
      this.setLoading(true)
      this.setError(null)
      
      try {
        // 先获取用户以确认存在
        const user = await this.fetchUser(userId)
        if (!user) {
          throw new Error('User not found')
        }
        
        // 清理本地状态
        this.clearUserContent(userId)
        
        // 从服务器删除
        const response = await fetch(`/api/users/${userId}`, {
          method: 'DELETE'
        })
        
        if (!response.ok) {
          // 如果服务器删除失败,恢复本地状态
          throw new Error('Failed to delete user on server')
        }
      } catch (error) {
        this.setError((error as Error).message)
        // 重新获取用户数据以恢复一致性
        await this.fetchUser(userId)
        throw error
      } finally {
        this.setLoading(false)
      }
    }
  }
})

interface User {
  id: string
  name: string
  email: string
  avatar?: string
}

interface Post {
  id: string
  title: string
  content: string
  userId: string
  createdAt: string
}

interface Comment {
  id: string
  content: string
  userId: string
  postId: string
  createdAt: string
}

Actions中的错误处理

全面的错误处理策略

typescript
export const useErrorHandlingStore = defineStore('errorHandling', {
  state: () => ({
    data: null as Data[] | null,
    loading: false,
    error: null as ApiError | null,
    retryCount: 0,
    maxRetries: 3,
    lastErrorTime: null as number | null,
    rateLimitReset: null as number | null
  }),
  
  actions: {
    // 重置错误状态
    resetError() {
      this.error = null
      this.retryCount = 0
      this.lastErrorTime = null
    },
    
    // 设置错误信息
    setError(error: unknown, context: string = '') {
      let apiError: ApiError
      
      if (error instanceof Error) {
        apiError = {
          message: error.message,
          status: 'CLIENT_ERROR',
          context,
          timestamp: Date.now()
        }
      } else if (typeof error === 'object' && error !== null) {
        apiError = {
          message: (error as any).message || 'Unknown error',
          status: (error as any).status || 'UNKNOWN',
          context,
          timestamp: Date.now(),
          details: error
        }
      } else {
        apiError = {
          message: String(error),
          status: 'UNKNOWN',
          context,
          timestamp: Date.now()
        }
      }
      
      this.error = apiError
      this.lastErrorTime = Date.now()
      this.retryCount++
    },
    
    // 检查是否应该重试
    shouldRetry(): boolean {
      if (this.retryCount >= this.maxRetries) {
        return false
      }
      
      // 检查是否在速率限制期间
      if (this.rateLimitReset && Date.now() < this.rateLimitReset) {
        return false
      }
      
      return true
    },
    
    // 带重试机制的API调用
    async fetchWithRetry(url: string, options: RequestInit = {}, maxRetries: number = 3): Promise<any> {
      let lastError: unknown
      
      for (let i = 0; i <= maxRetries; i++) {
        try {
          const response = await fetch(url, {
            ...options,
            headers: {
              ...options.headers,
              'X-Retry-Attempt': i.toString()
            }
          })
          
          // 检查速率限制
          if (response.status === 429) {
            const resetTime = response.headers.get('X-RateLimit-Reset')
            if (resetTime) {
              this.rateLimitReset = parseInt(resetTime) * 1000
            }
            throw new Error('Rate limit exceeded')
          }
          
          if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`)
          }
          
          return await response.json()
        } catch (error) {
          lastError = error
          
          if (i === maxRetries) {
            // 最后一次尝试也失败了
            break
          }
          
          // 指数退避
          const delay = Math.pow(2, i) * 1000 + Math.random() * 1000
          await new Promise(resolve => setTimeout(resolve, delay))
        }
      }
      
      throw lastError
    },
    
    // 安全的数据获取
    async safeFetchData() {
      this.loading = true
      this.resetError()
      
      try {
        const data = await this.fetchWithRetry('/api/data', {}, this.maxRetries)
        this.data = data
      } catch (error) {
        this.setError(error, 'fetchData')
        
        // 根据错误类型决定是否重试
        if (this.shouldRetry()) {
          console.log(`Retrying... Attempt ${this.retryCount}/${this.maxRetries}`)
          // 可以在这里实现自动重试逻辑
        }
      } finally {
        this.loading = false
      }
    },
    
    // 带验证的Action
    async validateAndSave(userData: Partial<User>) {
      this.loading = true
      this.resetError()
      
      try {
        // 客户端验证
        const validationErrors = this.validateUserData(userData)
        if (validationErrors.length > 0) {
          throw new ValidationError('Validation failed', validationErrors)
        }
        
        // 服务端保存
        const response = await fetch('/api/users', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(userData)
        })
        
        if (!response.ok) {
          // 尝试解析服务端验证错误
          const errorData = await response.json().catch(() => ({}))
          throw new ApiError(
            errorData.message || `HTTP ${response.status}`,
            response.status,
            errorData.fieldErrors
          )
        }
        
        const savedUser = await response.json()
        // 更新本地状态
        if (!this.data) this.data = []
        this.data.push(savedUser)
      } catch (error) {
        this.setError(error, 'validateAndSave')
        
        if (error instanceof ValidationError) {
          console.log('Validation errors:', error.fieldErrors)
        } else if (error instanceof ApiError) {
          console.log('API error:', error.status, error.fieldErrors)
        }
        
        throw error
      } finally {
        this.loading = false
      }
    },
    
    // 数据验证
    validateUserData(data: Partial<User>): string[] {
      const errors: string[] = []
      
      if (data.name && data.name.length < 2) {
        errors.push('Name must be at least 2 characters')
      }
      
      if (data.email && !this.isValidEmail(data.email)) {
        errors.push('Invalid email format')
      }
      
      if (data.name && data.name.length > 50) {
        errors.push('Name must be less than 50 characters')
      }
      
      return errors
    },
    
    isValidEmail(email: string): boolean {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
      return emailRegex.test(email)
    }
  }
})

interface ApiError {
  message: string
  status: string
  context: string
  timestamp: number
  details?: any
}

class ValidationError extends Error {
  fieldErrors: string[]
  
  constructor(message: string, fieldErrors: string[]) {
    super(message)
    this.name = 'ValidationError'
    this.fieldErrors = fieldErrors
  }
}

class ApiErrorClass extends Error {
  status: number
  fieldErrors?: any
  
  constructor(message: string, status: number, fieldErrors?: any) {
    super(message)
    this.name = 'ApiError'
    this.status = status
    this.fieldErrors = fieldErrors
  }
}

interface Data {
  id: string
  name: string
}

Actions性能优化

防抖和节流

typescript
export const useOptimizedActionsStore = defineStore('optimizedActions', {
  state: () => ({
    searchTerm: '',
    searchResults: [] as SearchResult[],
    debouncedSearchTerm: '',
    lastSearchTime: 0,
    isSearching: false,
    searchError: null as string | null,
    autoCompleteSuggestions: [] as string[],
    lastAutoCompleteTime: 0
  }),
  
  actions: {
    // 防抖搜索
    debouncedSearch: debounce(function(this: any, term: string) {
      this.debouncedSearchTerm = term
      if (term.trim()) {
        this.performSearch(term)
      } else {
        this.searchResults = []
      }
    }, 300),
    
    // 节流自动补全
    throttledAutoComplete: throttle(function(this: any, input: string) {
      if (input.length >= 2) {
        this.fetchAutoComplete(input)
      }
    }, 500),
    
    // 执行搜索
    async performSearch(term: string) {
      if (this.isSearching) return
      
      this.isSearching = true
      this.searchError = null
      
      try {
        // 防止过于频繁的搜索请求
        const now = Date.now()
        if (now - this.lastSearchTime < 500) {
          await new Promise(resolve => setTimeout(resolve, 500 - (now - this.lastSearchTime)))
        }
        
        const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`)
        if (!response.ok) throw new Error('Search failed')
        
        const results = await response.json()
        this.searchResults = results
        this.lastSearchTime = Date.now()
      } catch (error) {
        this.searchError = (error as Error).message
      } finally {
        this.isSearching = false
      }
    },
    
    // 获取自动补全建议
    async fetchAutoComplete(input: string) {
      const now = Date.now()
      if (now - this.lastAutoCompleteTime < 300) return
      
      try {
        const response = await fetch(`/api/autocomplete?q=${encodeURIComponent(input)}`)
        if (!response.ok) throw new Error('Autocomplete failed')
        
        const suggestions = await response.json()
        this.autoCompleteSuggestions = suggestions
        this.lastAutoCompleteTime = Date.now()
      } catch (error) {
        console.error('Autocomplete error:', error)
      }
    },
    
    // 批量处理(合并多次更新)
    async batchProcess(items: ProcessItem[], processor: (item: ProcessItem) => Promise<any>) {
      this.isSearching = true // 重用状态表示处理中
      
      try {
        // 将多个小请求合并为一个大请求
        const chunks = this.createChunks(items, 10) // 每批最多10个项目
        
        for (const chunk of chunks) {
          await Promise.all(chunk.map(item => processor(item)))
          // 在批次间添加小延迟,避免过于频繁的请求
          await new Promise(resolve => setTimeout(resolve, 100))
        }
      } finally {
        this.isSearching = false
      }
    },
    
    // 创建数据块
    createChunks<T>(array: T[], chunkSize: number): T[][] {
      const chunks: T[][] = []
      for (let i = 0; i < array.length; i += chunkSize) {
        chunks.push(array.slice(i, i + chunkSize))
      }
      return chunks
    },
    
    // 更新搜索词(触发防抖搜索)
    updateSearchTerm(term: string) {
      this.searchTerm = term
      this.debouncedSearch(term)
    },
    
    // 更新自动补全(触发节流)
    updateAutoComplete(input: string) {
      this.throttledAutoComplete(input)
    }
  }
})

// 辅助函数:防抖
function debounce<T extends (...args: any[]) => any>(func: T, wait: number) {
  let timeout: NodeJS.Timeout | null = null
  return function(this: any, ...args: Parameters<T>) {
    const later = () => {
      timeout = null
      func.apply(this, args)
    }
    if (timeout) clearTimeout(timeout)
    timeout = setTimeout(later, wait)
  } as T
}

// 辅助函数:节流
function throttle<T extends (...args: any[]) => any>(func: T, limit: number) {
  let inThrottle: boolean
  return function(this: any, ...args: Parameters<T>) {
    if (!inThrottle) {
      func.apply(this, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  } as T
}

interface SearchResult {
  id: string
  title: string
  description: string
}

interface ProcessItem {
  id: string
  data: any
}

Actions测试

Actions单元测试

typescript
// __tests__/actions.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
import { vi, describe, it, beforeEach, expect, afterEach } from 'vitest'

describe('User Store Actions', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  afterEach(() => {
    // 清理mock
    vi.restoreAllMocks()
  })

  it('should fetch user successfully', async () => {
    const userStore = useUserStore()
    
    // Mock fetch API
    const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' }
    global.fetch = vi.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve(mockUser)
      } as Response)
    ) as any

    await userStore.fetchUser('1')

    expect(userStore.user).toEqual(mockUser)
    expect(userStore.loading).toBe(false)
    expect(userStore.error).toBeNull()
    expect(global.fetch).toHaveBeenCalledWith('/api/users/1')
  })

  it('should handle fetch user error', async () => {
    const userStore = useUserStore()
    
    // Mock fetch API to return error
    global.fetch = vi.fn(() =>
      Promise.resolve({
        ok: false,
        status: 404,
        statusText: 'Not Found'
      } as Response)
    ) as any

    await expect(userStore.fetchUser('1')).rejects.toThrow()
    expect(userStore.error).toContain('404')
    expect(userStore.loading).toBe(false)
  })

  it('should update user successfully', async () => {
    const userStore = useUserStore()
    
    // 首先设置一个用户
    userStore.user = { id: '1', name: 'John', email: 'john@example.com' }
    
    const updatedData = { name: 'John Smith' }
    const mockUpdatedUser = { id: '1', name: 'John Smith', email: 'john@example.com' }
    
    global.fetch = vi.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve(mockUpdatedUser)
      } as Response)
    ) as any

    await userStore.updateUser(updatedData)

    expect(userStore.user).toEqual(mockUpdatedUser)
    expect(global.fetch).toHaveBeenCalledWith(
      '/api/users/1',
      expect.objectContaining({
        method: 'PUT',
        body: JSON.stringify(updatedData)
      })
    )
  })

  it('should handle concurrent actions properly', async () => {
    const userStore = useUserStore()
    
    // Mock slow API calls
    global.fetch = vi.fn(() => 
      new Promise(resolve => 
        setTimeout(() => resolve({
          ok: true,
          json: () => Promise.resolve({ id: '1', name: 'Test User' })
        } as Response), 100)
      )
    ) as any

    // 并发执行多个操作
    const promises = [
      userStore.fetchUser('1'),
      userStore.fetchUser('2'),
      userStore.fetchUser('3')
    ]

    await Promise.all(promises)

    expect(global.fetch).toHaveBeenCalledTimes(3)
  })
})

Actions最佳实践

1. Action命名约定

typescript
export const useNamingConventionsStore = defineStore('namingConventions', {
  state: () => ({
    items: [] as Item[],
    loading: false
  }),
  
  actions: {
    // ✅ 好的命名:使用动词,描述清楚
    fetchItems: async function() { /* ... */ },
    createItem: async function(data: Partial<Item>) { /* ... */ },
    updateItem: async function(id: string, data: Partial<Item>) { /* ... */ },
    deleteItem: async function(id: string) { /* ... */ },
    bulkUpdateItems: async function(updates: Array<{id: string, data: Partial<Item>}>) { /* ... */ },
    
    // 同步操作
    addItem: function(item: Item) { /* ... */ },
    removeItem: function(id: string) { /* ... */ },
    setSelectedItems: function(ids: string[]) { /* ... */ },
    
    // ❌ 避免:不清晰的命名
    // doSomething: async function() { /* ... */ },  // 不清楚做什么
    // proc: async function() { /* ... */ },         // 缩写不清晰
  }
})

2. Action组合模式

typescript
export const useActionCompositionStore = defineStore('actionComposition', {
  state: () => ({
    users: [] as User[],
    posts: [] as Post[],
    comments: [] as Comment[]
  }),
  
  actions: {
    // 原子操作
    async _fetchUsers(): Promise<User[]> {
      const response = await fetch('/api/users')
      const users = await response.json()
      this.users = users
      return users
    },
    
    async _fetchPosts(): Promise<Post[]> {
      const response = await fetch('/api/posts')
      const posts = await response.json()
      this.posts = posts
      return posts
    },
    
    // 组合操作
    async fetchDashboardData() {
      // 并行获取数据
      const [users, posts] = await Promise.all([
        this._fetchUsers(),
        this._fetchPosts()
      ])
      
      // 获取相关评论
      const postIds = posts.slice(0, 10).map(p => p.id)
      await this._fetchCommentsForPosts(postIds)
      
      return { users, posts, comments: this.comments }
    },
    
    // 私有辅助方法
    async _fetchCommentsForPosts(postIds: string[]) {
      if (postIds.length === 0) return
      
      const response = await fetch(`/api/comments?postIds=${postIds.join(',')}`)
      const comments = await response.json()
      this.comments = comments
    },
    
    // 事务性操作
    async createUserWithInitialPost(userData: Partial<User>, postTitle: string) {
      try {
        // 创建用户
        const user = await this._createUser(userData)
        
        // 创建初始帖子
        await this._createPost({
          title: postTitle,
          userId: user.id,
          content: 'Initial post'
        })
        
        return user
      } catch (error) {
        // 回滚操作
        if (this._currentUser) {
          await this._cleanupUserData(this._currentUser.id)
        }
        throw error
      }
    },
    
    async _createUser(userData: Partial<User>): Promise<User> {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData)
      })
      const user = await response.json()
      this.users.push(user)
      return user
    },
    
    async _createPost(postData: Partial<Post>): Promise<Post> {
      const response = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(postData)
      })
      const post = await response.json()
      this.posts.push(post)
      return post
    },
    
    async _cleanupUserData(userId: string) {
      // 清理用户相关的数据
      this.posts = this.posts.filter(post => post.userId !== userId)
      this.comments = this.comments.filter(comment => comment.userId !== userId)
      this.users = this.users.filter(user => user.id !== userId)
    }
  }
})

3. Action错误恢复

typescript
export const useResilientActionsStore = defineStore('resilientActions', {
  state: () => ({
    data: null as Data[] | null,
    lastSuccessfulFetch: null as number | null,
    retryAttempts: 0,
    maxRetryAttempts: 3,
    fallbackData: [] as Data[]
  }),
  
  actions: {
    async resilientFetchData(): Promise<Data[]> {
      try {
        const result = await this.fetchDataWithRetry()
        this.lastSuccessfulFetch = Date.now()
        this.retryAttempts = 0
        this.data = result
        return result
      } catch (error) {
        console.error('All retry attempts failed:', error)
        
        // 使用备用数据
        if (this.fallbackData.length > 0) {
          this.data = this.fallbackData
        }
        
        throw error
      }
    },
    
    async fetchDataWithRetry(maxRetries: number = 3): Promise<Data[]> {
      let lastError: Error | null = null
      
      for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
          const response = await fetch('/api/data')
          if (!response.ok) {
            throw new Error(`HTTP ${response.status}`)
          }
          return await response.json()
        } catch (error) {
          lastError = error as Error
          this.retryAttempts = attempt + 1
          
          if (attempt < maxRetries) {
            // 指数退避
            const delay = Math.pow(2, attempt) * 1000
            await new Promise(resolve => setTimeout(resolve, delay))
          }
        }
      }
      
      throw lastError
    },
    
    // 使用缓存数据
    async fetchWithCache(): Promise<Data[]> {
      // 检查缓存
      if (this.data && this.isCacheValid()) {
        return this.data
      }
      
      try {
        // 尝试获取最新数据
        const freshData = await this.resilientFetchData()
        return freshData
      } catch (error) {
        // 如果获取最新数据失败,尝试返回缓存数据
        if (this.data) {
          console.warn('Using cached data due to fetch error')
          return this.data
        }
        throw error
      }
    },
    
    isCacheValid(): boolean {
      if (!this.lastSuccessfulFetch) return false
      const cacheDuration = 5 * 60 * 1000 // 5分钟
      return Date.now() - this.lastSuccessfulFetch < cacheDuration
    }
  }
})

interface Data {
  id: string
  value: any
}

总结

Actions是Pinia中处理业务逻辑的核心部分,通过本章的学习,你应该掌握:

  1. 基础Actions:定义和使用同步和异步Actions
  2. 异步操作:处理API调用、错误处理和加载状态
  3. 状态管理:在Actions中管理复杂的状态转换
  4. Actions通信:Actions间的调用和协调
  5. 错误处理:全面的错误处理策略
  6. 性能优化:防抖、节流和批量处理
  7. 测试策略:如何测试Actions
  8. 最佳实践:命名、组合和恢复策略

正确的Actions设计是构建健壮、可维护应用的关键,确保遵循这些原则来处理你的业务逻辑。