当一个项目有管理后台、移动端 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 跳转、响应格式化。定义好这套契约后,业务代码只需要关心「调哪个接口、传什么参数、拿什么数据」,不用操心基础设施层面的事情。