普通视图

发现新文章,点击刷新页面。
昨天 — 2025年5月17日首页

ts极速封装axios,关注点分离,包会、包爽!

作者 wwwCJ
2025年5月17日 17:22

开篇

axios还用封装?


在这里插入图片描述


不着急,咱先聊聊为什么要封装axios,或者说我们的封装目标是什么。


1、三级拦截器:支持全局、实例、单次请求响应的拦截器设置

为什么要分级设置拦截器?


为了分离关注点,从而实现分层


拿错误处理来说,

当请求过程中发生错误时,错误拦截器执行顺序会是:全局拦截器、实例级拦截器、单次请求级拦截器。

那我们可以用实例级拦截器处理通用错误,用单次请求级拦截器处理特定业务错误。

业务错误在实例级错误拦截器里重新抛出就行了。

这样在错误处理时,实例级拦截器只需要处理通用错误,单次请求级拦截器只需要处理特定业务的错误。

岂不爽哉?


2、支持比axios更严格更清晰的类型要求

axios的get、post等方法泛型参数不传会自动给any。

而我们通过封装可以让 any 爬~

而且封装之后可以通过interface开放类型的拓展,还就那个开闭原则。


3、不锁死axios:方便渣~

哪天不想(能)用axios了,可以无痛切换。


所以说,有这些需求完全可以封装一下axios。


在这里插入图片描述


那怎么个封装法?


1、封装实例和请求配置

这里提一嘴,不建议在响应拦截器里偷懒返回res.data,因为axios内部拦截器是串行调用的,上一个拦截器的结果会作为下一个拦截器的输入,这会导致后续的其它拦截器拿到res.data,而不是res。


import type {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig
} from 'axios';
import axios from 'axios';

//RD: Request Data, SD: Response Data
export interface RequestConfig<RD, SD> extends AxiosRequestConfig<RD> {
reqOKFn?: ((config: RequestConfig<RD, SD>) => Promise<RequestConfig<RD, SD>> | RequestConfig<RD, SD>)[];
//不支持偷懒返回res.data,这是一种反模式, 因为会导致后续的其它拦截器拿到res.data,而不是res
resOKFn?: ((res: AxiosResponse<SD, RD>) => Promise<AxiosResponse<SD, RD>> | AxiosResponse<SD, RD>)[];
reqFailFn?: (error: AxiosError<SD, RD>) => Promise<AxiosResponse<SD, RD>> | AxiosResponse<SD, RD>;
resFailFn?: (error: AxiosError<SD, RD>) => Promise<AxiosResponse<SD, RD>> | AxiosResponse<SD, RD>;
}


class Requester<TRD, TSD> {
instance: AxiosInstance;
constructor(config: RequestConfig<TRD, TSD>) {
this.instance = axios.create(config);
}
        
  async request<RD, SD>(config: RequestConfig<RD, SD>): Promise<AxiosResponse<SD, RD>> {
let res = await this.instance.request<SD, AxiosResponse<SD>, RD>(config);
return res;
}
}

这里要求泛型必须传入参数且以之指定返回值类型,就可以进行更为严格的类型要求。


2、三级拦截器功能

全局拦截器直接在constructor里设置,全局就是对每个实例都设置。


实例拦截器在传入config里设置、在constructor中应用。


值得一提的是,axios中请求拦截器的签名是这样的:

(config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig

而我们想要设置的是这样的:

(config:RequestConfig) => RequestConfig

看一眼axios源码看看InternalAxiosRequestConfig是个啥:

export interface InternalAxiosRequestConfig<D = any> extends AxiosRequestConfig<D> {
  headers: AxiosRequestHeaders;
}

那请求拦截器函数实际上拿到的是加了个 headers 属性的 RequestConfig ,同时也是属性只多不少的 InternalAxiosRequestConfig

所以我们可以通过类型断言来进行适配, 同时保持类型安全。


import type {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig
} from 'axios';
import axios from 'axios';

//RD: Request Data, SD: Response Data
export interface RequestConfig<RD, SD> extends AxiosRequestConfig<RD> {
reqOKFn?: ((config: RequestConfig<RD, SD>) => Promise<RequestConfig<RD, SD>> | RequestConfig<RD, SD>)[];
//不支持偷懒返回res.data,这是一种反模式, 因为会导致后续的其它拦截器拿到res.data,而不是res
resOKFn?: ((res: AxiosResponse<SD, RD>) => Promise<AxiosResponse<SD, RD>> | AxiosResponse<SD, RD>)[];
reqFailFn?: (error: AxiosError<SD, RD>) => Promise<AxiosResponse<SD, RD>> | AxiosResponse<SD, RD>;
resFailFn?: (error: AxiosError<SD, RD>) => Promise<AxiosResponse<SD, RD>> | AxiosResponse<SD, RD>;
}

class Requester<TRD, TSD> {
instance: AxiosInstance;
//config:实例的默认配置
constructor(config: RequestConfig<TRD, TSD>) {
this.instance = axios.create(config);
// 这里 设置并应用 全局拦截器(会给每个实例都设置)
// this.instance.interceptors.request.use(onFulfilledCb,onRejectedCb);

// 这里 应用 实例的拦截器
this.instance.interceptors.request.use(async (config: InternalAxiosRequestConfig<TRD>) => {
/* 
实际上拿到的是加了 headers 属性的 RequestConfig ,
同时也是属性只多不少的 InternalAxiosRequestConfig
类型安全(鸭子类型)
*/
let configCustom = config as unknown as RequestConfig<TRD, TSD>;
for (const fn of configCustom.reqOKFn || []) {
configCustom = await fn(configCustom);
}
return configCustom as InternalAxiosRequestConfig<TRD>;
}, config.reqFailFn);

this.instance.interceptors.response.use(async (value: AxiosResponse<TSD, TRD>) => {
for (const fn of config.resOKFn || []) {
value = await fn(value);
}
return value;
}, config.resFailFn);
}

单次请求的拦截器在request方法里应用就行了。



async request<RD, SD>(config: RequestConfig<RD, SD>):: Promise<AxiosResponse<SD, RD>> {
// 这里 应用 单次请求和响应的拦截器
try {
for (const fn of config.reqOKFn || []) {
config = await fn(config);
}
let res = await this.instance.request<SD, AxiosResponse<SD>, RD>(config);
for (const fn of config.resOKFn || []) {
res = await fn(res);
}
return res;
} catch (error) {
/* 拦截器执行顺序:实例级拦截器、单次请求级拦截器
实例级拦截器处理通用错误,
单次请求级拦截器处理特定业务错误
*/
if (axios.isAxiosError(error)) {
if (error.response) {
//响应错误
if (config.resFailFn) {
                                        // 拦截器返回值会成为请求的最终结果(提供fallback)
return config.resFailFn(error); 
}
} else {
//请求或者请求创建错误
if (config.reqFailFn) {
return config.reqFailFn(error);
}
}
}

throw error;
}
}
}

这里注意要像这样手动设置request方法的返回值类型,不然会推导成Promise<any>


这样,当请求过程中发生错误时,拦截器执行顺序是:全局拦截器、实例级拦截器、单次请求级拦截器。


用实例级拦截器处理通用错误,用单次请求级拦截器处理特定业务错误。

业务错误在实例级错误拦截器里重新抛出就行了。


体会到拦截器分层的好处了吗?


其实不管前端后端,分层的架构思想都无处不在。


比如一个完整的前端应用会分为:

  1. UI组件层: 纯展示组件(按钮、表单等)
  2. 页面/容器层: 组合UI组件,连接数据与展示
  3. 状态管理层: 管理应用状态(Redux/Vuex等)
  4. 服务/API层: 处理与后端的通信(当前代码所处层次)
  5. 工具/辅助层: 提供通用功能

有些复杂的项目甚至还会使用BFF(Backend For Frontend)来加个一层。


在这里插入图片描述


3、结束

最后加上常用的getpost方法

async get<SD>(url: string, config?: RequestConfig<never, SD>) {
return await this.request<never, SD>({
url,
method: 'GET',
...config
});
}

async post<RD, SD>(url: string, data: RD, config?: RequestConfig<RD, SD>) {
return await this.request<RD, SD>({
url,
method: 'POST',
data,
...config
});
}

没了啊,就这。


使用示例:

interface ServerDataFormat<TData = unknown> {
code: string; 

message: string; 

data: TData; 
}

interface LoginResponse {
id: number;
username: string;
create_at: Date | null;
update_at: Date | null;
email: string;
token: string;
}

interface UserInfo {
username: string;

password: string;

email?: string;

captcha?: string;
}
import { type RequestConfig, Requester } from './requester';
import type { ServerDataFormat } from './types';

const config: RequestConfig<unknown, ServerDataFormat> = {
baseURL: 'http://localhost:3000',
timeout: 10000,
//这里 设置 实例拦截器
reqOKFn: [
async config => {
//添加token
const token = localStorage.getItem('token');
if (token && config.headers) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
}
],
resOKFn: [
async response => {
return response;
}
]
};

const instance = new Requester<unknown, ServerDataFormat>(config);

async function login(userInfo: UserInfo) {
return await instance.post<UserInfo, ServerDataFormat<LoginResponse>>('/user/login', userInfo);
}


结尾

后头咱来聊聊,如何从后端到前端,制定统一返回格式和错误处理,前端联合axios、react-query、redux,进行优雅的前后端通信、错误处理和状态管理。


在这里插入图片描述

❌
❌