封装axios(ts版本)——题目无须花哨

549 阅读5分钟

我在网上看见很多版本的ts封装axios,各有特色,那么今天就看看我们到底适合什么样的axios封装,我会将我遇到的需求考虑进去,希望给你们带来帮助,同时欢迎指正

在下面我只是写大概封装的流程,有很多业务逻辑没有写出来,主要是针对类型上的处理

一. 使用class类封装
// * http/axios.ts
// axios基本配置
const config = {
    baseURL: "/api", //看自己的代理对象
    timeout: 15000, // 请求超时时间
    withCredentials: true, // 跨域时候允许携带凭证
    validateStatus: (status: number) => {
        return status >= 200 && status <= 500;
    }
};
//为了方便我将所需要的类型定义写在这里
interface Result<T = any> {
    code: number;
    msg: string;
    data: T;
}
// [Omit,Partial,Record](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype)不懂的可以去官网了解 
type Params = Partial<Record<string, unknown>>;//传入的参数
type Config = Omit<AxiosRequestConfig, "url" | "method">;

class Axios {
    //axios 实列
    private instance: AxiosInstance;
    //锁
    private lock: number = 0;
    constructor(config: AxiosRequestConfig) {
        //创建实例,挂载拦截器
        this.instance = axios.create(config);
        this.interceptorsRequest();
        this.interceptorsResponse();
    }
    // * 请求拦截
    protected interceptorsRequest() {
        this.instance.interceptors.request.use((config: AxiosRequestConfig) => {
            const headers = config.headers;
            // ... 忽略部分代码方便阅读
            // 在headers中添加属性和值
            // (config.headers as AxiosRequestHeaders)["XXX-XXX"] = "XXX"
            // 我这里判断token是否存在及是否需要token,因为在某些情况下不需要传token,但是又有token,虽然很鸡肋,但是没办法
            if (getToken() && !headers?.isToken) {
                (config.headers as AxiosRequestHeaders)["Blade-Auth"] = "自己的token"
            }
            return config;
        },
        (error: AxiosError) => {
            return Promise.reject(error);
        });
    }
    // * 响应拦截
    protected interceptorsResponse() {
        this.instance.interceptors.response.use((response: AxiosResponse) => {
            const { config, status: code, data } = response;
            //...忽略部分代码
            //下面data中的需要根据自己后端返回的数据自行更改
            const status = data.code || code;//防止data.code没有值,一般不会
            const message = data.msg || data.error_description || "未知错误";
            //单独将401的状态抽出来判断,退出登录之类的逻辑书写
            if (status === 401) {
                //这里为什么要加个锁,因为一个页面可能会有很多请求,请求全部为401,那么都会进来,执行退出登录的逻辑,虽然我在路由切换的时候去取消了正在执行的请求,但难免它会在前面执行,
                if (this.lock === 1) return false;
                this.lock === 0 && ElMessage.error(message);
                this.lock = 1;
                //...忽略部分业务代码
                return Promise.reject(data);
            } else if (status !== 200) {
                // 统一处理,状态不为200的,这里的状态是后端自定义抛出的,不是请求的状态
                ElMessage.error(message);
                return Promise.reject(data);
            }
            this.lock = 0;
            return data;
        },
        (error: AxiosError) => {
            //根据自己业务处理异常状态
            ElMessage.error(error.message);
            return Promise.reject(error);
        });
    }

    // * 请求方法
    // 重写请求方法让返回的类型不是any,有很多人在这里封装的时候就会在里面包一个promise,这个大可不必,它自己本身返回的就是个promise
    /**
        下面的传入泛型<T = any, R = Result<T>>,返回的是Promise<R>
        我们先看需求:
            1.一般我们从后端拿到的数据是在response.data中,这个data中的数据格式一般是固定的
            {code: number;msg: string;data: any;},反正差不多对吧,而这里面data才是我们真正需要的,
            然后我就定义一个Result的interface,这样我就不用每次去写Result了,如下
            上面说了他本身返回的就是一个promise,所以需要用Promise包裹一下
            request<T>(config: AxiosRequestConfig): Promise<Result<T>> {
                return this.instance.request(config);
            }
            这里和下面的不一样啊,怎么回事勒?来继续
            2.有时候后端返回的数据不按常理出牌怎么办?在返回的data中不是我们写的固定格式
            {code,msg,data},而是直接返回了固定格式中的data对象,没有code,msg,data,给你的
            就是一个对象,此时此刻,心里开始....
            
            当传入一个泛型时T,返回的是有固定格式包裹的类型;R是Result<T>,所以在Promise中的R就是默认的Result<T>;
            当第二个泛型参数传入时也就是R,如果R有类型传入,则R就是不默认的Result<T>,而是传入的R类型,在Promise中直接返回的R就没有Result包裹;
            
            这样我们就可以灵活控制返回的类型格式,当然我们也可以将Result写在api.ts中,这样的话如果大部分接口
            都实固定格式的话,就要写很多,这个看自己喜欢什么样的方式
            request<T = any, R = Result<T>>(config: AxiosRequestConfig): Promise<Result<T>> {
                return this.instance.request(config);
            }
            //下面的get,post等等都差不多,个人比较喜欢用request这一个就行了,这个也看自己
            
    */
    request<T = any, R = Result<T>>(config: AxiosRequestConfig): Promise<R> {
        return this.instance.request(config);
    }
    get<T = any, R = Result<T>>(url: string, params?: Params, config?: Config): Promise<R> {
        return this.instance.get(url, { params, ...config });
    }
    post<T = any, R = Result<T>>(url: string, params?: Params, config?: Config): Promise<R> {
        return this.instance.post(url, params, config);
    }
    put<T = any, R = Result<T>>(url: string, params?: Params, config?: Config): Promise<R> {
        return this.instance.put(url, params, config);
    }
    delete<T = any, R = Result<T>>(url: string, params?: Params, config?: Config): Promise<R> {
        return this.instance.delete(url, { params, ...config });
    }
}

export default new Axios(config);

定义api,说到这个有的人比较喜欢将api定义到class中统一调用,这样的好处是用一个实列就可以取出所有的api,不用去写一排 import { xxx, xxx } from "api";甚至更多,class中统一调用这个看起来很爽,但是不利于tree-shaking,一般自己是很反感这样的操作

// api.ts
import axios from "http/axios";

interface ImageCode {
    key: string;
    image: string;
}
// 这里我针对上面写了一个例子

// 1. 这个返回的就是{key: string; image: string;}
export const GetCaptcha = () => axios.request<never, ImageCode>({url: "/blade-auth/oauth/captcha"});

// 2. 这个返回的就是{ code: number; msg: string;data:{key: string; image: string;}}
export const GetCaptcha1 = () => axios.request<ImageCode>({url: "/blade-auth/oauth/captcha"});

// 固定的格式可以根据自己的需求更改,这样我们就可以在api中愉快的玩耍了


image.png

二. 不适用类封装axios如何修改默认的返回类型AxiosResponse<T>

基本操作和上面的class类封装没什么区别,区别在于这里没有类

// axios.ts
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
import { ElMessage } from "element-plus";

const instance = axios.create({
    baseURL: "/api",
    timeout: 15000, // 请求超时时间
    withCredentials: true, // 跨域时候允许携带凭证
    validateStatus: (status: number) => {
        return status >= 200 && status <= 500;
    }
});
// 这里的拦截和 class 类封装的是一样的操作,这里我就不再写一次了
instance.interceptors.request.use(
    (config: AxiosRequestConfig) => {
        //书写自己的逻辑
        return config;
    },
    (error: AxiosError) => {
        return Promise.reject(error);
    }
);
instance.interceptors.response.use(
    (response: AxiosResponse) => {
        const { data } = response;
        //书写自己的逻辑
        return data;
    },
    (error: AxiosError) => {
        ElMessage.error(error.message);
        return Promise.reject(error);
    }
);

export default instance;

如果这样去写api的时候返回的就是AxiosResponse<T>,而我们在响应拦截的时候返回的没有response,只返回了response中的data

image.png 为什么返回的是AxiosResponse<T>呢?那么要怎么做勒?下图是axios自己的.d.ts文件,我们只需要再定义一个不就可以了

image.png

新建axios.d.ts文件,键入下面代码,就可以愉快的玩耍了

import type { Axios } from "axios";

declare module "axios" {
    declare interface AxiosInstance extends Axios {
        request<T = any, R = Result<T>>(config: AxiosRequestConfig): Promise<R>;
        get<T = any, R = Result<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
        delete<T = any, R = Result<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
        head<T = any, R = Result<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
        options<T = any, R = Result<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
        post<T = any, R = Result<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
        put<T = any, R = Result<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
        patch<T = any, R = Result<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
    }
}

image.png

代码地址