背景
在一个前端应用中,除了页面逻辑之外,通常都存在着大量的接口,怎么管理这些接口,怎么封装接口的调用,是开发过程中常见的问题。本文的目标是能够清晰的管理接口,方便的调用接口。
项目地址:vue-template
怎么管理前端请求
直接在页面中通过url调用接口
axios.post(`/api/updateUrser`, { ...data })
.then((result) => {
})
.catch((err) => {
....
})
这种方式非常直观,可读性很高,但复用接口只能把url又复制一份,容易出错;也不能直观的看出前端应用到底调用了哪些接口。因此需要一个集中管理接口的方案
统一配置所有接口url
直接把所有接口的url集中配置,这也是非常常见的请求管理方案。 api/index.js
const aboutApis = {
getUser: '/api/getUser',
updateUser: '/api/updateUser',
// ...
}
about页面
axios.get(aboutApis.getUser);
这种方案做到了集中管理,但依旧不够,一个接口并不仅仅只有url,还有请求体、返回值等等,因此我们需要一个更完善的请求管理方案,能够从接口配置中,获知这个接口的全部信息,url、请求体、返回值、请求头等都应该在这个配置中体现。
使用ts完善接口管理方案
得益于ts提供的类型系统,我们可以为一个请求配置标注我们需要的所有类型。
export interface GetUserInfoPayload {
name: string;
}
export interface UserInfoRes {
name: string;
tech: string;
}
/**
* @description 查询用户信息
*/
export function API_GET_USER_INFO(
payload: GetUserInfoPayload,
): ARC<CommonResponseData<UserInfoRes>> {
return {
url: 'api/getUserInfo',
method: 'get',
params: payload,
};
}
通过API_GET_USER_INFO这个函数,我们就对api/getUserInfo这个接口的信息一目了然了,调用这个接口,也不再需要从页面代码中查找它所需要的请求体、返回值等信息了。
这就是我们所需要的请求管理方案,后续所有的接口都通过这种形式组织。
axios的封装
基于请求管理方案封装axios
基于上面的请求配置,我们来基于axios封装一个request函数。它应该像这样被调用:
async function getUserInfo() {
const res = await request(API_GET_USER_INFO({ name: 'test' }));
ElMessage.success(res.data.data.name);
}
并且调用时,请求和响应的类型都有类型提示。
因为axios.request自身就已经实现了入参、出参的类型提示,我们只需要稍作修改即可。axios.request的源码:
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
- 声明ARC类型
新增axios.d.ts,AxiosRequestConfig<D>是Axios提供的请求类型,ARC本身表示请求类型类型,但范型T表示返回值类型,ARC中的范型D,它可以用来标注响应中请求配置的类型,但我们通常并不关心。
import 'axios';
declare module 'axios' {
// T用来标注返回值类型,D用来标注请求体类型
export interface ARC<T, D = any> extends AxiosRequestConfig<D> {
}
}
- 封装request
import axios, { type ARC, type AxiosError, type AxiosResponse } from 'axios';
// 创建axios实例
export const axiosIns = axios.create({
timeout: 10 * 1000,
method: 'POST',
});
export function request<R>(config: ARC<R>) {
return axiosIns.request<R, AxiosResponse<R>>(config);
}
- 配置与使用
配置
export interface GetUserInfoPayload {
name: string;
}
export interface UserInfoRes {
name: string;
tech: string;
}
/**
* @description 查询用户信息
*/
export function API_GET_USER_INFO(
payload: GetUserInfoPayload,
): ARC<CommonResponseData<UserInfoRes>> {
return {
url: 'api/getUserInfo',
method: 'get',
params: payload,
};
}
使用
async function getUserInfo() {
const res = await request(API_GET_USER_INFO({ name: 'test' }));
ElMessage.success(res.data.data.name);
}
请求拦截
看是否需要,这里统一加上apiVersion的query参数
// 请求拦截
axiosIns.interceptors.request.use((config) => {
const modifiedConfig = { ...config };
// 统一增加apiVersion=4
if (modifiedConfig.url && !modifiedConfig.url.includes('apiVersion=4')) {
if (modifiedConfig.url.includes('?')) {
modifiedConfig.url = `${modifiedConfig.url}&apiVersion=4`;
}
else {
modifiedConfig.url = `${modifiedConfig.url}?apiVersion=4`;
}
}
return modifiedConfig;
});
响应拦截
在响应拦截中我们会做三件事:
- 错误的统一拦截,http code不是2xx,统一处理错误
- 返回值code是4xx~599,统一报错,(基于前后端双方的具体约定)
- 返回值code=600,跳转到登录页,(基于前后端双方的具体约定)
ARC增加alertOnError,决定是统一报错,还是接口自己处理
import 'axios';
declare module 'axios' {
// D用来标注请求体类型,同名interface会被合并
export interface AxiosRequestConfig<D> {
alertOnError?: boolean;
}
// T用来标注返回值类型
export interface ARC<T, D = any> extends AxiosRequestConfig<D> {
}
}
响应拦截代码
// 响应拦截
axiosIns.interceptors.response.use(
(response) => {
const { data, config } = response as AxiosResponse<CommonResponseData>;
const { code, msg } = data;
// 200,正常的业务逻辑
if (code === 200) {
return Promise.resolve(response);
}
// 400~600,统一拦截的错误
if (code >= 400 && code < 600) {
// 如果要自定义错误提示,可以在config中配置alertOnError
if (config.alertOnError !== false) {
let message = msg || '服务错误';
ElMessage({
message,
type: 'error',
});
}
return Promise.reject(data);
}
if (code === 600) {
window.location.href = '/login';
return Promise.reject(new Error('登陆失效'));
}
return Promise.reject(data);
},
(error) => {
const { code } = error as AxiosError;
if (code === 'ECONNABORTED') {
ElMessage({
message: '访问超时,请刷新页面重试',
type: 'error',
});
}
else {
ElMessage({
message: '网络异常,请尝试刷新页面',
type: 'error',
});
}
return Promise.reject(error);
},
);