first commit
This commit is contained in:
768
docs/api/examples/frontend-integration.md
Normal file
768
docs/api/examples/frontend-integration.md
Normal file
@@ -0,0 +1,768 @@
|
||||
# 前端集成示例
|
||||
|
||||
本文档提供完整的前端集成代码示例,包括 JavaScript/TypeScript、React 和 Vue。
|
||||
|
||||
## 目录
|
||||
|
||||
- [TypeScript 基础示例](#typescript-基础示例)
|
||||
- [React 集成示例](#react-集成示例)
|
||||
- [Vue 集成示例](#vue-集成示例)
|
||||
- [Token 存储建议](#token-存储建议)
|
||||
- [错误处理](#错误处理)
|
||||
|
||||
---
|
||||
|
||||
## TypeScript 基础示例
|
||||
|
||||
### 认证客户端类
|
||||
|
||||
以下是一个完整的 TypeScript 认证客户端实现,包含注册、登录、Token 刷新等功能:
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
interface RegisterData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface LoginData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface RegisterResponse {
|
||||
email: string;
|
||||
created_at: string;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
interface LoginResponse {
|
||||
id: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
interface RefreshResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
class AuthClient {
|
||||
private baseURL: string;
|
||||
private accessToken: string | null = null;
|
||||
private refreshToken: string | null = null;
|
||||
|
||||
constructor(baseURL: string = 'http://localhost:3000') {
|
||||
this.baseURL = baseURL;
|
||||
// 从 localStorage 加载 Token
|
||||
this.accessToken = localStorage.getItem('access_token');
|
||||
this.refreshToken = localStorage.getItem('refresh_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
async register(email: string, password: string): Promise<RegisterResponse> {
|
||||
const response = await fetch(`${this.baseURL}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const result: ApiResponse<RegisterResponse> = await response.json();
|
||||
|
||||
if (result.code === 200) {
|
||||
this.saveTokens(result.data.access_token, result.data.refresh_token);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
throw new Error(result.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
async login(email: string, password: string): Promise<LoginResponse> {
|
||||
const response = await fetch(`${this.baseURL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const result: ApiResponse<LoginResponse> = await response.json();
|
||||
|
||||
if (result.code === 200) {
|
||||
this.saveTokens(result.data.access_token, result.data.refresh_token);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
throw new Error(result.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
*/
|
||||
async refreshTokens(): Promise<void> {
|
||||
if (!this.refreshToken) {
|
||||
throw new Error('No refresh token available');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseURL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh_token: this.refreshToken }),
|
||||
});
|
||||
|
||||
const result: ApiResponse<RefreshResponse> = await response.json();
|
||||
|
||||
if (result.code === 200) {
|
||||
this.saveTokens(result.data.access_token, result.data.refresh_token);
|
||||
} else {
|
||||
this.clearTokens();
|
||||
throw new Error(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起需要认证的请求
|
||||
*/
|
||||
async authenticatedFetch(url: string, options?: RequestInit): Promise<Response> {
|
||||
if (!this.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
let response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Token 过期,尝试刷新
|
||||
if (response.status === 401) {
|
||||
try {
|
||||
await this.refreshTokens();
|
||||
// 重试原请求
|
||||
response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// 刷新失败,清除 Token 并抛出错误
|
||||
this.clearTokens();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Token 到 localStorage
|
||||
*/
|
||||
private saveTokens(accessToken: string, refreshToken: string): void {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
localStorage.setItem('access_token', accessToken);
|
||||
localStorage.setItem('refresh_token', refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除 Token
|
||||
*/
|
||||
private clearTokens(): void {
|
||||
this.accessToken = null;
|
||||
this.refreshToken = null;
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
logout(): void {
|
||||
this.clearTokens();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return this.accessToken !== null;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const authClient = new AuthClient();
|
||||
|
||||
// 注册
|
||||
try {
|
||||
const result = await authClient.register('user@example.com', 'password123');
|
||||
console.log('注册成功:', result);
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error);
|
||||
}
|
||||
|
||||
// 登录
|
||||
try {
|
||||
const result = await authClient.login('user@example.com', 'password123');
|
||||
console.log('登录成功:', result);
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
}
|
||||
|
||||
// 访问受保护接口
|
||||
try {
|
||||
const response = await authClient.authenticatedFetch(
|
||||
'http://localhost:3000/auth/delete',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: '1234567890', password: 'password123' }),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
console.log('请求成功:', data);
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
}
|
||||
|
||||
// 登出
|
||||
authClient.logout();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React 集成示例
|
||||
|
||||
### AuthContext Provider
|
||||
|
||||
```typescript
|
||||
// AuthContext.tsx
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
accessToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
register: (email: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// 从 localStorage 加载 Token
|
||||
const storedAccessToken = localStorage.getItem('access_token');
|
||||
const storedUser = localStorage.getItem('user');
|
||||
|
||||
if (storedAccessToken && storedUser) {
|
||||
setAccessToken(storedAccessToken);
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
const response = await fetch('http://localhost:3000/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 200) {
|
||||
const userData: User = {
|
||||
id: result.data.id,
|
||||
email: result.data.email,
|
||||
created_at: result.data.created_at,
|
||||
};
|
||||
|
||||
setUser(userData);
|
||||
setAccessToken(result.data.access_token);
|
||||
|
||||
localStorage.setItem('access_token', result.data.access_token);
|
||||
localStorage.setItem('refresh_token', result.data.refresh_token);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (email: string, password: string) => {
|
||||
const response = await fetch('http://localhost:3000/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 200) {
|
||||
const userData: User = {
|
||||
id: result.data.id || '',
|
||||
email: result.data.email,
|
||||
created_at: result.data.created_at,
|
||||
};
|
||||
|
||||
setUser(userData);
|
||||
setAccessToken(result.data.access_token);
|
||||
|
||||
localStorage.setItem('access_token', result.data.access_token);
|
||||
localStorage.setItem('refresh_token', result.data.refresh_token);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
setAccessToken(null);
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
localStorage.removeItem('user');
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
accessToken,
|
||||
isAuthenticated: !!accessToken,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
loading,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
```
|
||||
|
||||
### API Hook(带 Token 刷新)
|
||||
|
||||
```typescript
|
||||
// useApi.ts
|
||||
import { useCallback } from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
export function useApi() {
|
||||
const { accessToken, setAccessToken, logout } = useAuth();
|
||||
|
||||
const fetchWithAuth = useCallback(
|
||||
async (url: string, options?: RequestInit): Promise<Response> => {
|
||||
if (!accessToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
let response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Token 过期,尝试刷新
|
||||
if (response.status === 401) {
|
||||
const refreshToken = localStorage.getItem('refresh_token');
|
||||
|
||||
if (refreshToken) {
|
||||
const refreshResponse = await fetch('http://localhost:3000/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
|
||||
const refreshResult = await refreshResponse.json();
|
||||
|
||||
if (refreshResult.code === 200) {
|
||||
setAccessToken(refreshResult.data.access_token);
|
||||
localStorage.setItem('access_token', refreshResult.data.access_token);
|
||||
localStorage.setItem('refresh_token', refreshResult.data.refresh_token);
|
||||
|
||||
// 重试原请求
|
||||
response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
'Authorization': `Bearer ${refreshResult.data.access_token}`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 刷新失败,登出
|
||||
logout();
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
} else {
|
||||
logout();
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
[accessToken, setAccessToken, logout]
|
||||
);
|
||||
|
||||
return { fetchWithAuth };
|
||||
}
|
||||
```
|
||||
|
||||
### 登录组件示例
|
||||
|
||||
```typescript
|
||||
// Login.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
export function Login() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await login(email, password);
|
||||
// 登录成功,路由跳转
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '登录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<h2>登录</h2>
|
||||
{error && <div style={{ color: 'red' }}>{error}</div>}
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="邮箱"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="密码"
|
||||
required
|
||||
/>
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vue 集成示例
|
||||
|
||||
### Auth Composable
|
||||
|
||||
```typescript
|
||||
// composables/useAuth.ts
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const user = ref<User | null>(null);
|
||||
const accessToken = ref<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const isAuthenticated = computed(() => !!accessToken.value);
|
||||
|
||||
// 初始化:从 localStorage 加载
|
||||
const init = () => {
|
||||
const storedAccessToken = localStorage.getItem('access_token');
|
||||
const storedUser = localStorage.getItem('user');
|
||||
|
||||
if (storedAccessToken && storedUser) {
|
||||
accessToken.value = storedAccessToken;
|
||||
user.value = JSON.parse(storedUser);
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
const response = await fetch('http://localhost:3000/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 200) {
|
||||
const userData: User = {
|
||||
id: result.data.id,
|
||||
email: result.data.email,
|
||||
created_at: result.data.created_at,
|
||||
};
|
||||
|
||||
user.value = userData;
|
||||
accessToken.value = result.data.access_token;
|
||||
|
||||
localStorage.setItem('access_token', result.data.access_token);
|
||||
localStorage.setItem('refresh_token', result.data.refresh_token);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (email: string, password: string) => {
|
||||
const response = await fetch('http://localhost:3000/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 200) {
|
||||
const userData: User = {
|
||||
id: result.data.id || '',
|
||||
email: result.data.email,
|
||||
created_at: result.data.created_at,
|
||||
};
|
||||
|
||||
user.value = userData;
|
||||
accessToken.value = result.data.access_token;
|
||||
|
||||
localStorage.setItem('access_token', result.data.access_token);
|
||||
localStorage.setItem('refresh_token', result.data.refresh_token);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
user.value = null;
|
||||
accessToken.value = null;
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
localStorage.removeItem('user');
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
return {
|
||||
user,
|
||||
accessToken,
|
||||
isAuthenticated,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
init,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Axios 拦截器示例
|
||||
|
||||
```typescript
|
||||
// api/axios.ts
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:3000',
|
||||
});
|
||||
|
||||
// 请求拦截器:添加 Authorization header
|
||||
api.interceptors.request.use((config) => {
|
||||
const accessToken = localStorage.getItem('access_token');
|
||||
if (accessToken) {
|
||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// 响应拦截器:处理 401 错误并刷新 Token
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
const refreshToken = localStorage.getItem('refresh_token');
|
||||
|
||||
if (refreshToken) {
|
||||
try {
|
||||
const response = await axios.post('/auth/refresh', {
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
const { access_token, refresh_token } = response.data.data;
|
||||
|
||||
localStorage.setItem('access_token', access_token);
|
||||
localStorage.setItem('refresh_token', refresh_token);
|
||||
|
||||
// 重试原请求
|
||||
originalRequest.headers.Authorization = `Bearer ${access_token}`;
|
||||
return axios(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
// 刷新失败,清除 Token
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
} else {
|
||||
// 没有 Refresh Token,跳转到登录页
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Token 存储建议
|
||||
|
||||
### localStorage vs sessionStorage vs Cookie
|
||||
|
||||
| 存储方式 | 优点 | 缺点 | 推荐场景 |
|
||||
|---------|------|------|----------|
|
||||
| localStorage | 数据持久化,刷新页面不丢失 | 容易受到 XSS 攻击 | Access Token、Refresh Token |
|
||||
| sessionStorage | 关闭标签页自动清除 | 刷新页面会丢失 | 不推荐 |
|
||||
| Cookie | 可设置 HttpOnly 防止 XSS | 容易受到 CSRF 攻击 | 服务器渲染场景 |
|
||||
|
||||
### 推荐方案
|
||||
|
||||
**前端应用(SPA)**:
|
||||
- Access Token:localStorage
|
||||
- Refresh Token:localStorage
|
||||
- 添加适当的 XSS 防护(内容安全策略、输入验证)
|
||||
|
||||
**安全性要求高的场景**:
|
||||
- Access Token:内存(React Context/Vue Reactive)
|
||||
- Refresh Token:HttpOnly Cookie(需要后端支持)
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 通用错误处理
|
||||
|
||||
```typescript
|
||||
async function handleApiCall<T>(
|
||||
apiCall: () => Promise<T>,
|
||||
onError?: (error: Error) => void
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
return await apiCall();
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error as Error);
|
||||
} else {
|
||||
console.error('API 调用失败:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const result = await handleApiCall(
|
||||
() => authClient.login('user@example.com', 'password123'),
|
||||
(error) => {
|
||||
alert(`登录失败: ${error.message}`);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 网络错误重试
|
||||
|
||||
```typescript
|
||||
async function fetchWithRetry(
|
||||
url: string,
|
||||
options?: RequestInit,
|
||||
maxRetries: number = 3
|
||||
): Promise<Response> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await fetch(url, options);
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) {
|
||||
throw error;
|
||||
}
|
||||
// 等待后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
throw new Error('Max retries reached');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [公开接口文档](../endpoints/public.md) - API 接口详细说明
|
||||
- [认证机制详解](../authentication.md) - JWT 认证流程
|
||||
- [受保护接口文档](../endpoints/protected.md) - 需要认证的接口
|
||||
|
||||
---
|
||||
|
||||
**提示**:以上示例代码仅供参考,实际使用时请根据项目需求调整。
|
||||
Reference in New Issue
Block a user