Files
web-rust-template-project/docs/api/examples/frontend-integration.md
2026-02-13 15:57:29 +08:00

769 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端集成示例
本文档提供完整的前端集成代码示例,包括 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 TokenlocalStorage
- Refresh TokenlocalStorage
- 添加适当的 XSS 防护(内容安全策略、输入验证)
**安全性要求高的场景**
- Access Token内存React Context/Vue Reactive
- Refresh TokenHttpOnly 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) - 需要认证的接口
---
**提示**:以上示例代码仅供参考,实际使用时请根据项目需求调整。