Appearance
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中处理业务逻辑的核心部分,通过本章的学习,你应该掌握:
- 基础Actions:定义和使用同步和异步Actions
- 异步操作:处理API调用、错误处理和加载状态
- 状态管理:在Actions中管理复杂的状态转换
- Actions通信:Actions间的调用和协调
- 错误处理:全面的错误处理策略
- 性能优化:防抖、节流和批量处理
- 测试策略:如何测试Actions
- 最佳实践:命名、组合和恢复策略
正确的Actions设计是构建健壮、可维护应用的关键,确保遵循这些原则来处理你的业务逻辑。