React中请求拦截与响应拦截的统一处理方案

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
相关推荐
zyplayer-doc1 小时前
zyplayer-doc企业知识库能做什么:从文档创建、权限管理到AI问答的完整能力
大数据·javascript·数据库·人工智能·pdf·word
IT_陈寒1 小时前
为什么我的Java Stream流操作会吃掉内存?
前端·人工智能·后端
烬羽2 小时前
《受控 vs 非受控:你以为用对了 useState,直到你写了那个表单》
javascript·react.js·前端框架
嘟嘟07172 小时前
React 受控组件与非受控组件:表单数据到底归谁管?
前端·javascript·react.js
嘟嘟07172 小时前
React 表单管理进阶:从单字段到带校验的完整登录表单
前端·javascript·react.js
光影少年3 小时前
react navite原生事件监听、全局事件通知
前端·react native·react.js
lvv3 小时前
不会被渲染的组件,为什么让我的首屏白屏了?(一次前端问题总结)
前端·webpack·性能优化
烬羽3 小时前
《memo 明明加了,为什么子组件还是渲染了?》
react.js·性能优化·全栈
做前端的娜娜子3 小时前
什么是浅拷贝(Shallow Copy)和深拷贝(Deep Copy)?如何手动实现一个深拷贝函数
javascript·面试·掘金·金石计划