# axios

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

## 本章目标

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

```javascript
// 新增或编辑数据时
// 验证表单
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组件封装一个遮罩类

```javascript
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类

```javascript
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类

```javascript
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类

```javascript
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类新增一个模拟异步方法。这样只需要和后端预定好数据格式和字段名称即可开发，减少不必要的等待时间。

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

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

1. 发起请求后，业务成功才执行后续动作，可能会有提示信息。业务失败一般都会有提示信息。多用于数据的增删改查. &#x20;
2. 发起请求后，业务失败和成功分别有不同的后续业务动作，一般都会有提示信息。多用于业务流处理，登录等场景。 &#x20;

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

   ```javascript
   /**
   * 基础提交数据函数
   * 成功失败都有回调
   *
   * @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的方法

   ```javascript
   /**
   * 提交数据函数
   * 成功失败都有回调
   * @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两个方法

```javascript
/**
 * 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)
}
```

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

```javascript
/**
 * 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)
}
```

#### 使用

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

```javascript
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. 假如后端返回的数据格式五花八门，格式不统一，如何处理？
