返回博客列表
Axios前端架构API 设计

Axios 拦截器统一前后端请求契约

通过请求和响应拦截器实现令牌注入、统一错误处理与多端一致的 API 调用体验

作者: ekent·发布于 2026年1月25日

当一个项目有管理后台、移动端 H5、小程序多个前端时,每个端都要处理:请求时注入 Token、响应时统一错误格式、401 时自动跳登录页。如果每个端各写一套,维护成本很高且容易不一致。我们在项目中用 Axios 拦截器封装了一个统一的请求层,所有端共享同一套逻辑。

基础封装

先创建一个配置好的 Axios 实例:

// lib/request.ts
import axios from 'axios';

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 15000,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default request;

直接用 axios.get() 全局调用的问题是:所有请求共享配置,一处修改影响全局。用 axios.create() 创建独立实例,不同的后端服务甚至可以用不同的实例。

请求拦截器:自动注入 Token

request.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('access_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

这样每个 API 调用都不需要手动传 Token:

// 不需要 headers: { Authorization: `Bearer ${token}` }
const { data } = await request.get('/api/users/me');

看起来简单,但有几个细节值得注意:

Token 来源的抽象。上面直接读 localStorage,但在小程序中没有 localStorage,需要用 Taro.getStorageSync。如果把存储方式抽象出来,同一套拦截器可以跨端复用:

// 定义存储接口
interface TokenStorage {
  getToken(): string | null;
  setToken(token: string): void;
  clear(): void;
}

// Web 端实现
const webStorage: TokenStorage = {
  getToken: () => localStorage.getItem('access_token'),
  setToken: (t) => localStorage.setItem('access_token', t),
  clear: () => localStorage.removeItem('access_token'),
};

响应拦截器:统一错误处理

后端返回的错误格式通常是统一的,响应拦截器可以集中处理:

request.interceptors.response.use(
  (response) => {
    // 成功响应,直接返回 data(省去每次 .data 的解构)
    return response.data;
  },
  (error) => {
    const status = error.response?.status;
    const message = error.response?.data?.message || '请求失败';

    switch (status) {
      case 401:
        // Token 过期或无效,清除本地状态并跳转登录
        tokenStorage.clear();
        window.location.href = '/login';
        break;

      case 403:
        console.error('权限不足:', message);
        break;

      case 404:
        console.error('资源不存在:', error.config?.url);
        break;

      case 429:
        console.error('请求过于频繁,请稍后重试');
        break;

      default:
        console.error(`请求错误 [${status}]:`, message);
    }

    return Promise.reject(error);
  }
);

401 处理的注意事项

401 自动跳登录页看似简单,但容易出问题:如果页面上有 5 个并发请求同时返回 401,会触发 5 次跳转。需要加防抖:

let isRedirecting = false;

function handleUnauthorized() {
  if (isRedirecting) return;
  isRedirecting = true;

  tokenStorage.clear();
  window.location.href = '/login';

  // 防止短时间内重复触发
  setTimeout(() => { isRedirecting = false; }, 3000);
}

返回值类型推断

直接用 Axios 的话,返回值类型是 AxiosResponse<T>,需要 .data 才能拿到业务数据。响应拦截器已经返回了 response.data,但 TypeScript 不知道这个变化。

解决方案是重新定义类型:

// 后端统一响应格式
interface ApiResponse<T> {
  code: number;
  data: T;
  message: string;
}

// 封装带类型的请求方法
export async function get<T>(url: string, params?: object): Promise<T> {
  const res = await request.get<any, ApiResponse<T>>(url, { params });
  return res.data;
}

export async function post<T>(url: string, data?: object): Promise<T> {
  const res = await request.post<any, ApiResponse<T>>(url, data);
  return res.data;
}

调用时类型自动推断:

interface User {
  id: number;
  name: string;
}

const user = await get<User>('/api/users/1');
// user 的类型自动推断为 User

请求取消

页面切换时,未完成的请求应该被取消,避免:组件已卸载但回调还在执行,导致更新已销毁的状态。

import { useEffect } from 'react';

function useApiCancel() {
  useEffect(() => {
    const controller = new AbortController();

    request.get('/api/data', { signal: controller.signal });

    return () => controller.abort();
  }, []);
}

总结

Axios 拦截器的价值不在于它能做多复杂的事,而在于它把重复逻辑收拢到一个地方:Token 注入、错误处理、401 跳转、响应格式化。定义好这套契约后,业务代码只需要关心「调哪个接口、传什么参数、拿什么数据」,不用操心基础设施层面的事情。