1. 引言
在现代前端开发中,与后端API的交互是React应用的核心功能之一。随着应用复杂度提升,对网络请求进行统一管理、添加全局逻辑(如认证、错误处理、日志记录)变得至关重要。请求拦截(Request Interception)和响应拦截(Response Interception)是实现这一目标的关键技术,它们允许我们在请求发出前和响应返回后注入自定义逻辑,从而实现代码复用、统一错误处理和提升开发效率。
本文将深入探讨在React生态中实现请求与响应拦截的统一处理方案,涵盖从基础概念到高级实践,并提供完整的代码示例。
2. 核心概念
2.1 什么是请求拦截?
请求拦截是指在HTTP请求被发送到服务器之前,对请求配置进行修改或添加额外逻辑的过程。常见的应用场景包括:
- 添加认证令牌:自动在请求头中添加Authorization token
- 统一请求头:设置Content-Type、Accept等公共头部
- 请求参数处理:序列化数据、添加时间戳等
- 请求日志:记录请求信息用于调试和监控
- 请求取消:实现请求超时或取消逻辑
2.2 什么是响应拦截?
响应拦截是指在接收到服务器响应后,在数据传递给业务代码之前,对响应进行处理的过程。常见的应用场景包括:
- 统一错误处理:根据HTTP状态码或业务错误码进行全局错误处理
- 数据格式化:统一解析响应数据格式
- 响应日志:记录响应信息用于调试
- Token刷新:在token过期时自动刷新并重试请求
- 加载状态管理:统一管理请求的加载状态
3. 实现方案对比
3.1 使用Axios拦截器
Axios是目前最流行的HTTP客户端库之一,内置了强大的拦截器机制。
3.1.1 基础拦截器配置
javascript
import axios from 'axios';
// 创建axios实例
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
apiClient.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 添加请求时间戳
config.headers['X-Request-Timestamp'] = Date.now();
// 记录请求日志(开发环境)
if (process.env.NODE_ENV === 'development') {
console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`, config);
}
return config;
},
(error) => {
// 对请求错误做些什么
console.error('Request interceptor error:', error);
return Promise.reject(error);
}
);
// 响应拦截器
apiClient.interceptors.response.use(
(response) => {
// 对响应数据做点什么
if (process.env.NODE_ENV === 'development') {
console.log(`[Response] ${response.status} ${response.config.url}`, response.data);
}
// 统一处理响应格式
return {
success: true,
data: response.data,
status: response.status,
headers: response.headers,
};
},
(error) => {
// 对响应错误做点什么
if (process.env.NODE_ENV === 'development') {
console.error(`[Response Error] ${error.response?.status || 'Network Error'}`, error);
}
// 统一错误处理
if (error.response) {
// 服务器返回了错误状态码
switch (error.response.status) {
case 401:
// 未授权,跳转到登录页
window.location.href = '/login';
break;
case 403:
// 权限不足
console.error('权限不足,请联系管理员');
break;
case 404:
// 资源不存在
console.error('请求的资源不存在');
break;
case 500:
// 服务器内部错误
console.error('服务器内部错误,请稍后重试');
break;
default:
console.error(`请求失败: ${error.response.status}`);
}
} else if (error.request) {
// 请求已发出但没有收到响应
console.error('网络错误,请检查网络连接');
} else {
// 请求配置出错
console.error('请求配置错误:', error.message);
}
return Promise.reject({
success: false,
message: error.message,
status: error.response?.status,
data: error.response?.data,
});
}
);
export default apiClient;
3.1.2 高级特性:Token自动刷新
javascript
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
// 响应拦截器增强:处理token过期
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// 如果是401错误且不是刷新token的请求
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// 如果正在刷新token,将请求加入队列
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
}).catch(err => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
// 刷新token
const refreshToken = localStorage.getItem('refresh_token');
const response = await axios.post('/auth/refresh', { refreshToken });
const { access_token } = response.data;
// 保存新token
localStorage.setItem('access_token', access_token);
// 更新Authorization头
apiClient.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
originalRequest.headers.Authorization = `Bearer ${access_token}`;
// 处理队列中的请求
processQueue(null, access_token);
// 重试原始请求
return apiClient(originalRequest);
} catch (refreshError) {
// 刷新失败,清空token并跳转到登录页
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
processQueue(refreshError, null);
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
3.2 使用Fetch API + 自定义封装
如果不希望引入第三方库,可以使用原生Fetch API进行封装。
javascript
class HttpClient {
constructor(baseURL = '') {
this.baseURL = baseURL;
this.requestInterceptors = [];
this.responseInterceptors = [];
}
// 添加请求拦截器
addRequestInterceptor(interceptor) {
this.requestInterceptors.push(interceptor);
return this;
}
// 添加响应拦截器
addResponseInterceptor(interceptor) {
this.responseInterceptors.push(interceptor);
return this;
}
// 执行请求拦截器链
async applyRequestInterceptors(config) {
let result = config;
for (const interceptor of this.requestInterceptors) {
result = await interceptor(result);
}
return result;
}
// 执行响应拦截器链
async applyResponseInterceptors(response) {
let result = response;
for (const interceptor of this.responseInterceptors) {
result = await interceptor(result);
}
return result;
}
// 核心请求方法
async request(url, options = {}) {
const config = {
url: this.baseURL + url,
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
};
try {
// 应用请求拦截器
const finalConfig = await this.applyRequestInterceptors(config);
// 发送请求
const response = await fetch(finalConfig.url, finalConfig);
// 应用响应拦截器
const finalResponse = await this.applyResponseInterceptors(response);
return finalResponse;
} catch (error) {
// 应用响应拦截器处理错误
for (const interceptor of this.responseInterceptors) {
try {
await interceptor(error, true);
} catch (interceptorError) {
console.error('Response interceptor error:', interceptorError);
}
}
throw error;
}
}
// 便捷方法
get(url, options = {}) {
return this.request(url, { ...options, method: 'GET' });
}
post(url, data, options = {}) {
return this.request(url, {
...options,
method: 'POST',
body: JSON.stringify(data),
});
}
put(url, data, options = {}) {
return this.request(url, {
...options,
method: 'PUT',
body: JSON.stringify(data),
});
}
delete(url, options = {}) {
return this.request(url, { ...options, method: 'DELETE' });
}
}
// 使用示例
const httpClient = new HttpClient('https://api.example.com');
// 添加请求拦截器
httpClient.addRequestInterceptor(async (config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 添加响应拦截器
httpClient.addResponseInterceptor(async (response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
success: true,
data,
status: response.status,
headers: response.headers,
};
});
export default httpClient;
3.3 使用React Query的全局配置
React Query(现为TanStack Query)提供了强大的数据获取和缓存功能,也支持全局配置拦截逻辑。
javascript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import axios from 'axios';
// 配置axios实例
const apiClient = axios.create({
baseURL: 'https://api.example.com',
});
// 请求拦截器
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// 处理未授权
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// 创建QueryClient并配置全局选项
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: async ({ queryKey }) => {
const [url, params] = queryKey;
const { data } = await apiClient.get(url, { params });
return data;
},
retry: (failureCount, error) => {
// 只在非401错误时重试
if (error.response?.status === 401) {
return false;
}
return failureCount < 3;
},
onError: (error) => {
// 全局查询错误处理
console.error('Query error:', error);
},
},
mutations: {
mutationFn: async (variables) => {
const { url, method = 'POST', data } = variables;
const { data: responseData } = await apiClient({
url,
method,
data,
});
return responseData;
},
onError: (error) => {
// 全局变更错误处理
console.error('Mutation error:', error);
},
},
},
});
// 在React组件中使用
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
4. 最佳实践与注意事项
4.1 错误处理策略
统一的错误处理是拦截器的核心价值之一:
javascript
// 错误类型定义
const ErrorType = {
NETWORK_ERROR: 'NETWORK_ERROR',
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
SERVER_ERROR: 'SERVER_ERROR',
CLIENT_ERROR: 'CLIENT_ERROR',
VALIDATION_ERROR: 'VALIDATION_ERROR',
AUTH_ERROR: 'AUTH_ERROR',
};
// 统一错误处理拦截器
const errorHandlerInterceptor = (error) => {
let errorType = ErrorType.NETWORK_ERROR;
let userMessage = '网络错误,请检查网络连接';
if (error.response) {
// 服务器响应了错误状态码
const status = error.response.status;
if (status >= 500) {
errorType = ErrorType.SERVER_ERROR;
userMessage = '服务器内部错误,请稍后重试';
} else if (status >= 400) {
errorType = ErrorType.CLIENT_ERROR;
switch (status) {
case 400:
userMessage = '请求参数错误';
break;
case 401:
errorType = ErrorType.AUTH_ERROR;
userMessage = '登录已过期,请重新登录';
break;
case 403:
userMessage = '权限不足,无法访问该资源';
break;
case 404:
userMessage = '请求的资源不存在';
break;
case 422:
errorType = ErrorType.VALIDATION_ERROR;
userMessage = '数据验证失败';
break;
default:
userMessage = `请求失败: ${status}`;
}
}
} else if (error.code === 'ECONNABORTED') {
errorType = ErrorType.TIMEOUT_ERROR;
userMessage = '请求超时,请稍后重试';
}
// 统一错误格式
const formattedError = {
type: errorType,
message: userMessage,
originalError: error,
timestamp: new Date().toISOString(),
};
// 根据环境处理错误
if (process.env.NODE_ENV === 'development') {
console.error('Request Error:', formattedError);
}
// 可以在这里集成错误上报服务
// reportErrorToService(formattedError);
// 显示用户友好的错误提示
showNotification(userMessage, 'error');
return Promise.reject(formattedError);
};
// 在响应拦截器中注册
apiClient.interceptors.response.use(
(response) => response,
errorHandlerInterceptor
);
4.2 性能监控与日志
javascript
// 性能监控拦截器
const performanceInterceptor = {
request: (config) => {
config.metadata = { startTime: Date.now() };
return config;
},
response: (response) => {
const endTime = Date.now();
const startTime = response.config.metadata?.startTime;
if (startTime) {
const duration = endTime - startTime;
console.log(`请求 ${response.config.url} 耗时: ${duration}ms`);
// 慢请求警告
if (duration > 5000) {
console.warn(`慢请求警告: ${response.config.url} 耗时 ${duration}ms`);
}
// 可以上报到监控系统
// reportPerformanceMetrics({
// url: response.config.url,
// method: response.config.method,
// duration,
// status: response.status,
// });
}
return response;
},
};
// 请求日志拦截器
const loggingInterceptor = {
request: (config) => {
console.group(`📤 请求: ${config.method?.toUpperCase()} ${config.url}`);
console.log('请求配置:', config);
console.groupEnd();
return config;
},
response: (response) => {
console.group(`📥 响应: ${response.status} ${response.config.url}`);
console.log('响应数据:', response.data);
console.log('响应头:', response.headers);
console.groupEnd();
return response;
},
error: (error) => {
console.group(`❌ 错误: ${error.config?.url}`);
console.error('错误详情:', error);
console.groupEnd();
return Promise.reject(error);
},
};
// 注册拦截器
apiClient.interceptors.request.use(
performanceInterceptor.request,
loggingInterceptor.error
);
apiClient.interceptors.request.use(loggingInterceptor.request);
apiClient.interceptors.response.use(
performanceInterceptor.response,
loggingInterceptor.error
);
apiClient.interceptors.response.use(loggingInterceptor.response);
4.3 请求取消与防抖
javascript
// 请求取消管理器
class RequestCanceler {
const