从零实现axios(1.2小节-创建xhr请求)

114 阅读4分钟

创建 xhr 请求

我们在根目录下创建adapters目录,在该目录下创建xhr.js文件,文件内容如下面代码所示。

xhrAdapter函数里面的核心就是通过new XMLHttpRequest()构造出一个请求对象,通过调用opensend方法发送一个 http 请求,同时通过监听readystatechange事件来处理请求的响应,例如readyState不等于 4,表示请求还没完成。我们通过parseHeaders函数处理响应头,通过settle返回响应数据,我们接下来实现这两个函数, xhrAdapter函数更多的细节我在代码里一一添加了注释,这里就不重复了

var settle = require("../core/settle");
var parseHeaders = require("../helpers/parseHeaders");

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // 请求数据
    var requestData = config.data;

    // 该方法暂时不实现
    function done() {}

    // 构造一个xhr对象
    var request = new XMLHttpRequest();

    // 目前先直接拿到url路径, 我们后面会对这个url进行处理
    // 暂时不用理会这些处理细节
    var fullPath = config.url;

    // xhrReq.open(method, url, async)
    // method: HTTP方法,比如GET、POST
    // url: 请求的URL
    // async: 表示是否异步执行操作,默认为true
    // 之后同样会对这个url进行处理,现在暂时不用理会
    request.open(config.method.toUpperCase(), fullPath, true);

    function onloadend() {
      if (!request) {
        return;
      }

      //  对响应头进行处理
      var responseHeaders =
        "getAllResponseHeaders" in request
          ? parseHeaders(request.getAllResponseHeaders())
          : null;
      // 如果config参数没有responseType,则默认为text类型
      var responseData =
        !config.responseType || config.responseType === "text" || responseType === 'json'
          ? request.responseText
          : request.response;
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request,
      };

      // 我们在settle返回响应数据
      settle(function _resolve(value) {
        resolve(value);
        done();
      }, function _reject(err) {
        reject(err);
        done();
      }, response);

      // 清除请求
      request = null;
    }

    // 监听 ready state
    request.onreadystatechange = function handleLoad() {
      // readyState不等于4,表示请求还没完成,所以不做任何处理
      if (!request || request.readyState !== 4) {
        return;
      }

      // 请求出错,我们没有得到回复,浏览器返回的 status 为0,这将由onerror处理。
      // 有一个例外:就是请求使用file:协议,即使请求成功,也会将状态返回为0
      // 因此,只有在不使用file:协议,并且status===0的情况下,我们直接返回
      if (
        request.status === 0 &&
        !(request.responseURL && request.responseURL.indexOf("file:") === 0)
      ) {
        return;
      }

      setTimeout(onloadend)
    };

    if (!requestData) {
      requestData = null;
    }

    request.send(requestData);
  });
};

我们在parseHeaders方法中处理响应头,我们实现该方法,先在根目录下创建helpers文件夹,在文件夹里创建parseHeaders.js文件 该方法的功能非常简单,就是把响应头的键值对保存到一个对象中去,因为响应头以换行符连接在一起,我们通过split('\n')方法就能拿到一个个响应头,由于单个响应头的格式是以:分割,如content-length: 1024:前面部分为响应头的 key,后面部分为值

"use strict";

var utils = require("../utils");

// 忽略以下的请求头
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",
];

/**
 * 解释一个请求头到对象中
 *
 * ```
 * Date: Wed, 27 Aug 2014 08:58:49 GMT
 * Content-Type: application/json
 * Connection: keep-alive
 * Transfer-Encoding: chunked
 * ```
 *
 * @param {String} headers 要解释的请求头
 * @returns {Object} 返回解释后的对象
 */
module.exports = function parseHeaders(headers) {
  var parsed = {};
  var key;
  var val;
  var i;

  if (!headers) {
    return parsed;
  }

  utils.forEach(headers.split("\n"), function parser(line) {
    i = line.indexOf(":");
    // 请求头的键
    key = utils.trim(line.substr(0, i)).toLowerCase();
    // 请求头的值
    val = utils.trim(line.substr(i + 1));

    if (key) {
      if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) {
        return;
      }
      // cookie要合并起来
      if (key === "set-cookie") {
        parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]);
      } else {
        parsed[key] = parsed[key] ? parsed[key] + ", " + val : val;
      }
    }
  });

  return parsed;
};

我们在根目录下创建utils.js文件,该文件里实现一些辅助函数,我们先实现forEachtrim辅助函数

"use strict";

// 判断值是不是一个数组
function isArray(val) {
  return Array.isArray(val);
}

/**
 * 处理字符串开始和结尾的空格
 *
 * @param {String} str 要处理的字符串
 * @returns {String} 首尾没有多余空格的字符串
 */
function trim(str) {
  // \uFEFF:字节序标记(Byte Order Mark)
  // \xA0:禁止换行空白符
  return str.trim ? str.trim() : str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
}

/**
 * @param {Object|Array} obj 要迭代的对象
 * @param {Function} fn 每个item要调用的回调函数
 */
function forEach(obj, fn) {
  // 没有值直接返回
  if (obj === null || typeof obj === "undefined") {
    return;
  }

  // 不是对象类型强转为数组
  if (typeof obj !== "object") {
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }

  if (isArray(obj)) {
    // 迭代数组的值
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // 如果值是一个对象类型,则对键值对进行迭代处理
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

module.exports = {
  isArray,
  forEach: forEach,
  trim: trim,
};

最后我们要在core文件夹下创建settle.js文件,具体代码如下

module.exports = function settle(resolve, reject, response) {
  // 我们将在第二章讲解config的各种默认属性,我们到时候再实现validateStatus方法,
  // 这里暂不实现该方法,即暂时不对响应状态码进行校验
  var validateStatus = response.config.validateStatus;
  // 如果状态码存在,并且存在验证状态码方法,则直接用validateStatus方法验证状态码
  // 如果状态码存在,验证状态码的方法不存在,则直接resolve
  // 如果response.status = 0,表明file:协议请求成功,也直接resolve
  // 验证不通过,则reject
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    // 错误处理这块,我们之后会专门讲到
    // 现在暂时返回一个空{}
    reject({});
  }
};

我们在下一节创建一个axios实例