API 集成示例
这个示例演示了如何有效地将 Pinia 与 REST API 集成。它包括数据获取、缓存策略、错误处理、乐观更新和分页。
概述
API 集成示例展示了:
- RESTful API 集成
- 数据获取和缓存
- 错误处理和重试逻辑
- 乐观更新
- 分页和无限滚动
- 请求去重
- 后台数据同步
- 离线支持
- 请求/响应拦截器
功能特性
- ✅ GET、POST、PUT、DELETE 操作
- ✅ 请求缓存和失效
- ✅ 错误处理和重试
- ✅ 加载状态管理
- ✅ 乐观更新
- ✅ 分页支持
- ✅ 请求去重
- ✅ 后台刷新
- ✅ 离线队列
- ✅ 请求取消
- ✅ 响应转换
类型定义
ts
// types/api.ts
export interface ApiResponse<T = any> {
data: T
message?: string
success: boolean
timestamp: string
}
export interface PaginatedResponse<T = any> {
data: T[]
pagination: {
page: number
limit: number
total: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
success: boolean
}
export interface ApiError {
message: string
code: string
status: number
details?: Record<string, any>
timestamp: string
}
export interface RequestConfig {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
url: string
data?: any
params?: Record<string, any>
headers?: Record<string, string>
timeout?: number
retries?: number
cache?: boolean
cacheTTL?: number
optimistic?: boolean
background?: boolean
}
export interface CacheEntry<T = any> {
data: T
timestamp: number
ttl: number
key: string
}
export interface LoadingState {
[key: string]: boolean
}
export interface ErrorState {
[key: string]: ApiError | null
}
export interface Post {
id: string
title: string
content: string
author: {
id: string
name: string
avatar?: string
}
tags: string[]
publishedAt: string
updatedAt: string
likes: number
comments: number
status: 'draft' | 'published' | 'archived'
}
export interface CreatePostData {
title: string
content: string
tags: string[]
status: 'draft' | 'published'
}
export interface UpdatePostData extends Partial<CreatePostData> {
id: string
}
export interface PostFilters {
author?: string
tags?: string[]
status?: Post['status']
search?: string
dateFrom?: string
dateTo?: string
}
API Store
ts
// stores/api.ts
import { ref, computed, watch } from 'vue'
import { defineStore } from 'pinia'
import type {
ApiResponse,
PaginatedResponse,
ApiError,
RequestConfig,
CacheEntry,
LoadingState,
ErrorState,
Post,
CreatePostData,
UpdatePostData,
PostFilters
} from '../types/api'
export const useApiStore = defineStore('api', () => {
// 状态
const cache = ref<Map<string, CacheEntry>>(new Map())
const loading = ref<LoadingState>({})
const errors = ref<ErrorState>({})
const pendingRequests = ref<Map<string, AbortController>>(new Map())
const offlineQueue = ref<RequestConfig[]>([])
const isOnline = ref(navigator.onLine)
const retryQueue = ref<Array<{ config: RequestConfig; attempt: number }>>([])
// 文章相关状态
const posts = ref<Post[]>([])
const currentPost = ref<Post | null>(null)
const postsFilters = ref<PostFilters>({})
const postsPagination = ref({
page: 1,
limit: 10,
total: 0,
totalPages: 0,
hasNext: false,
hasPrev: false
})
// 计算属性
const isLoading = computed(() => {
return (key: string) => loading.value[key] || false
})
const getError = computed(() => {
return (key: string) => errors.value[key] || null
})
const filteredPosts = computed(() => {
let filtered = posts.value
if (postsFilters.value.author) {
filtered = filtered.filter(post =>
post.author.id === postsFilters.value.author
)
}
if (postsFilters.value.tags?.length) {
filtered = filtered.filter(post =>
postsFilters.value.tags!.some(tag => post.tags.includes(tag))
)
}
if (postsFilters.value.status) {
filtered = filtered.filter(post =>
post.status === postsFilters.value.status
)
}
if (postsFilters.value.search) {
const search = postsFilters.value.search.toLowerCase()
filtered = filtered.filter(post =>
post.title.toLowerCase().includes(search) ||
post.content.toLowerCase().includes(search)
)
}
return filtered
})
// 工具函数
const generateCacheKey = (config: RequestConfig): string => {
const { method, url, params, data } = config
return `${method}:${url}:${JSON.stringify(params || {})}:${JSON.stringify(data || {})}`
}
const isCacheValid = (entry: CacheEntry): boolean => {
return Date.now() - entry.timestamp < entry.ttl
}
const setLoading = (key: string, value: boolean) => {
loading.value[key] = value
}
const setError = (key: string, error: ApiError | null) => {
errors.value[key] = error
}
const clearError = (key: string) => {
delete errors.value[key]
}
// 核心 API 方法
const request = async <T = any>(config: RequestConfig): Promise<T> => {
const cacheKey = generateCacheKey(config)
const loadingKey = `${config.method}:${config.url}`
// 检查 GET 请求的缓存
if (config.method === 'GET' && config.cache !== false) {
const cached = cache.value.get(cacheKey)
if (cached && isCacheValid(cached)) {
return cached.data
}
}
// 检查重复请求
if (pendingRequests.value.has(cacheKey)) {
// 等待现有请求
return new Promise((resolve, reject) => {
const checkRequest = () => {
if (!pendingRequests.value.has(cacheKey)) {
const cached = cache.value.get(cacheKey)
if (cached) {
resolve(cached.data)
} else {
reject(new Error('请求失败'))
}
} else {
setTimeout(checkRequest, 100)
}
}
checkRequest()
})
}
// 处理离线请求
if (!isOnline.value && config.method !== 'GET') {
offlineQueue.value.push(config)
throw new Error('请求已排队等待在线时处理')
}
setLoading(loadingKey, true)
clearError(loadingKey)
// 创建中止控制器
const abortController = new AbortController()
pendingRequests.value.set(cacheKey, abortController)
try {
const url = new URL(config.url, window.location.origin)
// 添加查询参数
if (config.params) {
Object.entries(config.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value))
}
})
}
const requestInit: RequestInit = {
method: config.method,
headers: {
'Content-Type': 'application/json',
...config.headers
},
signal: abortController.signal
}
if (config.data && config.method !== 'GET') {
requestInit.body = JSON.stringify(config.data)
}
const response = await fetch(url.toString(), requestInit)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const apiError: ApiError = {
message: errorData.message || `HTTP ${response.status}`,
code: errorData.code || 'HTTP_ERROR',
status: response.status,
details: errorData.details,
timestamp: new Date().toISOString()
}
throw apiError
}
const data = await response.json()
// 缓存成功的 GET 请求
if (config.method === 'GET' && config.cache !== false) {
const ttl = config.cacheTTL || 5 * 60 * 1000 // 默认 5 分钟
cache.value.set(cacheKey, {
data,
timestamp: Date.now(),
ttl,
key: cacheKey
})
}
return data
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('请求已取消')
}
const apiError = error as ApiError
setError(loadingKey, apiError)
// 重试逻辑
if (config.retries && config.retries > 0) {
const retryConfig = { ...config, retries: config.retries - 1 }
retryQueue.value.push({ config: retryConfig, attempt: 1 })
// 延迟后重试
setTimeout(() => {
processRetryQueue()
}, 1000 * Math.pow(2, 1)) // 指数退避
}
throw apiError
} finally {
setLoading(loadingKey, false)
pendingRequests.value.delete(cacheKey)
}
}
const processRetryQueue = async () => {
const retries = [...retryQueue.value]
retryQueue.value = []
for (const { config, attempt } of retries) {
try {
await request(config)
} catch (error) {
if (config.retries && config.retries > 0) {
retryQueue.value.push({
config: { ...config, retries: config.retries - 1 },
attempt: attempt + 1
})
}
}
}
}
const get = <T = any>(url: string, params?: Record<string, any>, options?: Partial<RequestConfig>): Promise<T> => {
return request<T>({
method: 'GET',
url,
params,
cache: true,
...options
})
}
const post = <T = any>(url: string, data?: any, options?: Partial<RequestConfig>): Promise<T> => {
return request<T>({
method: 'POST',
url,
data,
...options
})
}
const put = <T = any>(url: string, data?: any, options?: Partial<RequestConfig>): Promise<T> => {
return request<T>({
method: 'PUT',
url,
data,
...options
})
}
const del = <T = any>(url: string, options?: Partial<RequestConfig>): Promise<T> => {
return request<T>({
method: 'DELETE',
url,
...options
})
}
// 文章 API 方法
const fetchPosts = async (page = 1, limit = 10, filters?: PostFilters) => {
try {
const params = {
page,
limit,
...filters
}
const response = await get<PaginatedResponse<Post>>('/api/posts', params)
if (page === 1) {
posts.value = response.data
} else {
posts.value.push(...response.data)
}
postsPagination.value = response.pagination
postsFilters.value = filters || {}
return response
} catch (error) {
console.error('获取文章失败:', error)
throw error
}
}
const fetchPost = async (id: string) => {
try {
const response = await get<ApiResponse<Post>>(`/api/posts/${id}`)
currentPost.value = response.data
// 如果文章存在于列表中,则更新它
const index = posts.value.findIndex(post => post.id === id)
if (index !== -1) {
posts.value[index] = response.data
}
return response.data
} catch (error) {
console.error('获取文章失败:', error)
throw error
}
}
const createPost = async (data: CreatePostData) => {
try {
// 乐观更新
const tempPost: Post = {
id: `temp-${Date.now()}`,
...data,
author: { id: 'current-user', name: '您' },
publishedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
likes: 0,
comments: 0
}
posts.value.unshift(tempPost)
const response = await post<ApiResponse<Post>>('/api/posts', data, {
optimistic: true
})
// 用真实文章替换临时文章
const tempIndex = posts.value.findIndex(post => post.id === tempPost.id)
if (tempIndex !== -1) {
posts.value[tempIndex] = response.data
}
// 使缓存失效
invalidateCache('/api/posts')
return response.data
} catch (error) {
// 回滚乐观更新
const tempIndex = posts.value.findIndex(post => post.id.startsWith('temp-'))
if (tempIndex !== -1) {
posts.value.splice(tempIndex, 1)
}
console.error('创建文章失败:', error)
throw error
}
}
const updatePost = async (data: UpdatePostData) => {
try {
// 乐观更新
const index = posts.value.findIndex(post => post.id === data.id)
let originalPost: Post | null = null
if (index !== -1) {
originalPost = { ...posts.value[index] }
posts.value[index] = { ...posts.value[index], ...data, updatedAt: new Date().toISOString() }
}
const response = await put<ApiResponse<Post>>(`/api/posts/${data.id}`, data, {
optimistic: true
})
// 用服务器响应更新
if (index !== -1) {
posts.value[index] = response.data
}
if (currentPost.value?.id === data.id) {
currentPost.value = response.data
}
// 使缓存失效
invalidateCache(`/api/posts/${data.id}`)
invalidateCache('/api/posts')
return response.data
} catch (error) {
// 回滚乐观更新
if (originalPost && index !== -1) {
posts.value[index] = originalPost
}
console.error('更新文章失败:', error)
throw error
}
}
const deletePost = async (id: string) => {
try {
// 乐观更新
const index = posts.value.findIndex(post => post.id === id)
let removedPost: Post | null = null
if (index !== -1) {
removedPost = posts.value[index]
posts.value.splice(index, 1)
}
await del(`/api/posts/${id}`, { optimistic: true })
if (currentPost.value?.id === id) {
currentPost.value = null
}
// 使缓存失效
invalidateCache(`/api/posts/${id}`)
invalidateCache('/api/posts')
} catch (error) {
// 回滚乐观更新
if (removedPost && index !== -1) {
posts.value.splice(index, 0, removedPost)
}
console.error('删除文章失败:', error)
throw error
}
}
const likePost = async (id: string) => {
try {
// 乐观更新
const index = posts.value.findIndex(post => post.id === id)
if (index !== -1) {
posts.value[index].likes++
}
if (currentPost.value?.id === id) {
currentPost.value.likes++
}
await post(`/api/posts/${id}/like`, {}, { optimistic: true })
} catch (error) {
// 回滚乐观更新
if (index !== -1) {
posts.value[index].likes--
}
if (currentPost.value?.id === id) {
currentPost.value.likes--
}
console.error('点赞文章失败:', error)
throw error
}
}
// 缓存管理
const invalidateCache = (pattern?: string) => {
if (!pattern) {
cache.value.clear()
return
}
for (const [key] of cache.value) {
if (key.includes(pattern)) {
cache.value.delete(key)
}
}
}
const clearCache = () => {
cache.value.clear()
}
const cancelRequest = (url: string, method = 'GET') => {
const pattern = `${method}:${url}`
for (const [key, controller] of pendingRequests.value) {
if (key.startsWith(pattern)) {
controller.abort()
pendingRequests.value.delete(key)
}
}
}
const cancelAllRequests = () => {
for (const [key, controller] of pendingRequests.value) {
controller.abort()
}
pendingRequests.value.clear()
}
// 后台同步
const syncOfflineQueue = async () => {
if (!isOnline.value || offlineQueue.value.length === 0) return
const queue = [...offlineQueue.value]
offlineQueue.value = []
for (const config of queue) {
try {
await request(config)
} catch (error) {
console.error('同步离线请求失败:', error)
// 重新排队失败的请求
offlineQueue.value.push(config)
}
}
}
const refreshData = async () => {
// 清除缓存并重新获取当前数据
clearCache()
if (posts.value.length > 0) {
await fetchPosts(1, postsPagination.value.limit, postsFilters.value)
}
if (currentPost.value) {
await fetchPost(currentPost.value.id)
}
}
// 设置在线/离线监听器
const setupNetworkListeners = () => {
const handleOnline = () => {
isOnline.value = true
syncOfflineQueue()
}
const handleOffline = () => {
isOnline.value = false
}
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
// 清理函数
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}
// 定期自动刷新数据
const setupAutoRefresh = (interval = 5 * 60 * 1000) => { // 5 分钟
const timer = setInterval(() => {
if (isOnline.value && document.visibilityState === 'visible') {
refreshData()
}
}, interval)
return () => clearInterval(timer)
}
// 初始化
const cleanup = setupNetworkListeners()
const autoRefreshCleanup = setupAutoRefresh()
return {
// 状态
posts: readonly(posts),
currentPost: readonly(currentPost),
postsFilters: readonly(postsFilters),
postsPagination: readonly(postsPagination),
isOnline: readonly(isOnline),
offlineQueue: readonly(offlineQueue),
// 计算属性
isLoading,
getError,
filteredPosts,
// 核心 API 方法
request,
get,
post,
put,
del,
// 文章方法
fetchPosts,
fetchPost,
createPost,
updatePost,
deletePost,
likePost,
// 缓存管理
invalidateCache,
clearCache,
// 请求管理
cancelRequest,
cancelAllRequests,
// 同步方法
syncOfflineQueue,
refreshData,
// 清理
cleanup: () => {
cleanup()
autoRefreshCleanup()
cancelAllRequests()
}
}
})
组件使用
文章列表组件
vue
<!-- components/PostsList.vue -->
<template>
<div class="posts-list">
<div class="posts-header">
<h2>文章</h2>
<button @click="refreshPosts" :disabled="apiStore.isLoading('GET:/api/posts')">
{{ apiStore.isLoading('GET:/api/posts') ? '刷新中...' : '刷新' }}
</button>
</div>
<div class="posts-filters">
<input
v-model="searchQuery"
type="text"
placeholder="搜索文章..."
@input="debouncedSearch"
/>
<select v-model="statusFilter" @change="applyFilters">
<option value="">所有状态</option>
<option value="published">已发布</option>
<option value="draft">草稿</option>
<option value="archived">已归档</option>
</select>
</div>
<div v-if="apiStore.getError('GET:/api/posts')" class="error">
{{ apiStore.getError('GET:/api/posts')?.message }}
<button @click="retryFetch">重试</button>
</div>
<div v-if="apiStore.isLoading('GET:/api/posts') && !apiStore.posts.length" class="loading">
加载文章中...
</div>
<div class="posts-grid">
<div
v-for="post in apiStore.filteredPosts"
:key="post.id"
class="post-card"
:class="{ 'optimistic': post.id.startsWith('temp-') }"
>
<h3>{{ post.title }}</h3>
<p>{{ post.content.substring(0, 150) }}...</p>
<div class="post-meta">
<span>作者:{{ post.author.name }}</span>
<span>{{ formatDate(post.publishedAt) }}</span>
<span class="status" :class="post.status">{{ getStatusText(post.status) }}</span>
</div>
<div class="post-actions">
<button @click="likePost(post.id)" :disabled="post.id.startsWith('temp-')">
❤️ {{ post.likes }}
</button>
<button @click="editPost(post)" :disabled="post.id.startsWith('temp-')">
编辑
</button>
<button
@click="deletePost(post.id)"
:disabled="post.id.startsWith('temp-')"
class="danger"
>
删除
</button>
</div>
</div>
</div>
<div v-if="apiStore.postsPagination.hasNext" class="pagination">
<button
@click="loadMore"
:disabled="apiStore.isLoading('GET:/api/posts')"
>
{{ apiStore.isLoading('GET:/api/posts') ? '加载中...' : '加载更多' }}
</button>
</div>
<div v-if="!apiStore.isOnline" class="offline-notice">
您当前处于离线状态。连接恢复后将同步更改。
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useApiStore } from '../stores/api'
import type { Post } from '../types/api'
const apiStore = useApiStore()
const searchQuery = ref('')
const statusFilter = ref('')
let searchTimeout: NodeJS.Timeout
const debouncedSearch = () => {
clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => {
applyFilters()
}, 300)
}
const applyFilters = async () => {
const filters = {
search: searchQuery.value || undefined,
status: statusFilter.value || undefined
}
await apiStore.fetchPosts(1, 10, filters)
}
const refreshPosts = async () => {
await apiStore.refreshData()
}
const retryFetch = async () => {
await apiStore.fetchPosts(1, 10, apiStore.postsFilters)
}
const loadMore = async () => {
const nextPage = apiStore.postsPagination.page + 1
await apiStore.fetchPosts(nextPage, 10, apiStore.postsFilters)
}
const likePost = async (id: string) => {
try {
await apiStore.likePost(id)
} catch (error) {
console.error('点赞文章失败:', error)
}
}
const editPost = (post: Post) => {
// 导航到编辑页面或打开模态框
console.log('编辑文章:', post)
}
const deletePost = async (id: string) => {
if (confirm('确定要删除这篇文章吗?')) {
try {
await apiStore.deletePost(id)
} catch (error) {
console.error('删除文章失败:', error)
}
}
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('zh-CN')
}
const getStatusText = (status: string) => {
const statusMap = {
published: '已发布',
draft: '草稿',
archived: '已归档'
}
return statusMap[status] || status
}
onMounted(() => {
apiStore.fetchPosts()
})
onUnmounted(() => {
clearTimeout(searchTimeout)
})
</script>
创建文章组件
vue
<!-- components/CreatePost.vue -->
<template>
<form @submit.prevent="handleSubmit" class="create-post-form">
<div class="form-group">
<label for="title">标题</label>
<input
id="title"
v-model="form.title"
type="text"
required
:disabled="apiStore.isLoading('POST:/api/posts')"
/>
</div>
<div class="form-group">
<label for="content">内容</label>
<textarea
id="content"
v-model="form.content"
rows="10"
required
:disabled="apiStore.isLoading('POST:/api/posts')"
></textarea>
</div>
<div class="form-group">
<label for="tags">标签</label>
<input
id="tags"
v-model="tagsInput"
type="text"
placeholder="输入标签,用逗号分隔"
:disabled="apiStore.isLoading('POST:/api/posts')"
/>
</div>
<div class="form-group">
<label for="status">状态</label>
<select
id="status"
v-model="form.status"
:disabled="apiStore.isLoading('POST:/api/posts')"
>
<option value="draft">草稿</option>
<option value="published">发布</option>
</select>
</div>
<div v-if="apiStore.getError('POST:/api/posts')" class="error">
{{ apiStore.getError('POST:/api/posts')?.message }}
</div>
<div class="form-actions">
<button
type="submit"
:disabled="apiStore.isLoading('POST:/api/posts')"
>
{{ apiStore.isLoading('POST:/api/posts') ? '创建中...' : '创建文章' }}
</button>
<button type="button" @click="resetForm">
重置
</button>
</div>
</form>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useApiStore } from '../stores/api'
import type { CreatePostData } from '../types/api'
const emit = defineEmits<{
created: [post: any]
}>()
const apiStore = useApiStore()
const form = ref<CreatePostData>({
title: '',
content: '',
tags: [],
status: 'draft'
})
const tagsInput = ref('')
const handleSubmit = async () => {
try {
// 解析标签
form.value.tags = tagsInput.value
.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0)
const post = await apiStore.createPost(form.value)
emit('created', post)
resetForm()
} catch (error) {
console.error('创建文章失败:', error)
}
}
const resetForm = () => {
form.value = {
title: '',
content: '',
tags: [],
status: 'draft'
}
tagsInput.value = ''
}
</script>
测试
ts
// tests/api.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useApiStore } from '../stores/api'
// 模拟 fetch
global.fetch = vi.fn()
describe('API Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
it('成功获取文章', async () => {
const apiStore = useApiStore()
const mockResponse = {
data: [
{
id: '1',
title: '测试文章',
content: '测试内容',
author: { id: '1', name: '测试作者' },
tags: ['测试'],
publishedAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
likes: 0,
comments: 0,
status: 'published'
}
],
pagination: {
page: 1,
limit: 10,
total: 1,
totalPages: 1,
hasNext: false,
hasPrev: false
},
success: true
}
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockResponse)
} as Response)
await apiStore.fetchPosts()
expect(apiStore.posts).toHaveLength(1)
expect(apiStore.posts[0].title).toBe('测试文章')
})
it('处理 API 错误', async () => {
const apiStore = useApiStore()
vi.mocked(fetch).mockResolvedValueOnce({
ok: false,
status: 500,
json: () => Promise.resolve({ message: '服务器错误' })
} as Response)
await expect(apiStore.fetchPosts()).rejects.toThrow('服务器错误')
expect(apiStore.getError('GET:/api/posts')?.message).toBe('服务器错误')
})
it('执行乐观更新', async () => {
const apiStore = useApiStore()
// 设置初始文章
apiStore.posts = [{
id: '1',
title: '原始标题',
content: '原始内容',
author: { id: '1', name: '作者' },
tags: [],
publishedAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
likes: 5,
comments: 0,
status: 'published'
}]
// 模拟成功的点赞请求
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true })
} as Response)
await apiStore.likePost('1')
expect(apiStore.posts[0].likes).toBe(6)
})
it('缓存 GET 请求', async () => {
const apiStore = useApiStore()
const mockData = { data: '测试' }
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockData)
} as Response)
// 第一次请求
const result1 = await apiStore.get('/api/test')
expect(result1).toEqual(mockData)
expect(fetch).toHaveBeenCalledTimes(1)
// 第二次请求应该使用缓存
const result2 = await apiStore.get('/api/test')
expect(result2).toEqual(mockData)
expect(fetch).toHaveBeenCalledTimes(1) // 仍然只调用一次
})
})
核心概念
1. 请求缓存
自动缓存 GET 请求,可配置 TTL。
2. 乐观更新
立即更新 UI,失败时回滚。
3. 错误处理
全面的错误处理和重试逻辑。
4. 请求去重
防止对同一资源的重复请求。
5. 离线支持
离线时排队请求,在线时同步。
最佳实践
- 缓存策略 - 缓存 GET 请求,在变更时使缓存失效
- 错误边界 - 优雅地处理错误并提供用户反馈
- 加载状态 - 显示加载指示器以改善用户体验
- 乐观更新 - 立即更新 UI,失败时回滚
- 请求取消 - 在需要时取消待处理的请求
- 重试逻辑 - 为失败的请求实现指数退避
- 离线处理 - 离线时排队变更