axios-3-dispatchRequest

219 阅读5分钟

axios 中的发送请求

前面讲解了 mergeConfig拦截器等方法,今天了解发送请求dispatchRequest这个核心方法

dispatchRequest

请求拦截器,和响应拦截器,都是一个数组, var chain = [dispatchRequest, undefined]; axios 通过 Promise 链条把请求拦截器数组,chain,响应拦截器数组组成一个整体,其中dispatchRequest处于链条的中央,是核心方法

 var dispatchRequest = function dispatchRequest(config) {
     var adapter = config.adapter || defaults_1.adapter;
      return adapter(config).then(
          function onAdapterResolution(response){
              response.data = transformData.call(
                config,
                response.data,
                response.headers,
                response.status,
                config.transformResponse
          );
          return response;
      },function onAdapterRejection(reason){
           if (!isCancel(reason)) {
        // Transform response data
        if (reason && reason.response) {
          reason.response.data = transformData.call(
            config,
            reason.response.data,
            reason.response.headers,
            reason.response.status,
            config.transformResponse
          );
        }
      }

      return Promise.reject(reason);
      }
 }

dispatchRequest接受传入config配置

如果configadapter就使用传入的adapter,否则就用默认的adapter
一般情况下,都时用的默认配置,adapter为一个接收config,返回一个Promise函数
其中利用transformData方法对Promisevaluereason 进行再一步的处理,返回给下一个Promise链条
adapter进行多次封装,真正发送的方法是xhr

xhr

发送请求的方法,使用的 xhr 发送请求,没有使用fetch,返回一个Promise

 var xhr = function xhrAdapter(config){
      return new Promise(function dispatchXhrRequest(resolve, reject) {
      
           var requestData = config.data;
           var request = new XMLHttpRequest();
           
            var fullPath = buildFullPath(config.baseURL, config.url);
            
            request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
            
            request.timeout = config.timeout;
            
          
        ...
        function onloadend() {
        
         var response = {
              data: responseData,
              status: request.status,
              statusText: request.statusText,
              headers: responseHeaders,
              config: config,
              request: request
        };
        
        settle(function _resolve(value) {
          resolve(value);
          done();
         }, function _reject(err) {
          reject(err);
          done();
         }, response);
        }
        
         request.onloadend = onloadend;
        ...
        request.send(requestData);
      }
 }

上面是核心方法,返回一个Promise
可以看出axiosurl进行封装,第一次是使用buildFullPathbaseURLurl进行合并 ,第二次是发送请求的时候,使用buildURL,带上config.parmas,又进行了一次封装

先看方法buildFullPath,看如何合并URl

buildFullPath

baseURLurl进行合并

 /**
   * Creates a new URL by combining the baseURL with the requestedURL,
   * only when the requestedURL is not already an absolute URL.
   * If the requestURL is absolute, this function returns the requestedURL untouched.
   *
   * @param {string} baseURL The base URL
   * @param {string} requestedURL Absolute or relative URL to combine
   * @returns {string} The combined full path
   */
var buildFullPath = function buildFullPath(baseURL, requestedURL) {
    if (baseURL && !isAbsoluteURL(requestedURL)) {
      return combineURLs(baseURL, requestedURL);
    }
    return requestedURL;
  };

判断baseURL存在,并且 requestedURL不是绝对路径,执行combineURLs合并方法
否则直接返回requestedURL

判断是否是绝对路径也很有意思

var isAbsoluteURL = function isAbsoluteURL(url){
    return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url);
}

使用正则判断,只要满足开头是(小写英文字母+((小写英文字母 | 数字 | - | . )0个或者多个) 一个或者多个) + //
形如aa://或者"a2.://" 都可以满足

然后执行combineURLs方法,它也很有意思

var combineURLs = function combineURLs(baseURL, relativeURL) {
    return relativeURL
      ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
      : baseURL;
  };

如果baseURL是以/结尾,就替换成'',如果是relativeURL是以/开头,就把/替换成''

经过这种变化,最终返回形如https://www.aaa/aaaa这种fullpath路径

buildURL

  var buildURL = function buildURL(url, params, options) {
    // hash标记下标
    var hashmarkIndex = url.indexOf('#');

    if (hashmarkIndex !== -1) {
      url = url.slice(0, hashmarkIndex);
    }
    
 
  
    var _encode = options && options.encode || encode;

    var serializerParams = utils.isURLSearchParams(params) ?
      params.toString() :
      new AxiosURLSearchParams_1(params, options).toString(_encode);

    if (serializerParams) {
      url += (url.indexOf('?') === -1 ? '?' : '&') + serializerParams;
    }

    return url;
  };

接收三个参数url,params,options

  1. 获取纯净url 判断是否有#,使用slice切割,只要url,不要后面的参数
  2. encode 参数(params) 判断params是否是URLSearchParams格式,如果是,调用toString方法,URLSearchParams 无法解析完成的 url
    URLSearchParams
var paramsString2 = "?query=value";
var searchParams2 = new URLSearchParams(paramsString2);
searchParams2.has("query") //true

如果不是URLSearchParams格式,就自己写一个类似URLSearchParams的方法


  function AxiosURLSearchParams(params, options) {
    this._pairs = [];
    // 对 params 进行处理,比如如果是Date 格式,要执行value.toISOString() 方法
    // 如果是 null ,转化为 ''
    
    params && toFormData_1(params, this, options);
  }
  
  var prototype = AxiosURLSearchParams.prototype;
  
  prototype.append = function append(name, value) {
    this._pairs.push([name, value]);
  };
    
  // 编码,否则无法解析
 // function encode(val) {
  //   return encodeURIComponent(val).
  //     replace(/%3A/gi, ':').
  //     replace(/%24/g, '$').
  //     replace(/%2C/gi, ',').
  //     replace(/%20/g, '+').
  //     replace(/%5B/gi, '[').
  //     replace(/%5D/gi, ']');
  // }
  
 prototype.toString = function toString(encoder) {
    var _encode = encoder ? function(value) {
      return encoder.call(this, value, encode$1);
    } : encode$1;

    return this._pairs.map(function each(pair) {
      return _encode(pair[0]) + '=' + _encode(pair[1]);
    }, '').join('&');
  };

最后加上上文中buildFullPath返回的完整的url
产出一个形如http://www?id=2&name=zs的这种完整请求格式

请求发送之后的几种情况

当请求发送出去之后,分成几种情况

  1. 成功 onloaded
  2. 失败或者中断请求onabort,onerror
  3. 超时ontimeout
  4. 如果是 上传/下载,还需要有进度函数progress

成功 onloaded

 function onloadend(){
       // 又一次封装
        var response = {
          data: responseData,
          status: request.status,
          statusText: request.statusText,
          headers: responseHeaders,
          config: config,
          request: request
        };
        
        settle(function _resolve(value) {
          resolve(value);
          done();
        }, function _reject(err) {
          reject(err);
          done();
        }, response);

        // Clean up request
        request = null;
 }

使用settle方法,接收两个函数和一个封装的response,执行函数,执行Promiseresolve方法或者reject方法,和 done方法

看看settle 方法

var settle = function settle(resolve, reject, response){
  var validateStatus = response.config.validateStatus
  
   if( validateStatus(response.status)){
       resolve(response);
   }else {
     reject(new AxiosError_1(
        'Request failed with status code ' + response.status,
        [AxiosError_1.ERR_BAD_REQUEST, AxiosError_1.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
        response.config,
        response.request,
        response
      ))
   }
}

如果 发送的请求返回后的状态码满足validateStatus,就立即调用resolve 方法,xhr方法Promise进入成功态,否则执行reject,进入失败态 validateStatus 比较简单,是从config配置或者使用默认

 validateStatus: function validateStatus(status) {
     return status >= 200 && status < 300;
  },

失败或者中断请求onabort,onerror

都比较简单,只要发送错误信息即可

request.onabort = function handleAbort() {
        if (!request) {
          return;
        }
   reject(new AxiosError_1('Request aborted', AxiosError_1.ECONNABORTED, config, request));
        // Clean up request
        request = null;
      };
      
       request.onerror = function handleError() {
        // Real errors are hidden from us by the browser
        // onerror should only fire if it's a network error
        reject(new AxiosError_1('Network Error', AxiosError_1.ERR_NETWORK, config, request));

        // Clean up request
        request = null;
      };

特别注意,在浏览器中,如果状态码是500也属于发送成功,执行onLoad方法,只有网络请求失败,才执行onError方法

超时ontimeout

request.ontimeout = function handleTimeout(){
  var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';

reject(new AxiosError_1(
          timeoutErrorMessage,
          transitional$1.clarifyTimeoutError ? AxiosError_1.ETIMEDOUT : AxiosError_1.ECONNABORTED,
          config,
          request));

        // Clean up request
        request = null;
}

比较简单,直接reject出错误信息

进度回调progress

      if (typeof config.onDownloadProgress === 'function') {
        request.addEventListener('progress', config.onDownloadProgress);
      }

      // Not all browsers support upload events
      if (typeof config.onUploadProgress === 'function' && request.upload) {
        request.upload.addEventListener('progress', config.onUploadProgress);
      }

如果配置了onDownloadProgress或者有onUploadProgress,就需要对用户报知当前进度
request.upload-MDN

只讨论了发送出去的情况,还有中断请求,也就是 cancel的情况,留到下一次再说

错误处理函数

其中在ontimeoutonerror方法中,多次使用 reject(new AxiosError_1(...)) ,
AxiosError_1 来探究一下

 function AxiosError(message, code, config, request, response) {
   Error.call(this);

   if (Error.captureStackTrace) {
     Error.captureStackTrace(this, this.constructor);
   } else {
     this.stack = (new Error()).stack;
   }

   this.message = message;
   this.name = 'AxiosError';
   code && (this.code = code);
   config && (this.config = config);
   request && (this.request = request);
   response && (this.response = response);
 }

AxiosError 首先把调用 Error.call方法,改变this指向
Error.captureStackTrace(this, this.constructor);
由于Error.captureStackTrace()可以返回调用堆栈信息,因此在自定义Error类的内部经常会使用该函数,用以在error对象上添加合理的stack属性

error 简化版,例如:

 function AxiosError(message) {
   Error.call(this);
    // 只有v8 的浏览器支持这种captureStackTrace
   Error.captureStackTrace(this, this.constructor);
  
   this.message = message;
 }
 
 let r2 =new AxiosError("zzz",500)
 Promise.reject(r2)

如果有 Error.captureStackTrace(this, this.constructor);
image.png

如果没有
image.png

简单的call继承

 function Person (name,age){
  this.name = name || "person";
  this.age = age
}

function Student(age,grade){
     Person.call(this)
     this.age = age;
     this.grade = grade
}

let s = new Student(10,3)
console.log(s) // name:'person',age:10,grade:3

总结

从这个xhr方法中,学到了很多方法和技巧

  1. 判断http是否是绝对路径
  2. request 原生的 上传下载的进度函数
  3. 错误处理
  4. 使用 call 继承Error

time:2022/10/2,还有取消请求尚未完成,axios就要完结

参考: segmentfault.com/a/119000000…