4. 请求/响应拦截器:日志打印、错误格式化、自动注入

拦截器(Interceptor)是请求管道中的钩子函数,它在请求发送前和响应返回后插入自定义逻辑。本章聚焦拦截器在真实项目中最常用的三个职责:收敛日志输出、统一格式化后端错误、以及自动注入身份认证 。理解拦截器原理后,你可以将认证、日志、错误处理等横切关注点从业务代码中剥离,使每个 API 调用函数保持干净。

4.1 核心职责与执行顺序

请求拦截器在请求被发送到服务端之前执行;响应拦截器在客户端收到 HTTP 响应之后、业务回调之前执行。Axios、Fetch + 中间件包装、以及浏览器内置的 Service Worker 都能实现类似模式。本章以 Axios 为例,因为它的拦截器 API 最直观。

关键概念:拦截器是一个函数队列。请求拦截器按注册顺序从 “外” 到 “内” 执行(先注册的先处理),响应拦截器按注册顺序从 “内” 到 “外” 执行(后注册的先处理)。如果一个拦截器抛出错误,后续拦截器默认被跳过,直接进入错误处理链。
拦截器类型 执行时机 典型用途
请求拦截器 发送请求之前 添加 Authorization 头、序列化参数、打印请求日志
响应拦截器 收到响应之后 统一解析 data、格式化错误、打印响应日志、处理 401 重定向

4.2 请求拦截器: 自动注入与日志埋点

请求拦截器最通用的场景是从本地存储(如 localStorage 或 cookie)读取 并注入到请求头,同时打印一条包含方法、URL 和参数的调试日志。下面是一个生产可用示例。

import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

// 请求拦截器
apiClient.interceptors.request.use(
  (config) => {
    // 1.  自动注入
    const  = localStorage.getItem('access_');
    if () {
      config.headers.Authorization = `Bearer ${}`;
    }

    // 2. 日志打印(开发环境)
    if (process.env.NODE_ENV === 'development') {
      console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`, {
        params: config.params,
        data: config.data,
      });
    }

    // 3. 可在此处添加请求去重 key 或 Loading 计数
    return config;
  },
  (error) => {
    // 请求配置本身出错(极少发生)
    console.error('[Request Interceptor Error]', error);
    return Promise.reject(error);
  }
);
避坑:不要在请求拦截器内部使用 async/await 调用异步获取 (如刷新 API),因为 Axios 拦截器默认返回同步的 config。异步操作应放在错误处理或专门的请求队列中,避免导致请求永远 pending。

4.3 响应拦截器:错误格式化与业务统一返回

后端返回的数据结构通常包含 HTTP 状态码和业务错误码。响应拦截器将 HTTP 层面错误(4xx/5xx)与业务逻辑错误(如 200 但 code ≠ 0)统一转换为前端可读的格式,避免每个调用处都写 try-catch 判断。

// 响应拦截器
apiClient.interceptors.response.use(
  (response) => {
    // 1. 日志打印
    if (process.env.NODE_ENV === 'development') {
      console.log(`[API Response] ${response.config.url}`, response.data);
    }

    // 2. 业务错误码格式化
    const { code, message, data } = response.data;
    if (code !== 0) {
      // 将业务错误包装成统一 Error 对象
      const bizError = new Error(message || '未知业务错误');
      bizError.code = code;
      bizError.data = data;
      return Promise.reject(bizError);
    }

    // 3. 只返回业务数据层,外部直接使用 data
    return data;
  },
  (httpError) => {
    // HTTP 状态码错误(网络异常、4xx、5xx)
    const status = httpError.response?.status;
    let formattedMessage = '请求失败';

    if (!httpError.response) {
      formattedMessage = '网络连接异常,请检查网络';
    } else if (status === 401) {
      formattedMessage = '登录已过期,请重新登录';
      // 触发全局登出逻辑
    } else if (status === 403) {
      formattedMessage = '权限不足';
    } else if (status === 500) {
      formattedMessage = '服务端内部错误';
    }

    console.error(`[API HTTP Error] ${status}`, formattedMessage);
    return Promise.reject(new Error(formattedMessage));
  }
);

4.4 拦截器执行流程示意图

下图展示了从发起请求到业务拿到成功数据或错误提示的完整拦截器链路。

请求/响应拦截器执行流程 业务发起请求 请求拦截器处理 注入 / 打印日志 / 序列化参数 可抛出错误终止请求 服务器处理请求 响应拦截器处理 检查 HTTP 状态码 / 业务 code 成功 → 返回 data | 失败 → 格式化抛错 打印响应日志 成功 失败 业务拿到成功数据 统一错误提示

4.5 错误格式化与状态码映射

响应拦截器需要处理三类错误:网络异常(无响应)、HTTP 状态码错误(4xx/5xx)、业务错误码(如 200 但 code 非 0)。推荐在拦截器中建立一个标准映射表,避免硬编码字符串散落在各个组件中。

错误类别 判断条件 推荐提示文案
网络异常 error.response === undefined 网络连接异常,请检查网络设置
401 Unauthorized status === 401 登录已过期,请重新登录
403 Forbidden status === 403 您没有权限执行此操作
404 Not Found status === 404 请求的资源不存在
5xx Server Error status >= 500 服务端异常,请稍后重试
业务错误码 response.data.code !== 0 使用服务端返回的 message 字段

4.6 调试与验证

在开发阶段打开浏览器控制台的 Network 面板,配合拦截器中的 console.log 可以快速验证流程:

  1. 观察请求头是否包含 Authorization: Bearer xxx。
  2. 检查请求拦截器日志中的 params 和 data 是否正确序列化。
  3. 模拟 401 响应(如手动清空 ),验证拦截器是否跳转到登出页面。
  4. 模拟业务错误码返回,验证 Promise.reject 是否被业务 catch 到格式化后的错误对象。
// 验证示例:手动测试业务错误码
// 使用模拟响应替换真实 API
const mockResponse = { code: 1001, message: '商品库存不足', data: null };
// 触发拦截器后,外部 catch 应收到 Error('商品库存不足') 且 error.code === 1001

4.7 常见错误与优化

常见错误一: 注入失败 —— 检查 localStorage.getItem 是否在 SSR 或 Service Worker 环境中未定义。解决方案:在拦截器前添加环境判断或使用条件获取。
常见错误二:响应拦截器中重复 try-catch —— 业务层如果已经对 apiClient.get() 使用 try-catch,拦截器内部又用 try-catch 吞掉错误,会导致错误被静默处理。建议拦截器只做格式化并 return Promise.reject,由业务层统一处理。
性能优化:当 过期需要自动刷新时,不要在请求拦截器中直接发起刷新请求,否则会阻塞所有并发请求。正确做法是使用一个 “锁” 变量控制刷新状态,将失败请求加入队列,待刷新成功后再统一重放。该模式将在第 18 章 “ 自动续期与请求重放” 中详细实现。