• 一比一还原axios源码(二)—— 请求响应处理


      上一章,我们开发了一些简单的代码,这部分代码最最核心的一个方法就是buildURL,应对了把对象处理成query参数的方方面面。虽然我们现在可以发起简单的请求了,但是第一,我们无法接收到服务器的响应,哦不对,其实在浏览器层面,response已经是接收到了的,只是代码里还拿不到response,因为我们还没写。第二,post的请求还没实现。而处理拿到的response实际上就是处理响应体和响应头。实现post请求,实际上就是实现请求体和请求头。今天我们就来实现这四个点的内容。

      思考题:get请求可以发送body么?大家可以思考下,答案在结尾。不要提前看哦~

    一、请求头和请求体的处理

      处理请求的body,实际上就是XMLHttpRequest的send方法,它可以接收一个body作为参数,这个参数可以是Document、XMLHttpRequestBodyInit或者null。而XMLHttpRequestBodyInit可以是 BlobBufferSource (en-US)FormDataURLSearchParams, 或者 USVString 对象。当然,我们最常用的就是传一个对象的场景,所以我们需要额外的处理一下,给传递的body的对象数据转换成JSON字符串。

      

      上图,是MDN中send方法参数的详细描述。

       那么在axios中的使用方法是这样的:

    复制代码
    // Send a POST request
    axios({
      method: 'post',
      url: '/user/12345',
      data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
      }
    });
    复制代码

      上面的代码就是axios官方文档中的一部分,下面我们来实现它,首先,我们需要一个transformRequest方法,来把post请求中的data参数的对象,转换成JSON字符串,因为这里的data是一个对象,send方法是不接受对象的,所以我们要转换成字符串,也就是文档中的USVString 类型。

    function transformRequest(data) {
      if (utils.isPlainObject(data)) {
        return JSON.stringify(data);
      }
      return data;
    }

      我们直接在xhr.js中实现上面的代码即可,其中isPlainObject是工具类中的一个方法,用来判断是否是一个纯对象,你也知道在js中,typeof 的结果是“object”并不代表是纯对象,也可能是表单对象,日期对象等等,这里我们需要通过isPlainObjecr判断出它是一个纯粹的如我们所知“对象”。

    复制代码
    function isPlainObject(val) {
      if (toString.call(val) !== "[object Object]") {
        return false;
      }
    
      var prototype = Object.getPrototypeOf(val);
      return prototype === null || prototype === Object.prototype;
    }
    复制代码

      isPlainObject的实现如上,个人理解,这里判断条件是“可以拿到原型链上的"[object Object]",又通过原型链与Object的原型同源”才会被命中条件。而isObject方法,就是简单的判断typeof,这些工具方法大家都可以去gitHub上对应的章节分支及其下的文件中找到。以后这些就不再强调和重复了。

      按照上述步骤完成后,我们发现还是传过去的并不是我们想像的那样,这是因为我们还没处理header,默认的request header是text/plain,所以服务端无法处理我们传过去的数据,这时候我们就需要来处理下header。  

    复制代码
    function processHeaders(headers, data) {
      normalizeHeaderName(headers, "Content-Type");
      if (utils.isPlainObject(data)) {
        if (headers && !headers["Content-Type"]) {
          headers["Content-Type"] = "application/json;charset=utf-8";
        }
      }
      return headers;
    }
    复制代码

      上面就是处理header的部分,其实也很简单,normalizeHeaderName方法需要我们在helpers文件夹下在创建一个normalizeHeaderName文件,它的作用就是统一header的名称,你传入小写的,也会转换一下。具体的注释大家可以去自己看这里不多说。上面的代码其实就是如果是一个对象,那么就加上application的类型,很简单。我们直接写在xhr对xhrAdapter中传入的config.header做一下处理即可。

        config.headers = processHeaders(config.headers || {}, config.data);

      最后,如果我们没传data的话,但是又设置了content-type请求头,那么手动去除一下:

    复制代码
    Object.keys(config.headers).forEach((name) => {
          if (config.data === null && name.toLowerCase() === "content-type") {
            delete config.headers[name];
          } else {
            request.setRequestHeader(name, config.headers[name]);
          }
        });
    复制代码

      至此,请求体和请求头就处理完了,我们可以正确的发起携带body的请求了。那么到此我们来简单回顾一下,其实总结起来就一句话:针对普通对象的body传递,转换成json并手动设置正确的请求头

       总结一下,默认的request header 的content-type类型是text/plain,所以,虽然我们转换了body的对象为JSON字符串,但是服务器端是不知道的,所以需要设置request header的content-type为application/json即可让服务器识别。那么我们就可以正常的拿到响应体的内容了。

      那你可能会问了,开头的时候不是说了还有其他类型么?什么表单、arrayBuffer啥的?不用设置头字段么?额。。稍安勿躁,后面见分晓。

    二、响应头和响应体的处理

      上面第一小节,我们已经可以发起带body的请求,并且服务器也能根据request header正确的解析了,下面我们要做的就是来处理返回的数据。我们还是来看最开始的axios官网的例子:

     

      我们看到,结果是返回了一个promise。ok,这就是我们想要的东西~很简单,在xhrAdapter中包一层promise即可:

     

       很简单,对吧~中间的红框request.onreadystatechange绑定的方法,就是我们要核心实现的处理响应体的方法:

    复制代码
    request.onreadystatechange = function handleLoad() {
          if (request.readyState !== 4) {
            return;
          }
    
          const responseHeaders = parseHeaders(request.getAllResponseHeaders());
          const responseData =
            config.responseType && config.responseType !== "text"
              ? request.response
              : request.responseText;
          const response = {
            data: responseData,
            status: request.status,
            statusText: request.statusText,
            headers: responseHeaders,
            config,
            request,
          };
          resolve(response);
        };
    复制代码

      首先onreadystatechange是XMLHttpRequest实例的一个方法,可以监听响应事件,readyState也是XMLHttpRequest实例上的一个属性,它会告诉你响应的状态,这些大家可以去MDN查看,首先我们面临了第一个问题,就是我们通过XMLHttpRequest实例上的getAllResponseHeaders方法获取到的响应头其实是一个以\r\n(回车符和换行符)结尾拼接的字符串,我们需要把它们转换成对象,转换成对象的方法就需要parseHeaders辅助函数来处理了,下面我们在helpers文件夹中创建一个parseHeaders文件:

    复制代码
    "use strict";
    
    import utils from "../utils";
    
    // Headers whose duplicates are ignored by node
    // c.f. https://nodejs.org/api/http.html#http_message_headers
    // 这是我们需要忽略的header
    var ignoreDuplicateOf = [
      "age",
      "authorization",
      "content-length",
      "content-type",
      "etag",
      "expires",
      "from",
      "host",
      "if-modified-since",
      "if-unmodified-since",
      "last-modified",
      "location",
      "max-forwards",
      "proxy-authorization",
      "referer",
      "retry-after",
      "user-agent",
    ];
    
    /**
     * Parse headers into an object
     *
     * ```
     * Date: Wed, 27 Aug 2014 08:58:49 GMT
     * Content-Type: application/json
     * Connection: keep-alive
     * Transfer-Encoding: chunked
     * ```
     *
     * @param {String} headers Headers needing to be parsed
     * @returns {Object} Headers parsed into an object
     */
    export default function parseHeaders(headers) {
      var parsed = {};
      var key;
      var val;
      var i;
      // 没有headers就返回个空对象
      if (!headers) {
        return parsed;
      }
      // 用自定义的forEach方法来遍历分割后的headers
      utils.forEach(headers.split("\n"), function parser(line) {
        // 这时候,line还是个字符串,i就是每一个key、value中间的位置
        i = line.indexOf(":");
        // 分割key,去空格
        key = utils.trim(line.substr(0, i)).toLowerCase();
        // 分割value,去空格
        val = utils.trim(line.substr(i + 1));
    
        if (key) {
          // 如果存在key并且是重复项,则略过
          if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) {
            return;
          }
          // 如果key是set-cookie,那么用数组合并作为值
          if (key === "set-cookie") {
            parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]);
          } else {
            // 否则用逗号分隔已有的值或者直接设置值
            parsed[key] = parsed[key] ? parsed[key] + ", " + val : val;
          }
        }
      });
    
      return parsed;
    }
    复制代码

      其实这个代码也并不复杂,大家看注释说明就好。写好了转换代码,直接在onreadystatechange中转换一下即可。然后,还有一个需要额外处理的东西,就是responseType参数:

          const responseData =
            config.responseType && config.responseType !== "text"
              ? request.response
              : request.responseText;

      responseType的不同,会影响到返回的响应体的类型。最后,我们把这些转换后的数据,resolve出去即可:

    复制代码
    const response = {
            data: responseData,
            status: request.status,
            statusText: request.statusText,
            headers: responseHeaders,
            config,
            request,
          };
          resolve(response);
    复制代码

      这部分resolve的内容,实际上就是axios定义的需要返回的内容:

       一模一样,对嘛~

      OK,到此为止我们完成了完整的请求响应过程。并且处理其中的部分参数以及加入了promise。目前,我们所做的事情,完成了整个axios请求最核心的主线,那么我们来总结下到现在为止,我们都做了axios中的哪些事情:

      实现的axios API如下:

    复制代码
    axios({
       method:"post",
       url:"xxx",
       params:{},
       data:{},
       responseType:"",
       headers:{},     
    }).then((res) => {
       console.log(res) 
    })
    复制代码

      还有res中的response的数据。前面说过了,再有就是promise。那么在实际的代码中呢,我们实现了发起ajax请求的一条主线,也就是从请求发起,到响应返回的过程,并且在过程中,由于json的特殊性,对此还进行了相应头字段和body的转换,再有一个实用的buildURL方法,来处理对象到query字符串的转换。哦对,再有就是我们获取到的response header是字符串,我们还要分割下,把字符串转换成一个对象的parseHeader方法。

      以上,buildURL都是可以拿到实际的项目中去使用的,我就复制到了我们项目里,爽得一批(好吧,原谅我头发不长,见识也不长)。

      好了。。。。大家注意没,上面的代码没有.catch,是的,错误处理还没写,下一章我们就来写错误处理的相关代码。

     

    答案:

      从技术层面上讲,get是可以传body的,但是在客户端,浏览器的层面,不允许get传body,所有的get中的body都视为null。但是在服务器端的http请求中,get是可以传递body的。

      另外一个思考题:get和post请求有啥区别?   

  • 相关阅读:
    required a bean of type …… that could not be found.
    java mysql ssm框架的同城配送系统源码
    华为Atlas 300I 推理卡显卡安装
    简单解读react的setState
    RocketMQ 消息传递模型
    【Python】万字长文,Locust 性能测试指北
    npm 执行命令时报错npm ERR! code ERESOLVE npm ERR! ERESOLVE could not resolve
    第六章 配置命名空间(一)
    全连接层是什么,有什么作用?
    Avalonia踩坑
  • 原文地址:https://www.cnblogs.com/zaking/p/15887450.html