不凡说 / 文章详情

关于pomise封装ajax的那些事

2022-04-22 16:37 357

ajax封装promise

在实际工作中,我们往往不会直接采用原生ajax封装来请求接口数据,而是把ajax封装为promise形式.为的就是提高代码可读性,避免回调地狱.那具体如何封装呢,我们接下里详细说明下过程:

(一) 原生ajax的封装

基于XmlHttpRequest的原生封装:

/**
    * 封装的ajax函数
    * @param {Object} obj 接受一个对象参数,包含:
    * method 请求方式 默认GET  可传 "POST"
    * data 参数,默认为null
    * url 请求地址
    * success 请求成功后的回调函数,默认参数服务器返回的数据
    * fail 请求失败后的回调函数
*/
function ajax(params){
    if(!params.url){
        alert("请务必传入请求地址");
        return;
    }
    // 赋值默认参
    params = Object.assign({
        type: "GET",
        data: null,
        contentType: "application/x-www-form-urlencoded", // post请求时定义参数类型
        header: {}, // 与后台协定的头部信息
        dataType: "json", // 返回值类型
        success(){},
        fail(){},
    }, params);

    // 声明核心对象
    var xhr = new XMLHttpRequest();

    // 赋值返回值类型
    xhr.responseType = params.dataType;

    // 请求过程结束监听
    xhr.onload = function (){
        switch(xhr.status){
            case 200:
                params.success(xhr.response);
                break;
            case 400:
                params.fail(xhr.statusText, "参数错误");
                break;
            case 404: 
                params.fail(xhr.statusText, "URL信息错误");
                break;
            case 500:
                params.fail(xhr.statusText, "服务器内部错误");
                break;
            default:
                break;
        }
    }

    // 请求意外情况
    xhr.onerror = params.fail;

    // 请求过程的函数  onprogress

    // 参数序列化  qs.js
    var str = "";
    for(var i in params.data){
        // i => key params.data[i]  value   key:value => key=value&
        str += `&${i}=${params.data[i]}`;
    }
    // & name=value&name=value...
    str = str.slice(1);

    // 发送请求前做
    params.beforeSend && params.beforeSend();

    // 定义请求类型以及地址
    if(params.type.toUpperCase() == "GET"){
        xhr.open("GET", params.url + (params.data ? "?" + str : ""));
        // 发送请求
        xhr.send(null);
    }else {
        xhr.open("POST", params.url);
        xhr.setRequestHeader("Content-Type", params.contentType);
        for(var j in params.header){
            xhr.setRequestHeader(j, params.header[j]);
        }

        // 发送请求
        if(/json/.test(params.contentType)){
            xhr.send(JSON.stringify(params.data));
        }else if(/form/.test(params.contentType)){
            xhr.send(str);
        }
    }

}

我们在使用的时候,需要调用ajax,传入基本参数,并且需要传入回调函数来处理后续逻辑.

这里存在的问题是,如果需要发送请求1请求2,而请求2依赖于请求1的结果,则请求2必然要写在请求1的回调中,比如:

// 请求1
ajax({
    url:'/abc',
    success: function(res){
        var {id} = res.data;
        // 请求2 依赖于请求1的结果
        ajax({
            url: '/abc2?'+id,
            success: function(res){
                // ...
                // 之后的代码都需要在这里进行,可读性很差
            }
        })
}})

如何解决ajax的回调问题呢?

(二) ajax的promise封装

我们可以利用promise处理回调问题.

我们用延迟函数模拟一段ajax请求封装的函数,函数接受基本参数和回调,ajax返回结果后会把原先的参数稍作改动,并传入到回调函数.

	/**
       *  用延迟函数模拟ajax异步
       *  参数options  { params: {},success: Fn}
       * */
      function ajax(options) {
        // 模拟网络波动延迟时间
        var shakeTime = options.shakeTime || 1000;
        setTimeout(() => {
          // 模拟接受参数并处理返回结果,比如 a 变成了 a!!
          var rs = `${options.params}!!`;
          // 调用参数的回调把结果扔到外面处理
          options.success(rs);
        }, shakeTime);
      }

采用promise对上面的函数进行包装:

	function myAjax(params) {
		// 返回
        return new Promise((resolve, reject) => {
           // 这里调用的是上面封装的ajax方法
          ajax({
            params,
            success: function (res) {
              // 在这里成功后,不处理回调,而是直接resolve,把返回的结果传入
              resolve(res);
            },
          });
        });
      }

调用方式:

 myAjax({
     type:'get',
     params: {
         id: 3
     }
 }).then(function(res){
     //这里获取到上面myAjax中resolve的参数
     // 使用res结果处理接下来的逻辑...
 })

问题: 这样的确实现把ajax变为promise的一种封装,那么我们就可以对多个ajax配合async...await语法实现"异步回调变同步"的操作:

// 异步回调方式:
myAjax({
    ...
}).then(res=>{
    // 第二个请求
    myAjax({
    	id: res.id
    	...
	}).then(res=>{
    	...
	})
})
// 转换为"同步"模式
async function getUseSchool(){
    // 加入第一条获取的是用户明细,从而得到用户的schoolId
    var {schoolId} = await myAjax({
        url: '/xx/getUserInfo'
        id: 123
    });
    // 根据获取到的schoolId查询学校明细
    var schoolInfo = await myAjax({
        url: '/xx/getSchoolInfo'+schoolId
    });
    // 得到schoolInfo
}

通过以上操作,我们就可以解决前面提到的原生ajax的问题, 既提高了代码的可读性,而且不用写回调函数就能控制ajax的执行顺序.

(三) 关于并发和超时的处理

3.1 通过Promise.race()方法,我们还可以方便的处理ajax的超时问题:

 	  // 1. 带超时控制的ajax,假设5000ms超时
      // 1.1  使用Promise.race([p1,p2...]) 竞争,只有最快的会执行
      function myTimeoutHandleAjax(options) {
        // 默认3000ms
        var timeout = options.timeout || 3000;
        // 改参数可以模拟网络波动延迟
        var shakeTime = options.shakeTime || 1000;
        var p1 = myAjax({
            url: '/abc',
            ...options
        })
        var p2 = new Promise((resolve, reject) => {
          setTimeout(() => {
            // ajax.cancel()
            console.log('请求取消...');
            reject("请求超时!");
          }, timeout);
        });
		// 这里只有先获取结果的才会被执行并返回
        return Promise.race([p1,p2]);
      }
      // 1.2 测试在有超时控制的前提下进行请求
      myTimeoutHandleAjax({
        params: '这是用户1的参数',
        // timeout是超时时间,shakeTime是网络波动延迟. 如果 shkeTime>timeout 则判定为超时
        shakeTime: 6000,
        timeout: 3000,
      }).then(res=>{
        console.log('获取请求的结果:',res);
      }).catch(err=>{
        console.log(err);
      })

3.2 通过Promise.all()方法, 方便的处理多个请求都获取到结果后触发,比如首页几个请求都获取到结果后关闭loading遮罩层的需求:

     
      var pp1 = myTimeoutHandleAjax({
        params: '这是获取banner列表的参数'
      })
      var pp2 = myTimeoutHandleAjax({
        params: '这是获取hotList列表参数'
      })
      // 这里只有pp1和pp2都获取到结果才会执行
      Promise.all([pp1,pp2]).then(res=>{
         // 这里res获取的是一个数组类型的结果,包含多个ajax的结果
        console.log(res);
      })

到这里,基于原生ajax的promise封装原理基本都讲过了. 不过在开发中我们往往还需要对接口进行统一管理,方便扩展和维护.如何做呢?我们下期再讨论.