请求函数封装
请求函数是前端与后端的桥梁。良好的封装能统一异常处理、降低业务代码量、便于切换底层。
一、设计目标
- ✅ 统一 baseURL、超时、Header(token、lang、版本)
- ✅ 请求/响应拦截,自动处理登录态、错误码
- ✅ 业务层只关心
请求 + 业务参数,不需要处理 try/catch - ✅ 完整的 TypeScript 类型推导
二、基础封装
// utils/request.ts
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from "axios";
import { message } from "antd"; // 或自己的 UI 库
import { getToken, clearToken } from "./auth";
import router from "@/router";
export interface IResponse<T = unknown> {
code: number;
data: T;
message?: string;
}
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
headers: { "Content-Type": "application/json" },
});
// 请求拦截
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = getToken();
if (token) config.headers.Authorization = `Bearer ${token}`;
// 业务自定义 header
config.headers["X-App-Version"] = APP_VERSION;
return config;
},
(err: AxiosError) => Promise.reject(err)
);
// 响应拦截
service.interceptors.response.use(
(res: AxiosResponse<IResponse>) => {
const { code, data, message: msg } = res.data;
// 业务码 0 表示成功
if (code === 0) return data;
// 业务异常统一弹错
if (code === 401) {
clearToken();
router.push("/login");
} else {
message.error(msg ?? "请求失败");
}
return Promise.reject(res.data);
},
(err: AxiosError<IResponse>) => {
// HTTP 状态码兜底
const status = err.response?.status;
if (status === 401) {
clearToken();
router.push("/login");
} else if (status === 403) {
message.error("无权访问");
} else if (status && status >= 500) {
message.error("服务异常,请稍后再试");
} else {
message.error(err.message);
}
return Promise.reject(err);
}
);
export default service;
三、业务接口层
// api/user.ts
import request from "@/utils/request";
export interface IUser {
id: number;
name: string;
avatar: string;
}
export function getUserList(params: { page: number; size: number }) {
return request<IUser[]>({
url: "/user/list",
method: "GET",
params,
});
}
export function deleteUser(id: number) {
return request({
url: `/user/${id}`,
method: "DELETE",
});
}
推荐配合
useRequest(ahooks)/useSWR/react-query做数据状态管理,业务层几乎不用写 loading/error 逻辑。
四、TypeScript 进阶
1. 泛型推导
// T = 业务数据类型
function request<T = unknown>(config: AxiosRequestConfig): Promise<T> {
return service.request<IResponse<T>>(config).then((res) => res as T);
}
2. 统一处理后端的 snake_case
如果后端用下划线、前端用驼峰,封装一个转换层:
import { snakeCase, camelCase } from "lodash";
function keysToCamel(obj: unknown): unknown {
if (Array.isArray(obj)) return obj.map(keysToCamel);
if (obj && typeof obj === "object") {
return Object.keys(obj).reduce((acc, key) => {
acc[camelCase(key)] = keysToCamel((obj as any)[key]);
return acc;
}, {} as any);
}
return obj;
}
更优雅的方案是后端直接返回驼峰,省去这层转换。
五、取消请求
避免组件卸载后还更新 state:
const controller = new AbortController();
async function fetchData() {
try {
const res = await request({ url: "/data", signal: controller.signal });
setData(res);
} catch (e) {
// abort 错误无需处理
}
}
// 组件卸载
onUnmount(() => controller.abort());
axios v0.22+ 原生支持 AbortController:
const res = await service.get("/data", { signal: controller.signal });
六、并发与去重
1. 并发请求
const [user, order] = await Promise.all([getUser(), getOrder()]);
2. 失败重试
// 配合 axios-retry
import axiosRetry from "axios-retry";
axiosRetry(service, { retries: 3, retryDelay: axiosRetry.exponentialDelay });
注意:幂等接口才重试(GET),写操作(POST/PUT/DELETE)重试可能造成数据问题。
3. 请求去重
防抖场景下,连续触发同一个请求只发一次:
const inflight = new Map<string, Promise<any>>();
function dedupeRequest(key: string, fetcher: () => Promise<any>) {
if (inflight.has(key)) return inflight.get(key);
const p = fetcher().finally(() => inflight.delete(key));
inflight.set(key, p);
return p;
}
七、性能优化
- 接口合并:列表页尽量用
Promise.all一次拉所有依赖数据 - 请求缓存:不变的数据用 SWR 风格
stale-while-revalidate - 超时降级:慢接口走骨架屏,避免一直转圈
- payload 精简:列表接口做字段裁剪,按需请求
八、避坑指南
- 不要在业务代码里 try/catch:拦截器统一处理,业务层只写 happy path
- 避免在请求拦截里 await 异步操作:会阻塞所有请求,启动时同步拉取 token
- 错误信息要友好:网络错误、超时、500 的提示文案要区分
- 避免循环依赖:request 工具不要反向 import 业务代码
- dev 环境打印请求日志:方便调试;prod 关闭
- 大文件上传用专门的 SDK:axios 默认 body 大小有限制
九、面试高频
- Q:axios 拦截器分几种?执行顺序? A:请求拦截器先入后出,响应拦截器先入先出。简单记:请求洋葱模型从外到内,响应从内到外。
- Q:取消请求有几种方式?
A:旧版
CancelToken(已废弃),新版AbortController(推荐)。 - Q:HTTP 状态码和业务状态码要分开处理吗? A:要。HTTP 状态码管网络层(401、500),业务码管业务逻辑(0 成功、自定义错误码),两者都要拦截。