基于ts的axios封装

417 阅读4分钟

背景

在一个前端应用中,除了页面逻辑之外,通常都存在着大量的接口,怎么管理这些接口,怎么封装接口的调用,是开发过程中常见的问题。本文的目标是能够清晰的管理接口,方便的调用接口。
项目地址: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>;
  1. 声明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> {
    }
}
  1. 封装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);
}
  1. 配置与使用
    配置
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;
});

响应拦截

在响应拦截中我们会做三件事:

  1. 错误的统一拦截,http code不是2xx,统一处理错误
  2. 返回值code是4xx~599,统一报错,(基于前后端双方的具体约定)
  3. 返回值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);
    },
);