axios 源码精读

前言

阅读框架源码的好处在于提升编程水平,以及了解框架的设计思想,配合其官方文档,让我们对它的使用变得更加得心应手。

axios 是一个小而精的框架,不像 vue 那般庞大而复杂到让人望而生畏,阅读也起来相对容易。在阅读源码前,建议掌握 call、apply、bind,Promise,闭包,this 指向,原型链等知识,以及一些设计模式。

axios 版本:0.18.1

1.目录结构

axios 的源码在 lib 目录,其结构如下
在这里插入图片描述

在正式介绍源码前,先回顾一下 axios 的常见的使用姿势,以便更好的理解源码。

2.axios 的使用姿势

看源码前必须熟练使用 axios,以下就列举了 axios 的使用方式以及如何封装 axios。

使用方式

方式一:axios(config)

axios({
    
    
  method: "post",
  url: "/user/12345",
  data: {
    
    
    firstName: "Fred",
    lastName: "Flintstone",
  },
});

方式二:axios(url[, config])

axios("/user/12345", {
    
    
  /* config */
});

方式三:axios[method](url[, config])

axios.get("/user/12345", {
    
    
  /* config */
});

这种方式针对 get、delete、head、options 方法

方式四:axios[method](url[, data[, config]])

axios.post(
  "/user/12345",
  {
    
    
    firstName: "Fred",
    lastName: "Flintstone",
  },
  {
    
    
    /* config */
  }
);

这种方式针对 post、put、patch 方法

方式五:axios.request(config)

axios.request({
    
    
  method: "post",
  url: "/user/12345",
  data: {
    
    
    firstName: "Fred",
    lastName: "Flintstone",
  },
});

是否会好奇 axios 如何实现支持这五种方式的?后续将会解答。

封装 axios

有时我们更倾向于创建一个 Axios 实例,然后设置拦截器。

import axios from "axios";
import {
    
     BASE_URL } from "./http";

// create an axios instance
const service = axios.create({
    
    
  baseURL: BASE_URL, // url = base url + request url
  withCredentials: true, // send cookies when cross-domain requests(是否支持跨域)
  timeout: 5000, // request timeout(超时时间)
});

// request interceptor(请求拦截器)
service.interceptors.request.use(
  (config) => {
    
    
    // do something before request is sent
    return config;
  },
  (error) => {
    
    
    // do something with request error
    // console.log(error) // for debug
    return Promise.reject(error);
  }
);

// response interceptor(响应拦截器)
service.interceptors.response.use(
  (response) => {
    
    
    const res = response.data;
    return res;
  },
  (error) => {
    
    
    if (error.response) {
    
    
      // console.log('err' + error) // for debug
      switch (error.response.status) {
    
    
        // 不同状态码下执行不同操作
        case 401:
          break;
        case 404:
          break;
        case 500:
          break;
        default:
      }
    }
    return Promise.reject(error);
  }
);

export default service;

拦截器是 axios 最核心的一个功能,通过阅读源码将了解其拦截器是如何实现的。

3.从 axios.js 开始

axios.js 中主要包含以下部分:

  • 第一部分:bind、utils 工具方法;
  • 第二部分:axios、axios.Axios、axios.create;
  • 第三部分:取消相关的方法(Cancel、CancelToken、isCancel)、all、spread 处理并发请求的助手函数(这里暂不介绍)。

这里着重介绍第二部分的实现。

axios.js 源码如下:

"use strict";

var utils = require("./utils");
var bind = require("./helpers/bind");
var Axios = require("./core/Axios");
var defaults = require("./defaults");

/**
 * 创建一个 Axios 实例
 *
 * @param {Object} defaultConfig 实例的配置
 * @return {Axios} 一个 Axios 实例
 */
function createInstance(defaultConfig) {
    
    
  // 创建一个 Axios 实例
  var context = new Axios(defaultConfig);

  // bind 返回一个函数实例(这里是 wrap 方法)
  // 修改 Axios.prototype.request 的 this 指向为 context
  // 执行 axios(config) 相当于执行 Axios.prototype.request(config)
  var instance = bind(Axios.prototype.request, context);

  // 把 Axios.prototype 的所有属性/方法都复制到 instance 中, context 作为 bind 的作用域
  // Axios.prototype 上有 request, get, post 等 8 个方法
  utils.extend(instance, Axios.prototype, context);

  // 把 context 的所有属性都复制到 instance 中
  // context 包含 defaults 默认配置 和 interceptors 拦截器
  utils.extend(instance, context);

  return instance;
}

// 创建要导出的默认实例
var axios = createInstance(defaults);

// 暴露 Axios 类以允许类继承
axios.Axios = Axios;

// 暴露创建 Axios 实例的工厂方法 【工厂模式】
axios.create = function create(instanceConfig) {
    
    
  // merge: 将用户传入的配置与默认配置进行合并
  return createInstance(utils.merge(defaults, instanceConfig));
};

// 暴露取消 request 相关的方法
axios.Cancel = require("./cancel/Cancel");
axios.CancelToken = require("./cancel/CancelToken");
axios.isCancel = require("./cancel/isCancel");

// 暴露 all/spread (处理并发请求的助手函数)
axios.all = function all(promises) {
    
    
  return Promise.all(promises);
};
axios.spread = require("./helpers/spread");

module.exports = axios;
// 允许在 TypeScript 中使用默认导入语法
module.exports.default = axios;

注:代码中的 bind、utils 后面有介绍。

从这个文件中可以看到其对外暴露了一个 Axios 实例 axios。axios 是通过 createInstance() 方法创建的。

这里不妨打印一下 axios(直接在 VScode 中执行当前文件),其结果如下:

function wrap() {
    
    
  var args = new Array(arguments.length);
  for (var i = 0; i < args.length; i++) {
    
    
    args[i] = arguments[i];
  }
  return fn.apply(thisArg, args);
}

可以看到,axios 其实就是一个 wrap() 方法,结合上面的源码知道,fn 为 Axios.prototype.request,thisArg 为 context(Axios 实例)。所以执行 axios(config) 就相当于执行 Axios.prototype.request(config)。

如果好奇 context 长什么模样也可打印一下,其结果如下:

Axios {
    
    
  defaults: {
    
    
    adapter: [Function: httpAdapter],
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    validateStatus: [Function: validateStatus],
    headers: {
    
    
      common: [Object],
      delete: {
    
    },
      get: {
    
    },
      head: {
    
    },
      post: [Object],
      put: [Object],
      patch: [Object]
    }
  },
  interceptors: {
    
    
    request: InterceptorManager {
    
     handlers: [] },
    response: InterceptorManager {
    
     handlers: [] }
  }
}

注:这里的方括号‘[]’并不一定是数组,比如 adapter: [Function: httpAdapter]

可以看到,打印的是一个 Axios 类的实例对象,包含 defaults(默认配置)和 interceptors(拦截器),至于具体细节放在后面再介绍。

再看 utils.extend(instance, Axios.prototype, context); 代码。

这步操作是在将 Axios.prototype 的所有属性/方法都复制到 instance 中, context 作为 bind 时的作用域(见 utils.entend 内部实现)。这里的 instance 其实就是 axios;context 上面已经打印了,是 Axios 的默认实例。Axios.prototype 又包含啥呢?打印结果如下:

Axios {
    
    
  request: [Function: request],
  delete: [Function],
  get: [Function],
  head: [Function],
  options: [Function],
  post: [Function],
  put: [Function],
  patch: [Function]
}

所以经过 utils.extend 后 instance 就并不是一个简单的 wrap 方法,其上还包括了 Axios.prototype 中的 8 个方法。因此,这就解释了为什么可以通过 axios.get 等方式(文章开头时 axios 的使用方式 三、四、五)进行发送请求。

最后 utils.extend(instance, context);

这里将 context 中的 defaults、interceptors(拦截器)扩展到了 instance 中。所以,在回头看看在 封装 axios 中为什么能通过 service.interceptors 设置拦截器就不感到奇怪了吧。

看到这儿,你如果感觉有些晕,说明你还对 axios 整体结构还不熟悉,不妨继续往下,之后再回头看一遍。

4.第一部分:bind、utils 工具方法

bind 方法

"use strict";
// 手写实现一个 bind 方法,兼容低版本浏览器
module.exports = function bind(fn, thisArg) {
    
    
  return function wrap() {
    
    
    // 将 arguments 转为数组
    // 注: 这里是否多余?apply 支持 arguments 类数组对象
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
    
    
      args[i] = arguments[i];
    }
    // 执行 fn 并传入作用域和参数
    return fn.apply(thisArg, args);
  };
};

utils 中的方法有很多,这里只介绍下面三个。

utils.forEach

/**
 * forEach 方法的实现。并支持对象的遍历。【迭代器模式】
 * 遍历数组和对象,为每一项调用一个函数 fn
 *
 * @param {Object|Array} obj 迭代的对象
 * @param {Function} fn 为每一项执行的回调函数
 */
function forEach(obj, fn) {
    
    
  // 不是一个有效值时,退出
  if (obj === null || typeof obj === "undefined") {
    
    
    return;
  }

  // 不是一个可迭代的值时
  if (typeof obj !== "object") {
    
    
    obj = [obj];
  }

  if (isArray(obj)) {
    
    
    // 遍历并覆盖数组值
    for (var i = 0, l = obj.length; i < l; i++) {
    
    
      fn.call(null, obj[i], i, obj);
    }
  } else {
    
    
    // 遍历并覆盖对象值
    // for in 会遍历原型上的属性, 这里用 hasOwnProperty() 过滤掉原型上的属性
    for (var key in obj) {
    
    
      // 判断键值是否存在
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
    
    
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

utils.merge

/**
 * 对象属性进行合并,后面对象属性优先级高于前面对象属性
 *
 * 例如:
 * ```js
 * var result = merge({foo: 123}, {foo: 456});
 * console.log(result.foo); // 输出 456
 * ```
 *
 * @param {Object} obj1 合并对象
 * @returns {Object} 所有合并属性的结果
 */
function merge(/* obj1, obj2, obj3, ... */) {
    
    
  var result = {
    
    };
  function assignValue(val, key) {
    
    
    if (typeof result[key] === "object" && typeof val === "object") {
    
    
      // 若是对象类型的值,则继续递归
      result[key] = merge(result[key], val);
    } else {
    
    
      result[key] = val;
    }
  }

  // 遍历参数
  for (var i = 0, l = arguments.length; i < l; i++) {
    
    
    forEach(arguments[i], assignValue);
  }
  return result;
}

utils.extend

/**
 * 将 b 的属性复制到 a 上,属性名相同则覆盖
 *
 * @param {Object} a 要扩展的对象
 * @param {Object} b 要复制属性的对象
 * @param {Object} thisArg 要绑定功能的对象
 * @return {Object} 对象 a 的结果值
 */
function extend(a, b, thisArg) {
    
    
  forEach(b, function assignValue(val, key) {
    
    
    if (thisArg && typeof val === "function") {
    
    
      // 是函数
      a[key] = bind(val, thisArg);
    } else {
    
    
      // 不是函数
      a[key] = val;
    }
  });
  return a;
}

5.第二部分:axios、axios.Axios、axios.create

axios、axios.create 上面已经介绍了,下面主要介绍 Axios 类的实现。

Axios.js

这个文件声明了一个 Axios 构造器,然后在其原型上添加了 request、delete、get、head、options、post、put、patch 8 个方法。

"use strict";

var defaults = require("./../defaults");
var utils = require("./../utils");
var InterceptorManager = require("./InterceptorManager");
var dispatchRequest = require("./dispatchRequest");

/**
 * Axios 构造器
 *
 * @param {Object} instanceConfig 实例的配置参数
 */
function Axios(instanceConfig) {
    
    
  // 设置请求配置
  this.defaults = instanceConfig;
  // 为请求和响应添加 interceptors (拦截器)
  this.interceptors = {
    
    
    request: new InterceptorManager(),
    response: new InterceptorManager(),
  };
}

/**
 * 发送请求
 *
 * @param {Object} config 用户传入的配置 (将其合并到默认配置 defaults 上)
 */
Axios.prototype.request = function request(config) {
    
    
  // 如果 config 是一个字符串,则将其当作 url
  if (typeof config === "string") {
    
    
    config = utils.merge(
      {
    
    
        url: arguments[0],
      },
      arguments[1]
    );
  }

  // 将配置进行合并
  // defaults: Axios 默认配置
  // {method: 'get'}: 默认是 get 方法
  // this.defaults: Axios 构造器中的配置属性
  // config: 用户传入的配置
  config = utils.merge(defaults, {
    
     method: "get" }, this.defaults, config);
  config.method = config.method.toLowerCase(); // 方法名转小写

  // 连接拦截器中间件
  // undefined: 后面需要两两取出, 用于占位
  var chain = [dispatchRequest, undefined];
  // 将 config 转换为 promise, 同时后续 config 将作为参数进行传递
  var promise = Promise.resolve(config);

  // 将 request 拦截器逐一插入到 链表的头部
  // 注:这里的 forEach 是 InterceptorManager.prototype.forEach
  this.interceptors.request.forEach(function unshiftRequestInterceptors(
    interceptor
  ) {
    
    
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // 将 response 拦截器逐一插入到 链表的尾部
  this.interceptors.response.forEach(function pushResponseInterceptors(
    interceptor
  ) {
    
    
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // 通过上面的步骤, 我们就形成了如下链表:
  // request 拦截器(interceptors.request) + 发起请求的方法(dispatchRequest) + response 拦截器(interceptors.response)

  while (chain.length) {
    
    
    // 从链表中从头连续取出2个元素,第一个作为 promise 的 resolve handler, 第二个作为 reject handler
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

// 提供支持的请求方法的别名
// 注意这四个方法都不需要 请求负载(request payload)
utils.forEach(
  ["delete", "get", "head", "options"],
  function forEachMethodNoData(method) {
    
    
    Axios.prototype[method] = function (url, config) {
    
    
      // this.request: 调用 Axios.prototype.request
      // 将参数合并为一个, config 非必填
      return this.request(
        utils.merge(config || {
    
    }, {
    
    
          method: method,
          url: url,
        })
      );
    };
  }
);

// 注意这四个方法都有 请求负载(request payload)
utils.forEach(["post", "put", "patch"], function forEachMethodWithData(method) {
    
    
  Axios.prototype[method] = function (url, data, config) {
    
    
    // this.request: 调用 Axios.prototype.request
    // 将参数合并为一个, config 非必填
    return this.request(
      utils.merge(config || {
    
    }, {
    
    
        method: method,
        url: url,
        data: data,
      })
    );
  };
});

module.exports = Axios;

注:请求负载(request payload),就是传递的 data 数据。

通过上面的代码可以发现,不管什么方法,其最后都是执行 request 方法。

request 方法接收一个 config 参数,若 config 是字符串,则将 arguments[0] 当作 url,arguments[1] 当作其他配置参数。所以,这就是为什么即能够通过 axios(config) 传参方式也能够通过 axios(url[, config]) 的传参方式进行访问的原因。当然,还有一种传参方式 axios[method](url[, data[, config]])(只支持 post、put、patch),通过其 post、put、patch 的实现就能看明白原因。

接着往下,开始合并 config,然后将 config.method 转换为小写。

然后,最重要的部分到了。定义一个链表 chain,然后通过 Promise.resolve() 将 config 转换为 promise,再将请求与响应拦截器添加到链表头尾。最终,返回的是一个 promise。

由于这里涉及拦截器,所以先看看 InterceptorManager.js。

InterceptorManager.js

InterceptorManager.js 中声明了一个 InterceptorManager 拦截器管理类,然后在其原型上添加了 use、eject、forEach 方法。

"use strict";

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

/**
 * 声明一个拦截器管理的类
 */
function InterceptorManager() {
    
    
  // 用一个数组存放所有拦截器
  this.handlers = [];
}

/**
 * 添加一个拦截器
 *
 * @param {Function} fulfilled 成功的回调
 * @param {Function} rejected 失败的回调
 * @return {Number} 返回添加的拦截器的下标, 用于以后删除
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
    
    
  this.handlers.push({
    
    
    fulfilled: fulfilled,
    rejected: rejected,
  });
  return this.handlers.length - 1;
};

/**
 * 删除一个拦截器
 *
 * @param {Number} id 通过 use 方法返回的 id
 */
InterceptorManager.prototype.eject = function eject(id) {
    
    
  if (this.handlers[id]) {
    
    
    this.handlers[id] = null;
  }
};

/**
 * 遍历所有非 null 的 handler, 并调用
 *
 * @param {Function} fn 调用每个拦截器的函数
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
    
    
  utils.forEach(this.handlers, function forEachHandler(h) {
    
    
    // 这里的 h 就是 handlers 中的值
    if (h !== null) {
    
    
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

结合最开始的 封装 axios 部分代码,简写如下示例代码:

// -示例代码-
// 创建一个 axios 实例
const service = axios.create({
    
    
  baseURL: "baseurl",
  withCredentials: true,
  timeout: 5000,
});
// 添加请求拦截器
service.interceptors.request.use(
  (config) => config, // fn1
  (error) => Promise.reject(error) // fn2
);
// 添加响应拦截器
service.interceptors.response.use(
  (response) => response.data, // fn3
  (error) => Promise.reject(error) // fn4
);
// 发送请求
service({
    
    
  url: "/api",
  method: "POST",
  data: {
    
    
    name: "Bob",
  },
})
  .then((res) => {
    
    
    // 拿到结果
  })
  .catch((err) => {
    
    
    // 错误信息
  });

结合上面的示例代码、InterceptorManager.js 、Axios.js 就更容易理解拦截器的实现与运行流程。

这里执行 service(config) 就是在执行 Axios.prototype.request(config),最终 chain 链表如下:

[
  [Function], // fn1
  [Function], // fn2
  [(Function: dispatchRequest)],
  undefined,
  [Function], // fn3
  [Function], // fn4
];

其执行过程为:fn1,fn2(request 拦截器) => dispatchRequest(发起请求的方法) => fn3,fn4(response 拦截器)。

接下来就开始查看 dispatchRequest 的实现了。

dispatchRequest.js

"use strict";

var utils = require("./../utils");
var transformData = require("./transformData");
var isCancel = require("../cancel/isCancel");
var defaults = require("../defaults");
var isAbsoluteURL = require("./../helpers/isAbsoluteURL");
var combineURLs = require("./../helpers/combineURLs");

/**
 * 如果请求已取消,则抛出“取消”。
 */
function throwIfCancellationRequested(config) {
    
    
  if (config.cancelToken) {
    
    
    config.cancelToken.throwIfRequested();
  }
}

/**
 * 使用配置的适配器向服务器发送请求。
 *
 * @param {object} config 用于请求的配置
 * @returns {Promise} 返回 fulfilled(已成功) 状态的 Promise
 */
module.exports = function dispatchRequest(config) {
    
    
  throwIfCancellationRequested(config);

  // 如果配置了 baseURL, 且 config.url 也不是绝对路径, 则拼接
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    
    
    config.url = combineURLs(config.baseURL, config.url);
  }

  // 确保 headers 存在
  config.headers = config.headers || {
    
    };

  // 对即将发起的请求的 数据 和 header 做预处理
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // 合并不同配置的 header
  config.headers = utils.merge(
    config.headers.common || {
    
    },
    config.headers[config.method] || {
    
    },
    config.headers || {
    
    }
  );

  // 删除 headers 中的 method
  utils.forEach(
    ["delete", "get", "head", "post", "put", "patch", "common"],
    function cleanHeaderConfig(method) {
    
    
      delete config.headers[method];
    }
  );

  // 适配器
  var adapter = config.adapter || defaults.adapter;

  // 使用适配器发起请求
  return adapter(config).then(
    function onAdapterResolution(response) {
    
    
      // 请求成功

      throwIfCancellationRequested(config);

      // 转换响应数据
      response.data = transformData(
        response.data,
        response.headers,
        config.transformResponse
      );

      // 返回响应数据
      return response;
    },
    function onAdapterRejection(reason) {
    
    
      // 请求失败
      if (!isCancel(reason)) {
    
    
        throwIfCancellationRequested(config);

        // 如果请求错误且有数据
        // 转换响应数据
        if (reason && reason.response) {
    
    
          reason.response.data = transformData(
            reason.response.data,
            reason.response.headers,
            config.transformResponse
          );
        }
      }

      // 返回错误信息,通过 catch 捕获
      return Promise.reject(reason);
    }
  );
};

dispatchRequest(config) 返回的是 adapter(config) 执行的结果

接着需要看 adapter 的实现。这里的 adapter 一般是从 defaults 中获取的,所以先看看 defaults。

defaults.js

"use strict";

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

// 默认 Content-Type
var DEFAULT_CONTENT_TYPE = {
    
    
  "Content-Type": "application/x-www-form-urlencoded",
};

// 如果 headers 中 Content-Type 不存在,则设置其值
function setContentTypeIfUnset(headers, value) {
    
    
  if (
    !utils.isUndefined(headers) &&
    utils.isUndefined(headers["Content-Type"])
  ) {
    
    
    headers["Content-Type"] = value;
  }
}

// 不同环境下,获取默认的适配器
function getDefaultAdapter() {
    
    
  var adapter;
  if (typeof XMLHttpRequest !== "undefined") {
    
    
    // 浏览器环境使用 XHR
    adapter = require("./adapters/xhr");
  } else if (typeof process !== "undefined") {
    
    
    // node 环境使用 HTTP
    adapter = require("./adapters/http");
  }
  return adapter;
}

var defaults = {
    
    
  // 默认请求方式
  adapter: getDefaultAdapter(),

  // 格式化 requestData
  transformRequest: [
    function transformRequest(data, headers) {
    
    
      // 将 headers 中不标准的属性名,格式化为 Content-Type 属性名
      normalizeHeaderName(headers, "Content-Type");

      // 如果是以下几种格式, 则直接返回
      if (
        utils.isFormData(data) ||
        utils.isArrayBuffer(data) ||
        utils.isBuffer(data) ||
        utils.isStream(data) ||
        utils.isFile(data) ||
        utils.isBlob(data)
      ) {
    
    
        return data;
      }
      if (utils.isArrayBufferView(data)) {
    
    
        return data.buffer;
      }
      // 如果是 URLSearchParams 对象, 设置 Content-Type
      if (utils.isURLSearchParams(data)) {
    
    
        setContentTypeIfUnset(
          headers,
          "application/x-www-form-urlencoded;charset=utf-8"
        );
        return data.toString();
      }
      // 如果是 对象, 设置 Content-Type
      if (utils.isObject(data)) {
    
    
        setContentTypeIfUnset(headers, "application/json;charset=utf-8");
        return JSON.stringify(data);
      }
      return data;
    },
  ],

  // 格式化 responseData, 将其解析为对象
  transformResponse: [
    function transformResponse(data) {
    
    
      if (typeof data === "string") {
    
    
        try {
    
    
          data = JSON.parse(data);
        } catch (e) {
    
    
          /* Ignore */
        } // 这里并未对 JSON.parse() 的错误做处理
      }
      return data;
    },
  ],

  // 设置请求超时时间, 默认为 0. 超时将终止请求, 0 意味着没有超时
  timeout: 0,

  // CSRF/XSRF (跨站请求伪造)
  // `xsrfCookieName` 是要用作 xsrf 令牌的值的 cookie 的名称
  // `xsrfHeaderName` 是携带 xsrf 令牌值的 http 头的名称
  xsrfCookieName: "XSRF-TOKEN",
  xsrfHeaderName: "X-XSRF-TOKEN",

  // 定义允许的响应内容的最大尺寸
  maxContentLength: -1,

  // 验证请求状态
  // `validateStatus` 定义对于给定的 HTTP 响应状态码是 resolve 或 reject 的 promise 。
  // 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
  // promise 将被 resolve; 否则, promise 将被 reject
  validateStatus: function validateStatus(status) {
    
    
    return status >= 200 && status < 300;
  },
};

defaults.headers = {
    
    
  // 通用的 HTTP 字段
  common: {
    
    
    Accept: "application/json, text/plain, */*",
  },
};

// 添加 请求方法
utils.forEach(["delete", "get", "head"], function forEachMethodNoData(method) {
    
    
  defaults.headers[method] = {
    
    }; // 没有 Content-Type
});

// 设置默认的 Content-Type
utils.forEach(["post", "put", "patch"], function forEachMethodWithData(method) {
    
    
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

module.exports = defaults;

defaults 中包含的内容整理如下图:

在这里插入图片描述

adapter 这里只介绍 xhr。

xhr.js

"use strict";

var utils = require("./../utils");
var settle = require("./../core/settle");
var buildURL = require("./../helpers/buildURL");
var parseHeaders = require("./../helpers/parseHeaders");
var isURLSameOrigin = require("./../helpers/isURLSameOrigin");
var createError = require("../core/createError");

// 浏览器环境下使用的方法
module.exports = function xhrAdapter(config) {
    
    
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    
    
    var requestData = config.data;
    var requestHeaders = config.headers;

    // 如果是 FormData 对象, 删除 headers 中的 Content-Type 字段, 让浏览器自动生成
    if (utils.isFormData(requestData)) {
    
    
      delete requestHeaders["Content-Type"];
    }

    // 创建一个 xhr 对象
    // 参考: https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
    var request = new XMLHttpRequest();

    // 配置 HTTP 请求头的 Authentication [HTTP身份验证]
    // 参考: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication
    if (config.auth) {
    
    
      var username = config.auth.username || "";
      var password = config.auth.password || "";
      // 使用 btoa 创建一个 base-64 编码的字符串
      requestHeaders.Authorization = "Basic " + btoa(username + ":" + password);
    }

    // open(请求方法, 请求URL, 是否支持异步): 初始化一个请求
    request.open(
      config.method.toUpperCase(),
      buildURL(config.url, config.params, config.paramsSerializer),
      true
    );

    // 请求的最大请求时间, 超出则终止请求
    request.timeout = config.timeout;

    // 监听 readyState 属性变化, 其值为 4 时表示请求成功
    request.onreadystatechange = function handleLoad() {
    
    
      if (!request || request.readyState !== 4) {
    
    
        return;
      }

      // 在请求完成前, status 的值为 0。如果 XMLHttpRequest 出错, 浏览器返回的 status 也为 0。
      // file 协议除外, status 为 0 时也是一个成功的请求
      // responseURL: 返回经过序列化(serialized)的响应 URL
      if (
        request.status === 0 &&
        !(request.responseURL && request.responseURL.indexOf("file:") === 0)
      ) {
    
    
        return;
      }

      // 准备响应
      // getAllResponseHeaders 返回所有响应头
      // responseText 返回的文本数据
      var responseHeaders =
        "getAllResponseHeaders" in request
          ? parseHeaders(request.getAllResponseHeaders())
          : null;
      var responseData =
        !config.responseType || config.responseType === "text"
          ? request.responseText
          : request.response;
      var response = {
    
    
        data: responseData, // 响应正文
        status: request.status, // 响应状态
        statusText: request.statusText, // 响应状态的文本信息
        headers: responseHeaders, // 响应头
        config: config,
        request: request,
      };

      // status >= 200 && status < 300 resolve
      // 否则 reject
      settle(resolve, reject, response);

      // 清除 request
      request = null;
    };

    // 请求出错
    request.onerror = function handleError() {
    
    
      // 抛出网络错误
      reject(createError("Network Error", config, null, request));

      // 清除 request
      request = null;
    };

    // 请求超时
    request.ontimeout = function handleTimeout() {
    
    
      // 抛出超时错误
      reject(
        createError(
          "timeout of " + config.timeout + "ms exceeded",
          config,
          "ECONNABORTED",
          request
        )
      );

      // 清除 request
      request = null;
    };

    // 增加 xsrf header
    // 仅在标准浏览器环境中运行时才能执行此操作。
    // xsrf header 是用来防御 CSRF 攻击
    if (utils.isStandardBrowserEnv()) {
    
    
      var cookies = require("./../helpers/cookies");

      // 增加 xsrf header
      var xsrfValue =
        (config.withCredentials || isURLSameOrigin(config.url)) &&
        config.xsrfCookieName
          ? cookies.read(config.xsrfCookieName)
          : undefined;

      if (xsrfValue) {
    
    
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }

    // 将 config 中配置的 requestHeaders, 循环设置到请求头上
    if ("setRequestHeader" in request) {
    
    
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
    
    
        if (
          typeof requestData === "undefined" &&
          key.toLowerCase() === "content-type"
        ) {
    
    
          // 如果 data 未定义, 移除 Content-Type
          delete requestHeaders[key];
        } else {
    
    
          // 设置 request header
          request.setRequestHeader(key, val);
        }
      });
    }

    // `withCredentials` 表示跨域请求时是否需要使用凭证
    if (config.withCredentials) {
    
    
      request.withCredentials = true;
    }

    // `responseType` 表示服务器响应的数据类型,
    // 可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
    if (config.responseType) {
    
    
      try {
    
    
        request.responseType = config.responseType;
      } catch (e) {
    
    
        // DOMException 抛出浏览器不兼容 XMLHttpRequest Level 2.
        // 但是,对于 “json” 类型,可不抛出错误,因为可以通过默认的 transformResponse 方法对其进行解析。
        if (config.responseType !== "json") {
    
    
          throw e;
        }
      }
    }

    // `onDownloadProgress` 允许为下载处理进度事件
    if (typeof config.onDownloadProgress === "function") {
    
    
      request.addEventListener("progress", config.onDownloadProgress);
    }

    // `onUploadProgress` 允许为上传处理进度事件
    if (typeof config.onUploadProgress === "function" && request.upload) {
    
    
      request.upload.addEventListener("progress", config.onUploadProgress);
    }

    if (config.cancelToken) {
    
    
      // 取消操作
      config.cancelToken.promise.then(function onCanceled(cancel) {
    
    
        if (!request) {
    
    
          return;
        }

        request.abort();
        reject(cancel);
        // 清除 request
        request = null;
      });
    }

    if (requestData === undefined) {
    
    
      requestData = null;
    }

    // 发送请求
    request.send(requestData);
  });
};

xhrAdapter 返回一个 Promise,执行 adapter(config) 就相当于执行 dispatchXhrRequest。因此,可以得到如下执行流程:

在这里插入图片描述

结尾

到这儿其实我们就对 axios 核心源码部分有了解了。其中涉及多种涉及模式,比如工厂模式、迭代器模式、适配器模式、中间件等。其中较为精华的部分便是其使用中间件实现拦截器,这部分内容不妨多看两遍。

猜你喜欢

转载自blog.csdn.net/qq_39025670/article/details/114224541