基于 axios 的 http 请求工具
基于 axios 实现一个 http 请求工具,支持设置请求缓存和取消 http 请求等功能
首先实现一个 简单的 http 请求工具
import axios, {
AxiosError,
AxiosInterceptorManager,
AxiosRequestConfig,
AxiosResponse,
} from 'axios';
// 接口返回的数据格式
interface ResponseData<T = any> {
code: number;
data: T;
message: string;
}
// 发起请求的参数
interface RequestConfig extends AxiosRequestConfig {
url: string;
}
class Request {
private instance = axios.create({
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
withCredentials: true,
});
constructor() {
// 设置请求拦截
const request = this.instance.interceptors.request as AxiosInterceptorManager<RequestConfig>;
request.use(config => this.handleRequest(config));
// 设置响应拦截
const { response } = this.instance.interceptors;
response.use(config => this.handleResponse(config));
}
// 请求拦截
private handleRequest(config: RequestConfig): RequestConfig {
// TODO: 请求前拦截处理 loading, url 或请求头等
return config;
}
// 响应拦截
private handleResponse(response: AxiosResponse<Blob>): AxiosResponse {
// TODO: 响应后拦截处理 loading, 状态码或数据等
return response;
}
// 错误拦截: 处理错误
private handleError(error: AxiosError) {
// TODO: 请求出错时的处理
return Promise.reject(error);
}
async request<T = any>(requestConfig: RequestConfig) {
const { method = 'GET', ...config } = requestConfig;
try {
const response = await this.instance.request<ResponseData<T>>({ ...config, method });
return response.data;
} catch (error) {
this.handleError(error as AxiosError);
throw error; // 将异常重新抛出去,不要吃掉异常
}
}
get<T = any>(requestConfig: RequestConfig) {
return this.request<T>({ method: 'GET', ...requestConfig });
}
post<T = any>(requestConfig: RequestConfig) {
return this.request<T>({ method: 'POST', ...requestConfig });
}
put<T = any>(requestConfig: RequestConfig) {
return this.request<T>({ method: 'PUT', ...requestConfig });
}
patch<T = any>(requestConfig: RequestConfig) {
return this.request<T>({ method: 'PATCH', ...requestConfig });
}
delete<T = any>(requestConfig: RequestConfig) {
return this.request<T>({ method: 'DELETE', ...requestConfig });
}
}
封装 Request 类,将 axios 示例缓存在 instance 中,在 Request 类构造函数中设置拦截器。封装 request 函数作为发起请求的函数, 接收泛型表示接口返回的 data 字段的数据类型。增加 get、post、patch 等别名函数,代替传入 method。
示例
interface ResData {
name: string;
age: number;
}
const request = new Request()
const result = await request.get<ResData>({
url: '/user-info',
params: {
id: '12138',
},
});
增加拦截器的处理
在拦截器中同意处理一些东西,比如在请求拦截中发起全局 loading,在响应拦截中处理数据等等
loading
import axios, {
AxiosError,
AxiosInterceptorManager,
AxiosRequestConfig,
AxiosResponse,
} from 'axios';
// 接口返回的数据格式
interface ResponseData<T = any> {
code: number;
data: T;
message: string;
}
// 发起请求的参数
interface RequestConfig extends AxiosRequestConfig {
url: string;
// 请求时是否需要发起 loaing,默认为 true
loading?: boolean;
}
class Request {
...
constructor() {
// 设置请求拦截
const request = this.instance.interceptors.request as AxiosInterceptorManager<RequestConfig>;
request.use(config => this.handleRequestLoading(config));
request.use(config => this.handleRequest(config));
// 设置响应拦截
const { response } = this.instance.interceptors;
response.use(config => this.handleResponseLoading(config));
response.use(config => this.handleResponse(config));
}
// 请求拦截: 处理 loading
private handleRequestLoading(config: RequestConfig): RequestConfig {
const { loading = true, url } = config;
if (loading) openLoading(); // openLoading 是开启全局 Loading 的方法
return config;
}
// 响应拦截: 处理 loading
private handleResponseLoading(response: AxiosResponse<Blob>): AxiosResponse {
const {
config: { url, loading },
} = response;
closeLoading(); // closeLoading 是关闭全局 Loading 的方法
return response;
}
// 请求拦截
private handleRequest(config: RequestConfig): RequestConfig {
// TODO: 请求前拦截处理 loading, url 或请求头等
return config;
}
// 响应拦截
private handleResponse(response: AxiosResponse<Blob>): AxiosResponse {
// TODO: 响应后拦截处理 loading, 状态码或数据等
return response;
}
// 错误拦截: 处理错误
private handleError(error: AxiosError) {
// TODO: 请求出错时的处理
return Promise.reject(error);
}
...
}
export default Request;
上面的代码分别在请求拦截和响应拦截中获取 requestConfig 中的 loading 字段的值,如果 loading 的值为 true,则开启/关闭全局的 loading。
但是这样做有一个缺陷,全局 loading 一般都是单例的,如果同时有两个需要 loading 的请求发起,那么当一个请求完成时会关闭 loading,此时另一个请求还未完成,这样就不符合实际需求了
我们可以设计一个 Loading 类来管理 loading 状态
class Loading {
private loadingApiList: string[] = [];
open(key: string) {
if (this.loadingApiList.length === 0) openLoading();
this.loadingApiList.push(key);
}
close(key: string) {
const index = this.loadingApiList.indexOf(key);
if (index > -1) this.loadingApiList.splice(index, 1);
if (this.loadingApiList.length === 0) closeLoading();
}
}
- Loaing 类使用了 loadingApiList 将正在进行且需要 loading 的请求的 url 缓存起来。
- 每次请求需要 loading 时,调用 Loading 类的 open 函数,open 函数会判断当前是否有还在 Loading 的请求,如果没有则调起 loading,将请求的 url 存入 loadingApiList
- 当请求结束时调用 Loading 类的 close 函数,close 函数会将该请求的 url 从 loadingApiList 中删除,再判断当前是否还有请求需要 Loading,如果没有则关闭 loading
class Request {
...
constructor(){
...
this.loading = new Loagn();
}
// 请求拦截: 处理 loading
private handleRequestLoading(config: RequestConfig): RequestConfig {
const { loading = true, url } = config;
if (loading) this.loading!.open(url);
return config;
}
// 响应拦截: 处理 loading
private handleResponseLoading(response: AxiosResponse<Blob>): AxiosResponse {
const {
config: { url },
} = response;
this.loading!.close(url!);
return response;
}
...
}
注:使用 Class 类来处理 loading 是基于业务系统上的其他需要,或为了更优雅的应对其他复杂情况,并非一定要使用 Class 写法,用 hooks 或工具函数也可以
处理 http 响应状态和业务响应状态
// http 状态码对应的 message
const RESPONSE_STATUS_MESSAGE_MAP: Record<number | 'other', string> = {
400: '客户端请求的语法错误,服务器无法理解。',
401: '请求未经授权。',
403: '服务器是拒绝执行此请求。',
404: '请求不存在。',
405: '请求方法不被允许。',
500: '服务器内部错误,无法完成请求。',
501: '服务器不支持当前请求所需要的功能。',
502: '作为网关或代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。',
503: '由于临时的服务器维护或者过载,服务器当前无法处理请求。',
504: '作为网关或者代理服务器尝试执行请求时,没有从上游服务器收到及时的响应。',
other: '未知错误, 请稍后尝试',
};
class Request {
...
constructor(){
...
const { response } = this.instance.interceptors;
response.use(config => this.handleResponseLoading(config));
response.use(
config => this.handleResponseStatus(config),
error => this.handleError(error)
);
}
// 请求拦截: http 响应状态和业务响应状态
private handleResponseStatus(response: AxiosResponse<ResponseData>): Promise<AxiosResponse> {
const { status, data } = response;
if (status !== 200) {
const statusMessage = RESPONSE_STATUS_MESSAGE_MAP[status] || '服务器错误, 请稍后尝试';
return Promise.reject(new AxiosError('', response.statusText, response.config, response.request, response));
}
// 业务状态码根据自身系统接口规范处理
const { code } = data || {};
if (code === 401) {
// 401 一般为未登录或 token 过期处理,这里一般处理为退出登录和 refresh
return this.refresh(response); // refresh 函数会实现 token 刷新并重新请求,这里可以忽略
}
// 一般非 200 未错误状态,显示提示消息
if (code !== 200) {
const { message } = data;
// 将接口的 message 传递给 AxiosError
return Promise.reject(new AxiosError(message, response.statusText, response.config, response.request, response));
}
return Promise.resolve(response);
}
// 错误拦截: 处理错误,处理通知消息
private handleError(error: AxiosError) {
this.loading.close(error.config!.url!);
// 判断请求是否被取消,若果被取消不需要处理通知消息
if (axios.isCancel(error)) return error;
const { status, message } = error as AxiosError;
// 如果是 http 错误,使用状态码匹配错误信息,否则使用接口返回的 mesage
const _message = status === 200 ? message : RESPONSE_STATUS_MESSAGE_MAP[status!];
sendMessage(_message || RESPONSE_STATUS_MESSAGE_MAP.other); // 发送错误信息的 message
return error;
}
...
}
实现取消请求功能 – abort
未完待续…
实现 http 缓存功能 – cache
未完待续…uq