Skip to main content

请求函数封装

请求函数是前端与后端的桥梁。良好的封装能统一异常处理、降低业务代码量、便于切换底层

一、设计目标

  • ✅ 统一 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 精简:列表接口做字段裁剪,按需请求

八、避坑指南

  1. 不要在业务代码里 try/catch:拦截器统一处理,业务层只写 happy path
  2. 避免在请求拦截里 await 异步操作:会阻塞所有请求,启动时同步拉取 token
  3. 错误信息要友好:网络错误、超时、500 的提示文案要区分
  4. 避免循环依赖:request 工具不要反向 import 业务代码
  5. dev 环境打印请求日志:方便调试;prod 关闭
  6. 大文件上传用专门的 SDK:axios 默认 body 大小有限制

九、面试高频

  • Q:axios 拦截器分几种?执行顺序? A:请求拦截器先入后出,响应拦截器先入先出。简单记:请求洋葱模型从外到内,响应从内到外
  • Q:取消请求有几种方式? A:旧版 CancelToken(已废弃),新版 AbortController(推荐)。
  • Q:HTTP 状态码和业务状态码要分开处理吗? A:要。HTTP 状态码管网络层(401、500),业务码管业务逻辑(0 成功、自定义错误码),两者都要拦截。