axios

在日常开发中,ajax请求是必不可少的。

本章目标

针对以上场景二次封装,减少代码量,并优化用户体验。 例:

// 新增或编辑数据时
// 验证表单
form.validate((valid: any) => {
    if (valid) {
        // 提交数据的ajax函数
        submitFun(params).then((res: any) => {
            // 业务成功后逻辑
        })
    }
});

// 登录时
// 验证表单
form.validate((valid: any) => {
    if (valid) {
        // 提交数据的ajax函数
        submitFun(params).then((res: any) => {
            // 业务成功后逻辑
            // 登录成功后逻辑
        }).catch(() => {
            // 业务失败后逻辑
            // 刷新验证码等逻辑
        });
    }
});

具体实现

加载遮罩

在实际交互中,请求数据时为了避免频繁请求和提升用户体验,我们会在请求时加一个遮罩。 在这里我们基于element-ui的Loading组件封装一个遮罩类

import { Loading } from 'element-ui'
let mask: any = null;
export default {
    count: 0,
    // 加载遮罩
    loading(message: string) {
        // 计数器加1
        this.count++;
        // console.log('loading,请求总数:', this.count);
        mask = Loading.service({
            lock: true,
            text: message,
            spinner: 'el-icon-loading',
            background: 'rgba(0, 0, 0, 0.7)'
        });
    },
    // 关闭遮罩
    clear() {
        const me = this as any;
        if (me.count > 1) {
            // 当计数器大于1时减1
            me.count--;
            // console.log('loadingend,请求总数减一:', me.count);
        } else {
            // console.log('loadingend,请求总数归0隐藏遮罩')
            // 当计数器等于1时隐藏遮罩
            // 计数器归0
            me.count = 0;
            mask.close();
        }
    }
}

基于axios和element-ui的Message组件,以及遮罩类封装request类

import axios from 'axios'
import { Message } from 'element-ui';
import mask from '@/utils/mask';
// 相关配置,方便移植到其他项目
const requestConfig = {
    // 网络错误提示语
    errorMessage: "您的网络不佳,请检查您的网络"
}
const service = axios.create({
    withCredentials: true,
    // api 的 base_url
    // 在vue配置中设置,可以根据不同环境来配置不同的地址,避免出错
    baseURL: process.env.VUE_APP_BASEURL
});

// 拦截请求
service.interceptors.request.use(
    (config: any) => {
        // 不管怎么样都要显示加载遮罩,否则遮罩类的计数逻辑会出问题
        // toastMessage为自定义配置,在发起请求时传入
        mask.loading(config.toastMessage);
        return config;
    },
    error => {
        return Promise.reject(error)
    }
)
// 拦截请求结果
service.interceptors.response.use(
    (res: any) => {
        mask.clear();
        return res
    },
    (error: any) => {
        mask.clear();
        // 网络错误错误是显示提示信息
        if (error.message == "Network Error") {
            Message({
                message: requestConfig.errorMessage,
                type: 'error',
                customClass: "zZindex"
            });
        }
        return Promise.reject(error)
    }
)
export default {}

鉴权

前后端分离开发是现在主流开发模式,那么用户登录后在每次请求时都需要先后端提交token进行鉴权 在这里我们通过cookie保存token信息,这里先封装一个cookie类

import Cookies from 'js-cookie'
// 所有的cookie都加统一的前缀,前缀来至于vue项目配置,这样可以避免同域名下cookie污染
const appName = process.env.VUE_APP_NAME;
const cookies = {
    /**
    * @description 存储 cookie 值
    * @param {String} name cookie name
    * @param {String} value cookie value
    * @param {Object} setting cookie setting
    */
    set: function(name: string = 'default', value: string = '', cookieSetting: any = {}) {
        const currentCookieSetting = {
            expires: 1
        }
        Object.assign(currentCookieSetting, cookieSetting)
        Cookies.set(`${appName}-${name}`, value, currentCookieSetting)
    },
    /**
     * @description 拿到 cookie 值
     * @param {String} name cookie name
     */
    get: function(name: string = 'default') {
        return Cookies.get(`${appName}-${name}`)
    },
    /**
     * @description 拿到 cookie 全部的值
     */
    getAll: function() {
        return Cookies.get()
    },
    /**
     * @description 删除 cookie
     * @param {String} name cookie name
     */
    remove: function(name: string = 'default') {
        return Cookies.remove(`${appName}-${name}`)
    }
}
export default cookies

在这里我们可以再次封装request类

import axios from 'axios'
import { Message, MessageBox } from 'element-ui';
import mask from '@/utils/mask';
import cookies from '@/utils/store/cookies'
import { get } from 'lodash';
// 相关配置,方便移植到其他项目
const requestConfig = {
    // 判断请求是否成功的节点名称
    successProperty: "code",
    // 请求成功返回值,预留配置,后面用到
    successValue: '200',
    // 登录过期返回值
    expiredValue: 401,
    // token名称,在这里存取都是同一个名称
    tokenName: 'Authorization',
    // 像后端发起请求时token数据前缀
    tokenPrefixes: 'Bearer ',
    // 网络错误提示语
    errorMessage: "您的网络不佳,请检查您的网络"
}
const service = axios.create({
    withCredentials: true,
    // api 的 base_url
    // 在vue配置中设置,可以根据不同环境来配置不同的地址,避免出错
    baseURL: process.env.VUE_APP_BASEURL
});

// 拦截请求
service.interceptors.request.use(
    (config: any) => {
        // 获取存储在cookies中的token
        const token = cookies.get(requestConfig.tokenName);
        if (token && token != 'undefined') {
            // 请求头自动添加token
            config.headers[requestConfig.tokenName] = `${requestConfig.tokenPrefixes} ${token}`;
        }
        // 不管怎么样都要显示加载遮罩,否则遮罩类的计数逻辑会出问题
        // toastMessage为自定义配置,在发起请求时传入
        mask.loading(config.toastMessage);
        return config;
    },
    error => {
        return Promise.reject(error);
    }
)
// 拦截请求结果
service.interceptors.response.use(
    (res: any) => {
        mask.clear();
        // 校验登录是否过期
        const data = res.data;
        if (data && get(data, requestConfig.successProperty) == requestConfig.expiredValue) {
           // 退出登录逻辑
        }
        return res;
    },
    (error: any) => {
        mask.clear();
        // 网络错误错误是显示提示信息
        if (error.message == "Network Error") {
            Message({
                message: requestConfig.errorMessage,
                type: 'error',
                customClass: "zZindex"
            });
        }
        return Promise.reject(error);
    }
)

export default {}

常用请求封装

模拟请求

在实际开发过程中,可能会存在开发时后端并未介入的情况,我们可以在request类新增一个模拟异步方法。这样只需要和后端预定好数据格式和字段名称即可开发,减少不必要的等待时间。

/**
 * 获取模拟异步方法,你传入什么数据就返回什么数据
 *
 * @export
 * @param {*} data
 * @return {*} 模拟数据
 */
export function getJson(data: any):any {
    return new Promise((resolve) => {
        resolve({
            // 模拟返回成功状态
            code: requestConfig.successValue,
            // 传入的数据
            data: data,
            // 分页用
            total: 40
        });
    });
}

如果以业务逻辑来区分ajax请求,一般分为一下几种

  1. 发起请求后,业务成功才执行后续动作,可能会有提示信息。业务失败一般都会有提示信息。多用于数据的增删改查.

  2. 发起请求后,业务失败和成功分别有不同的后续业务动作,一般都会有提示信息。多用于业务流处理,登录等场景。

    针对以上场景,我们可以先封装一个基类方法,这个方法用来处理后端返回的数据,业务失败和成功分别走不同的逻辑,在使用时我们只需要通过then()与catch()来区分即可

    /**
    * 基础提交数据函数
    * 成功失败都有回调
    *
    * @export
    * @param {*} data axios配置
    * @param {*} options 额外配置 [{
    *         successCode = requestConfig.successValue, 提交成功状态码
    *         isSuccessToast = false,提交成功是否显示提示
    *         isNoToast = false 是否隐藏提示,为ture时不会有任何提示
    *         toastMessage = "Loading..." 遮罩提示文字
    *     }={}]
    * @return {*}
    */
    export function axiosRequest(data: any,
     {
         successCode = requestConfig.successValue,
         isSuccessToast = false,
         isNoToast = false,
         toastMessage = "Loading..."
     }: any = {}): Promise<any> {
     return new Promise((resolve, reject) => {
         // 设置遮罩提示文字
         data.toastMessage = toastMessage;
         service.request(data).then((res: any) => {
             const data = res.data,
                 // 判断提交数据结果
                 success = get(data, requestConfig.successProperty) == successCode,
                 // 确定消息提示类型
                 type = success ? 'success' : 'error';
             // 只要isNoToast不为true,请求失败必然提升
             // isSuccessToast为ture时,请求成功也会提示
             if (isSuccessToast || !success) {
                 const mes = data.msg;
                 // 登录过期错误不做提示
                 if (!isNoToast && data.code != requestConfig.expiredValue) {
                     Message({
                         message: mes,
                         type: type,
                         customClass: "zZindex"
                     });
                 }
             }
             // 标识请求结果状态
             data.success = success;
             if (success) {
                 resolve(res);
             } else {
                 reject(res);
             }
         }).catch(() => {
             // 请求失败
             reject({
                 success: false,
                 data: null
             });
         });
     });
    }

    在这个基础方法中我们对后端数据进行了处理,并针对常用场景进行了配置化处理,但是这个方法并不推荐直接使用,我们需要针对这个方法再次扩展一个针对场景2的方法

    /**
    * 提交数据函数
    * 成功失败都有回调
    * @export
    * @param {*} data axios配置
    * @param {*} options 额外配置 [{
    *         successCode = requestConfig.successValue, 提交成功状态码
    *         isSuccessToast = false,提交成功是否显示提示
    *         isNoToast = false 提交失败是否隐藏提示
    *     }={}]
    * @returns
    */
    export function ajaxBack(data: any, options?: any) {
     return new Promise((resolve, reject) => {
         axiosRequest(data, options).then((res: any) => {
             // 一般来说前端只需要res.data中的数据
             resolve(res.data);
         }).catch(res => {
             reject(res);
         });
     });
    }

    对于场景1,我们可以在场景2的基础上二次封装,一般来说可以分为get、post两个方法

/**
 * get 方式提交数据
 * 无错误回调
 * @export
 * @param {string} url 地址
 * @param {*} [params] 参数
 * @param {*} options 额外配置 [{
 *         successCode = requestConfig.successValue, 提交成功状态码
 *         isSuccessToast = false,提交成功是否显示提示
 *         isNoToast = false 提交失败是否隐藏提示
 *     }={}]
 * @returns
 */
export function ajax(url: string, params?: any, options?: any) {
    return new Promise((resolve) => {
        ajaxBack({
            url: url,
            method: "get",
            params: params
        },
            options
        ).then(res => {
            resolve(res);
        }).catch(() => { });
    });
}

/**
 * 通过data提交数据,可设置数据提交方式
 * 无错误回调
 * @export
 * @param {string} method 数据提交方式
 * @param {string} url 地址
 * @param {*} [params] 参数
 * @param {*} options 额外配置 [{
 *         successCode = requestConfig.successValue, 提交成功状态码
 *         isSuccessToast = false,提交成功是否显示提示
 *         isNoToast = false 提交失败是否隐藏提示
 *     }={}]
 * @return {*}
 */
export function ajaxDataMethod(method: string, url: string, params?: any, options?: any): Promise<any> {
    return new Promise((resolve) => {
        ajaxBack({
            url: url,
            method: method,
            data: params
        },
            options
        ).then(res => {
            resolve(res);
        }).catch(() => { });
    });
}


/**
 * POST 方式提交数据
 * 无错误回调
 * @export
 * @param {string} url 地址
 * @param {*} [params] 参数
 * @param {*} options 额外配置 [{
 *         successCode = requestConfig.successValue, 提交成功状态码
 *         isSuccessToast = false,提交成功是否显示提示
 *         isNoToast = false 提交失败是否隐藏提示
 *     }={}]
 * @returns
 */
export function ajaxP(url: string, params?: any, options?: any) {
    return ajaxDataMethod("POST", url, params, options)
}

另外你也可以根据业务需求再次封装一些常用场景,例:

/**
 * PATCH 方式提交数据
 * 无错误回调
 * @export
 * @param {string} url 地址
 * @param {*} [params] 参数
 * @param {*} options 额外配置 [{
 *         successCode = requestConfig.successValue, 提交成功状态码
 *         isSuccessToast = false,提交成功是否显示提示
 *         isNoToast = false 提交失败是否隐藏提示
 *     }={}]
 * @returns
 */
export function ajaxH(url: string, params?: any, options?: any) {
    return ajaxDataMethod("PATCH", url, params, options)
}

/**
 * put 方式提交数据
 * 无错误回调
 * @export
 * @param {string} url 地址
 * @param {*} [params] 参数
 * @param {*} options 额外配置 [{
 *         successCode = requestConfig.successValue, 提交成功状态码
 *         isSuccessToast = false,提交成功是否显示提示
 *         isNoToast = false 提交失败是否隐藏提示
 *     }={}]
 * @returns
 */
export function ajaxPut(url: string, params?: any, options?: any) {
    return ajaxDataMethod("PUT", url, params, options)
}

/**
 * delete 方式提交数据
 * 无错误回调
 * @export
 * @param {string} url 地址
 * @param {*} [params] 参数
 * @param {*} options 额外配置 [{
 *         successCode = requestConfig.successValue, 提交成功状态码
 *         isSuccessToast = false,提交成功是否显示提示
 *         isNoToast = false 提交失败是否隐藏提示
 *     }={}]
 * @returns
 */
export function ajaxDelete(url: string, params?: any, options?: any) {
    return ajaxDataMethod("DELETE", url, params, options)
}

使用

假如我们有一个系统模块,针对不同接口我们可以这样定义,例:

import {
    ajax, ajaxP, ajaxPut, ajaxDelete, ajaxBack
} from '@/utils/data/request';
// 系统列表
export function list(params: any) {
    return ajaxBack({
        url: `system/sys/list`,
        method: "get",
        params: params
    })
}
// 删除系统
export function del(id: any) {
    return ajaxDelete(`system/sys/${id}`, null, {
        isSuccessToast: true
    })
}
// 新建系统
export function add(params: any) {
    return ajaxP('system/sys', params, {
        isSuccessToast: true
    })
}
// 修改系统
export function edit(params: any) {
    return ajaxPut('system/sys', params, {
        isSuccessToast: true
    })
}
// 系统详情
export function info(params: any) {
    return ajax(`system/sys/${params.id}`)
}
// 系统改变状态
export function status(params: any) {
    return ajaxPut(`system/sys/changeStatus/${params.id}/${params.status}`, null, {
        isSuccessToast: true
    })
}
// 导出
export function exportExcel(params: any) {
    return ajax('system/sys/export', params)
}

通过封装request类,我们可以做到用最少的代码来应对最多变的后端🐶

结语

  1. 本文主要针对pc端场景进行了封装,那么移动端应该如何封装呢?

  2. 本文对每个请求都做了自动遮罩处理,那么是否存在不需要自动遮罩的场景,这种需求如何处理?

  3. 上一章我们讲了数据代理如何实现,那么在实际场景中,本章内容如何与数据代理结合使用?

  4. 假如后端返回的数据格式五花八门,格式不统一,如何处理?

最后更新于