Axios 实例封装实践

//src/lib/httpAxios.js
/**
 * Axios 实例封装
 * 1. 创建 Axios 实例
 * 2. 请求拦截器:添加 token,并使用 "Bearer " 前缀
 * 3. 响应拦截器:统一处理 HTTP 错误和业务错误
 * 4. 提取 Axios 错误的友好提示信息
 * 5. 导出 Axios 实例
 */
import axios from "axios";
import Cookies from "universal-cookie";
const cookies = new Cookies();

import { refreshToken } from "@/lib/authService"; // 注意引入路径

/**
 * 创建 axios 实例
 * 可以根据需要配置 baseURL、timeout、headers 等
 */
const httpAxios = axios.create({
  baseURL:
    process.env.NEXT_PUBLIC_API_BASE_URL || "https://www.test.com",
  timeout: 10000,
  // withCredentials: true,
});

/**
 * 提取 Axios 错误的友好提示信息
 * @param {Error} error - Axios 捕获的错误对象
 * @returns {string} - 提取出的错误信息
 */
function extractErrorMessage(error) {
  // 如果有响应数据
  if (error.response) {
    const { status, data } = error.response;
    // 根据后端的返回结构进行解析
    if (data) {
      // 如果 data 是对象 (object),则尝试解析 message 字段
      if (typeof data === "object") {
        //根据实际后端返回的数据结构进行解析
        if (data.data?.message) {
          return data.data.message || JSON.stringify(data.data);
        }
        return data.message || JSON.stringify(data);
      }
      // 如果 data 不是对象,则直接返回
      return data;
    }
    return `HTTP Error: ${status}`;
  } else if (error.request) {
    // 请求发出后未收到响应
    return "No response received. Please check your network.";
  }
  // 其他错误直接返回 error.message
  return error.message;
}

/**
 * 请求拦截器:添加 token,并使用 "Bearer " 前缀
 * 如果需要支持取消请求,可使用 AbortController,参考: https://axios-http.com/docs/cancellation
 * @param {object} config - Axios 请求配置
 * @returns {object} - 处理后的请求配置
 * @throws {Error} - 请求错误
 * @see https://axios-http.com/docs/req_config
 */
httpAxios.interceptors.request.use(
  (config) => {
    const token = cookies.get("jwtToken");
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    // 如果需要支持取消请求,可使用 AbortController,参考:
    // const controller = new AbortController();
    // config.signal = controller.signal;
    return config;
  },
  (error) => {
    console.error("Request error:", error);
    return Promise.reject(error);
  }
);

/**
 * 响应拦截器:统一处理 HTTP 错误和业务错误
 * 在响应拦截器中,对后端返回的业务状态码进行判断,并将错误标准化后再抛出,便于在组件中统一捕获和处理错误
 */
httpAxios.interceptors.response.use(
  (response) => {
    /**
     * 响应拦截器:直接返回响应数据
     * 如果需要对响应数据进行统一处理,可在此处添加代码
     * 例如,对响应数据进行格式化、统一处理错误等
     * 注意:如果需要对错误进行统一处理,应该在响应拦截器中添加处理逻辑
     */
    return response;
  },
  async (error) => {
    let errorMsg = extractErrorMessage(error);
    const originalRequest = error.config;
    if (error.response) {
      const { status } = error.response;
      // 检查是否为 401 错误,并且该请求尚未重试过
      if (status === 401 && !originalRequest._retry) {
        originalRequest._retry = true;
        try {
          // 调用全局的刷新 token 函数
          const newToken = await refreshToken();
          // 更新请求头并重新发起请求
          originalRequest.headers.Authorization = `Bearer ${newToken}`;
          return httpAxios(originalRequest);
        } catch (refreshError) {
          // 刷新 token 失败时,可进行统一处理,如清理 token 并跳转登录页面
          return Promise.reject(refreshError);
        }
      }
    }
    return Promise.reject(new Error(errorMsg));
  }
);

export default httpAxios;

这里我们通过 Axios 的拦截器实现了请求拦截和响应拦截,实现了 token 的自动添加和刷新,以及错误的统一处理。这样我们就可以在组件中直接使用 httpAxios 实例来发起请求,而不用关心 token 的添加和刷新,以及错误的处理。

//src/lib/authService.js

import httpAxios from "@/lib/httpAxios";
import Cookies from "universal-cookie";
const cookies = new Cookies();

/**
 * token 刷新通常属于全局状态管理或认证逻辑,这里抽离成一个独立的函数,而不依赖于 React hook。
 * 在需要刷新 token 的地方,直接调用该函数即可。
 */
export async function refreshToken() {
  try {
    const token = cookies.get("jwtToken");
    // 直接调用 axios 实例的请求方法,不通过 hook
    const response = await httpAxios.request({
      url: "/jwt-login/v1/refresh",
      method: "POST",
      params: {
        // 通过 params 传递 token
        JWT: token,
      },
    });
    // 返回格式为 { "success": true, data: { jwt: "new-token" } }
    const newToken = response.data?.data?.jwt;
    if (newToken) {
      return newToken;
    }
    throw new Error("刷新 token 失败");
  } catch (err) {
    throw err;
  }
}