阅读 879

ts + vue 封装 axios

分步骤讲解

安装 axiosqs

yarn add axios
yarn add qs
复制代码

axios 配置

封装 axios,由于 axios 是插件所以新建一个 plugins 文件夹来放置:

// ./src/plugins/axios/axiosConfigs.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import store from '@/store';
import { ElMessage } from "element-plus";

// 创建axios的实例
const service = axios.create({
    // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL
    baseURL: process.env.NODE_ENV === 'production' ? `/` : '/api',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
    },
    withCredentials: false, // 跨域请求时是否需要使用凭证
    timeout: 30000,
    // `validateStatus` 定义对于给定的 HTTP 响应状态码是 resolve 或 reject  promise 。
    validateStatus() {
        // `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
        // 使用 async-await,处理 reject 情况较为繁琐,所以全部返回 resolve,在业务代码中处理异常
        return true;
    },

    // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
    transformResponse: [(data) => {
        if (typeof data === 'string' && data.startsWith('{')) {
            data = JSON.parse(data);
        }
        return data;
    }]
});

// 添加请求拦截器
service.interceptors.request.use((config: AxiosRequestConfig) => {
    // console.log('发送请求之前', config.url);

    // 获取 token ,并将其添加至请求头中
    let token = store.state.user.token;
    if(token){
        config.headers.Authorization = token;
        // config.headers.Authorization = 'Bearer ' + token;
    }

    return config;
}, (error: any) => {
    // console.log('发送请求错误', error.response, error.data);

    // 错误抛到业务代码
    error.data = {
        message: '服务器异常,请联系管理员!'
    };

    return Promise.reject(error);
});

//添加响应拦截器
service.interceptors.response.use((response: AxiosResponse) => {
    // console.log('响应拦截', response.status, response);

    /* 处理 http 错误,抛到业务代码 */
    const status = response.status;
    const decide = status < 200 || status >= 300;
    if (decide) {
        const message = showStatus(status);
        // console.log("处理 http 错误", message);
        if (typeof response.data === 'string') {
            response.data = { message };
        } else {
            response.data.message = message;
        }
        ElMessage({
            message,
            type: 'error',
            showClose: true
        })
        return Promise.reject(response.data);
    }

    return response;
}, (error: any) => {
    // console.log('请求错误', error, axios.isCancel(error), error.message);

    if (axios.isCancel(error)) {
        // console.log('重复请求: ' + error.message);
        ElMessage({
            message: '请勿重复请求',
            type: 'warning',
            showClose: true
        });
    } else {
        const message = '请求超时或服务器异常,请检查网络或联系管理员!';
        ElMessage({
            message,
            type: 'error',
            showClose: true
        });
    }

    return Promise.reject(error);
});

const showStatus = (status: number) => {
    let message = '';
    switch (status) {
        case 400:
            message = '请求错误(400)';
            break;
        case 401:
            message = '未授权,请重新登录(401)';
            break;
        case 403:
            message = '拒绝访问(403)';
            break;
        case 404:
            message = '请求出错(404)';
            break;
        case 408:
            message = '请求超时(408)';
            break;
        case 500:
            message = '服务器错误(500)';
            break;
        case 501:
            message = '服务未实现(501)';
            break;
        case 502:
            message = '网络错误(502)';
            break;
        case 503:
            message = '服务不可用(503)';
            break;
        case 504:
            message = '网络超时(504)';
            break;
        case 505:
            message = 'HTTP版本不受支持(505)';
            break;
        default:
            message = `连接出错(${status})!`;
    }
    return message;
    // return `${message},请检查网络或联系管理员!`
};

export default service;
复制代码

封装 axios 请求

设置请求及响应数据格式

设置响应数据格式,设置 getpost 等方法的格式:

// ./src/plugins/axios/types.ts
import { AxiosResponse, AxiosRequestConfig } from 'axios';

// 网络请求响应格式,T 是具体的接口返回类型数据
interface CustomSuccessData<T> {
    code: number;
    msg?: string;
    message?: string;
    data?: T;
    [keys: string]: any;
}

interface Get {
    <T>(url: string, config?: AxiosRequestConfig): Promise<CustomSuccessData<T>>;
}

interface Post {
    <T>(url: string, params?: string | object, config?: AxiosRequestConfig): Promise<CustomSuccessData<T>>;
}

// ... delete 等等

export {
    CustomSuccessData,
    Get,
    Post
}
复制代码

封装 axios 请求方法

封装 getpost 等方法,使用 request 统一调用:

// ./src/plugins/axios/request.ts
import service from '@/plugins/axios/axiosConfigs'
import { Get, Post } from './types'; // 接口泛型

// 封装 get 方法,类型为Get
const get: Get = async (url, config) => {
    const response = await service.get(url, { ...config});
    return response.data;
};

const post: Post = async (url, params, config) => {
    const response = await service.post(url, params, {...config});
    return response.data;
};

// ... delete 等等

// 使用 request 统一调用
const request = {
    get,
    post
};

export default request;
复制代码

接口配置

配置 api 接口,新增 api 文件夹:

// ./src/api/httpUrl.ts
const config: Api = {
    rootUrl: "http://localhost:8080/",
};

const httpApi: Api =  {
    // 测试接口
    banner: config.rootUrl + 'home/banner', // banner
    login: config.rootUrl + 'user/login', // 用户登录
}

export default httpApi;
复制代码

请求接口配置:

// ./src/api/requestApi.ts
import request from '@/plugins/axios/request'; // axios 封装
import '@/utils/interfaces/AjaxResponse'; // 后端响应数据接口
import '@/utils/interfaces/AjaxRequest'; // 前端请求数据接口
import httpUrl from "./httpUrl"; // 接口 url
import qs from "qs";

// 获取 banner
const getBanner = async () => {
    return await request.get<Array<AjaxResponse.Banner>>(httpUrl.banner);
}

// 用户登录
const login = async (params: AjaxRequest.login) => {
    return await request.post<string>(httpUrl.login, qs.stringify(params));
}

const handleError = (err: any) => {
    // console.log("请求错误", err);
    throw err;
}

export {
    getBanner,
    login,
    handleError
}
复制代码

使用

<template>
    <div class="home">
        {{num}}
        <el-button @click="apiTest">测试按钮</el-button>
    </div>
</template>

<script lang="ts" setup>
import { getBanner, login, handleError } from '@/api/requestApi'; // 请求接口

const num = ref(100);

const apiTest = async () => {
    console.log("apiTest");

    // get 请求
    const banner = await getBanner().catch(handleError);
    console.log('banner', banner);

    // post 请求
    // 后续会转成 vuex actions,此处只是测试
    let logintest = await login({
        tel: '13430046832', // 随手输的
        password: '123456'
    }).catch(handleError);
    console.log("logintest", logintest);

}
</script>
复制代码

错误测试

把请求头设置为 'Content-Type': 'application/json;charset=utf-8',请求 login 接口,报错:

throw.jpg

取消重复请求

设置 添加/移除/清空 请求的方法:

// ...
import qs from 'qs';

const service = axios.create({
    // ...
});

// 声明一个 Map 用于存储每个请求的标识 和 取消函数
const pending = new Map();

/**
 * @description: 添加请求
 * @param {AxiosRequestConfig} config
 */
const addPending = (config: AxiosRequestConfig) => {
    const url = [
        config.method,
        config.url,
        qs.stringify(config.params),
        qs.stringify(config.data)
    ].join('&');
    
    config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
        if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
            // console.log("请求队列中不存在当前请求,添加请求")
            pending.set(url, cancel);
        }
    })
}


/**
 * @description: 移除请求
 * @param {AxiosRequestConfig} config
 * @return {*}
 */
let isRemove = false
const removePending = (config: AxiosRequestConfig) => {
    const url = [
        config.method,
        config.url,
        qs.stringify(config.params),
        qs.stringify(config.data)
    ].join('&');

    if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
        // console.log("请求队列中已存在当前请求,不再重复请求");
        const cancel = pending.get(url);
        isRemove = true
        cancel(url); // 取消请求
        pending.delete(url);
    }
}

/**
 * @description: 清空 pending 中的请求(在路由跳转时调用)
 */
export const clearPending = () => {
    // console.log("清空请求队列");
    for (const [url, cancel] of pending) {
        cancel(url); // 取消请求
    }
    pending.clear(); // 清空请求
}
复制代码

在拦截器中加入:

// 添加请求拦截器
service.interceptors.request.use((config: AxiosRequestConfig) => {
    // console.log('发送请求之前', config.url);

    removePending(config) // 在请求开始前,对之前的请求做检查取消操作
    addPending(config) // 将当前请求添加到 pending 中

    // ...

    return config;
}, (error: any) => {
    // ...
});

//添加响应拦截器
service.interceptors.response.use((response: AxiosResponse) => {
    // console.log('响应拦截', response.status, response);

    removePending(response) // 在请求结束后,移除本次请求

    // ...

}, (error: any) => {
    if (axios.isCancel(error)) { // 取消请求
        if(isRemove){ // 路由切换导致的取消请求,不提示
            ElMessage({
                message: '请勿重复请求',
                type: 'warning',
                showClose: true
            });
            isRemove = false
        }
    } else {
        // ...
    }
    
    // ...
});
复制代码

在路由跳转时撤销所有请求

在路由跳转时撤销所有请求。在路由文件 ./src/router/index.ts 中加入:

// ./src/router/index.ts
import { clearPending } from "@/plugins/axios/axiosConfigs";
import store from '@/store'

// ...

// 路由守卫
router.beforeEach((to, from, next) => {

    clearPending(); // 在跳转路由之前,先清除所有的请求

    if(to.meta.isAuth){
        const token = store.state.user.token;
        if(token){
            next();
        }else{
            next({
                name: 'login'
            });
        }
    }else{
        next();
    }

});

// ...
复制代码

axios 配置完整代码

// ./src/plugins/axios/axiosConfigs.ts
/*
 * @Author: una
 * @Date: 2021-06-16 17:16:12
 * @LastEditors: una
 * @LastEditTime: 2021-07-09 14:51:28
 * @Description: 封装 axios
 */
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import store from '@/store';
import { ElMessage } from "element-plus";
import qs from 'qs';

// 创建axios的实例
const service = axios.create({
    // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL
    baseURL: process.env.NODE_ENV === 'production' ? `/` : '/api',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
        // 'Content-Type': 'application/json;charset=utf-8'
    },
    withCredentials: false, // 跨域请求时是否需要使用凭证
    timeout: 30000,
    // `validateStatus` 定义对于给定的 HTTP 响应状态码是 resolve 或 reject  promise 。
    validateStatus() {
        // `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
        // 使用 async-await,处理 reject 情况较为繁琐,所以全部返回 resolve,在业务代码中处理异常
        return true;
    },

    // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
    transformResponse: [(data) => {
        if (typeof data === 'string' && data.startsWith('{')) {
            data = JSON.parse(data);
        }
        return data;
    }]
});

// 声明一个 Map 用于存储每个请求的标识 和 取消函数
const pending = new Map();

/**
 * @description: 添加请求
 * @param {AxiosRequestConfig} config
 */
const addPending = (config: AxiosRequestConfig) => {
    const url = [
        config.method,
        config.url,
        qs.stringify(config.params),
        qs.stringify(config.data)
    ].join('&');

    config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
        if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
            // console.log("请求队列中不存在当前请求,添加请求")
            pending.set(url, cancel);
        }
    });
}

/**
 * @description: 移除请求
 * @param {AxiosRequestConfig} config
 * @return {*}
 */
let isRemove = false
const removePending = (config: AxiosRequestConfig) => {
    const url = [
        config.method,
        config.url,
        qs.stringify(config.params),
        qs.stringify(config.data)
    ].join('&');

    if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
        // console.log("请求队列中已存在当前请求,不再重复请求");
        const cancel = pending.get(url);
        isRemove = true
        cancel(url); // 取消请求
        pending.delete(url);
    }
}

/**
 * @description: 清空 pending 中的请求(在路由跳转时调用)
 */
export const clearPending = () => {
    // console.log("清空请求队列");
    for (const [url, cancel] of pending) {
        cancel(url); // 取消请求
    }
    pending.clear(); // 清空请求
}

// 添加请求拦截器
service.interceptors.request.use((config: AxiosRequestConfig) => {
    // console.log('发送请求之前', config.url);

    removePending(config); // 在请求开始前,对之前的请求做检查取消操作
    addPending(config); // 将当前请求添加到 pending 中

    // 获取token,并将其添加至请求头中
    const token = store.state.user.token;
    if(token){
        config.headers.Token = token;
        // config.headers.Authorization = 'Bearer ' + token;
    }

    return config;
}, (error: any) => {
    // console.log('发送请求错误', error.response, error.data);

    // 错误抛到业务代码
    error.data = {
        message: '服务器异常,请联系管理员!'
    };

    return Promise.reject(error);
});

//添加响应拦截器
service.interceptors.response.use((response: AxiosResponse) => {
    // console.log('响应拦截', response.status, response);

    removePending(response); // 在请求结束后,移除本次请求

    /* 处理 http 错误,抛到业务代码 */
    const status = response.status;
    const decide = status < 200 || status >= 300;
    if (decide) { // http 错误
        const message = showStatus(status);
        // console.log("处理 http 错误", message);
        if (typeof response.data === 'string') {
            response.data = { message };
        } else {
            response.data.message = message;
        }
        ElMessage({
            message,
            type: 'error',
            showClose: true
        });
        return Promise.reject(response.data);
    }else { // 接口连接成功
            if(response.data.code == 200){
                return response.data
            }else { // 接口报错
                if(response.config.url){
                    if(response.config.url.indexOf('login') > -1){
                        store.commit('user/SET_LOGIN_ERR_MSG', response.data.data)
                        store.commit('user/SET_TOKEN', '')
                    }else{
                        ElMessage({
                            message: response.data.data || response.data.message,
                            type: 'error',
                            showClose: true
                        });
                    }
                    throw response;// 抛出错误
                }
            }
    }
}, (error: any) => {
    // console.log('请求错误', error, axios.isCancel(error), error.message);

    if (axios.isCancel(error)) { // 取消请求
        if(isRemove){ // 路由切换导致的取消请求,不提示
            // console.log('重复请求: ' + error.message);
            ElMessage({
                message: '请勿重复请求',
                type: 'warning',
                showClose: true
            });
            isRemove = false
        }
    } else {
        const message = '请求超时或服务器异常,请检查网络或联系管理员!';
        ElMessage({
            message,
            type: 'error',
            showClose: true
        });
    }

    return Promise.reject(error);
});

const showStatus = (status: number) => {
    let message = '';
    switch (status) {
        case 400:
            message = '请求错误(400)';
            break;
        case 401:
            message = '未授权,请重新登录(401)';
            break;
        case 403:
            message = '拒绝访问(403)';
            break;
        case 404:
            message = '请求出错(404)';
            break;
        case 408:
            message = '请求超时(408)';
            break;
        case 500:
            message = '服务器错误(500)';
            break;
        case 501:
            message = '服务未实现(501)';
            break;
        case 502:
            message = '网络错误(502)';
            break;
        case 503:
            message = '服务不可用(503)';
            break;
        case 504:
            message = '网络超时(504)';
            break;
        case 505:
            message = 'HTTP版本不受支持(505)';
            break;
        default:
            message = `连接出错(${status})!`;
    }
    return message;
    // return `${message},请检查网络或联系管理员!`
};

export default service;
复制代码

扩展:设置全局变量

自定义属性添加到每个组件实例中,可以通过 this 访问(不建议使用):

import { createApp } from 'vue'
import App from './App.vue'
import route from './route'

// 配置请求数据
import { AxiosInstance } from "axios";
import Axios from "axios";

// 全局配置Axios
declare module '@vue/runtime-core' {
    interface ComponentCustomProperties {
        $axios: AxiosInstance;
    }
}
let app = createApp(App)
app.config.globalProperties.$axios = Axios;  // this.Axios
app.use(route)
app.mount('#app')
复制代码

使用:

import httpUrl from "@/api/httpUrl"; // 接口 url

// ...

mounted(){

    const apiTest = () => {

        // eslint-disable-next-line no-unexpected-multiline
        (this as any).$axios.get(httpUrl.banner).then((res: any) => {
            let data = res.data.data
            console.log("banner", data)
        }).catch((error: any) => {
            console.log('获取首页banner 失败', error.)
        })

    }

    apiTest()
}
复制代码

此处只作为一个扩展,不推荐使用这种方式。

参考文章

文章分类
前端
文章标签