Axios源码解析

Axios源码解析

文章目录

  • Axios源码解析
    • 总述
    • 项目结构
    • 基本流程
    • 实例(instance)的导入
      • createInstance() - 创建实例
        • 主要代码
      • instance的扩展
        • bind() - 包装请求
        • utils.extend() - 实例的复制
      • 其他扩展
        • axios.create -- 创建新实例的工厂
        • axios.all -- 同时执行多个请求
        • axios.spread -- 用于调用函数并扩展参数数组的语法糖
        • axios.Cancel -- 支持主动取消请求
          • Cancel对象 -- 在取消操作时抛出
          • CancelToken -- 请求取消操作的对象
          • CancelToken的Promise
      • Axios对象
        • 构造函数
        • 拦截器(interceptors)
        • InterceptorManager对象
          • InterceptorManager.prototype.use -- 拦截器的注册
          • InterceptorManager.prototype.eject -- 拦截器的移除
          • InterceptorManager.prototype.forEach -- 遍历、过滤拦截器
        • Axios.prototype.request -- 发送请求的核心
        • Axios.prototype[method] -- 提供请求的语法糖
    • 实例的运行(执行请求)
      • dispatchRequest
      • XHR适配器
        • 代码及解释
        • 核心内容 - XMLHttpRequest
          • open() - 请求的准备
          • send() - 请求的正式执行
          • readyState - 请求的阶段
          • onreadystatechange - 阶段监听
          • abort() - 请求的终止
          • settle() - 根据响应结果更新Promise
          • onerror
        • HTTP basic authentication
          • 实现方式
          • 缺陷
        • XSRF(**CSRF**)跨站请求伪造
          • 简单的案例
          • 处理手段
          • axios中的处理方式
        • 多次出现的 request = null
      • HTTP适配器
        • 代码及解释
        • Buffer(缓冲区)
        • Proxy - 代理
    • 异常处理
        • createError.js
        • enhanceError.js
    • 参考资料

总述

Axios是一个NB的网络请求库(前后端神器),基于Promise封装了HTTP请求,用于浏览器node.js,GitHub 77000+ Star(截至2020年10月18日)。也是前端必备的一个第三方库。

Axios的代码不算复杂,反而清晰易懂、十分优雅(个人觉得特别是请求/响应拦截器的处理和cancelToken的处理),另外它涉及了很多JavaScript的基础知识,非常适合用来巩固基础。

本文在讲源码的同时也会穿插一些涉及前端的知识。

本文使用的axios版本: v0.20.0

项目结构

axios的项目结构如下,省略了和本文无关的目录或文件和细节目录。它的源代码在lib下:

axios                                                             
├─ lib                                                             // 项目源码目录
│  ├─ adapters                                                     // 请求适配器
│  │  ├─ http.js                                                   // http适配器
│  │  └─ xhr.js                                                    // xhr适配器
│  ├─ axios.js                                                     // axios的实例
│  ├─ cancel                                                       // 请求取消模块
│  ├─ core                                                         // 核心模块
│  │  ├─ Axios.js                                                  // Axios对象
│  │  ├─ buildFullPath.js                                          // url构建
│  │  ├─ createError.js                                            // 自定义异常相关
│  │  ├─ dispatchRequest.js                                        // 请求封装
│  │  ├─ enhanceError.js                                           // 自定义异常相关
│  │  ├─ InterceptorManager.js                                     // 拦截器类
│  │  ├─ mergeConfig.js                                            // 配置合并工具
│  │  ├─ settle.js                                                 // promise处理工具
│  │  └─ transformData.js                                          // 数据转换工具
│  ├─ defaults.js                                                  // 默认配置
│  ├─ helpers                                                      // 各种工具函数         │  └─ utils.js

基本流程

Axios的一次基本流程如下:

  • 初始化axios实例(包括配置处理、拦截器等)
  • 执行请求拦截器
  • 根据当前环境选择合适的网络适配器(adapter)(xhr – 浏览器, http request – node.js),并执行之(发送请求)
  • 处理响应数据
  • 执行响应拦截器
  • 请求完成,调用者可获取响应数据

实例(instance)的导入

执行下面的代码,我们创建了一个axios实例,我们接下来按照代码顺序来阐述整个执行过程。

const axios = require('../../lib/axios');

模块的代码如下:

// 创建一个Axios实例
function createInstance(defaultConfig) {
   
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
  // 将axios.prototype复制到实例(继承)
  utils.extend(instance, Axios.prototype, context);
  // 将上下文复制到实例(继承)
  utils.extend(instance, context);
  return instance;
}

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

// 公开Axios类以允许类继承
axios.Axios = Axios;

// 用于创建新实例的工厂
axios.create = function create(instanceConfig) {
   
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// Expose all/spread
axios.all = function all(promises) {
   
  return Promise.all(promises);
};

axios.spread = require('./helpers/spread');

module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;

createInstance() - 创建实例

主要代码
function createInstance(defaultConfig) {
   
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
  // 将axios.prototype复制到实例(继承)
  utils.extend(instance, Axios.prototype, context);
  // 将上下文复制到实例(继承)
  utils.extend(instance, context);
  return instance;
}

见名知义,createInstance便是创建实例的核心函数了,它返回了instance这个变量,内部通过extendbind这两个工具来进行加工。

instance的扩展

bind() - 包装请求

第二行:var instance = bind(Axios.prototype.request, context), 这里的bind函数如下:

'use strict';

module.exports = function bind(fn, thisArg) {
   
  return 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);
  };
};

bind()最终返回一个function,这个function的作用:以thisArg为函数调用上下文(this),调用fn

最终,instance变成了一个函数,即Axios.prototype.request,函数调用上下文(this)为context, 也就是new Axios(defaultConfig), 它来自在前一行新建的一个axios对象。

关于 apply()

给函数传参,并拥有控制函数调用上下文即函数体内 this 值的能力 ,在上面的例子中,函数的this为 thisArg,参数为args(一个数组),它通过一个简单的循环遍历得到。

类似的,ECMAScript 中的函数还有一个方法:call(),只不过call()的参数需要一个一个列出来。

关于 arguments

函数内部存在的一个特殊对象,它是一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。

在上面代码出现的argumentswrap()函数的参数。

值得注意的是,虽然在ECMAScript5 中已经内置了这个方法:Function.prototype.bind(), 但由于兼容性问题(iE9+),不直接使用。

utils.extend() - 实例的复制

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;
}

里面又出现了一个foreach(),内容如下:

function forEach(obj, fn) {
   
  // Don't bother if no value provided
  if (obj === null || typeof obj === 'undefined') {
   
    return;
  }

  // Force an array if not already something iterable
  if (typeof obj !== 'object') {
   
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }

  if (isArray(obj)) {
   
    // Iterate over array values
    for (var i = 0, l = obj.length; i < l; i++) {
   
      fn.call(null, obj[i], i, obj);
    }
  } else {
   
    // Iterate over object keys
    for (var key in obj) {
   
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
   
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

我们自上而下解读:

  • 特殊情况的判断,obj为空,或者objundefined,我们不处理,直接 return

    if (obj === null || typeof obj === 'undefined') {
         
      return;
    }
    
  • 对于不可迭代的对象,我们强制转换成一个array

    if (typeof obj !== 'object') {
         
        obj = [obj];
    }
    
  • 对于可迭代的数组,我们执行 fn

    for (var i = 0, l = obj.length; i < l; i++) {
       fn.call(null, obj[i], i, obj);
    }
    
  • 对于可迭代的对象,也是差不多的思路。

    for (var key in obj) {
         
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
         
         fn.call(null, obj[key], key, obj);
        }
    }
    

综上所述,foreach 遍历一个(可迭代的)数组或一个对象,为每个项执行传入的函数

回到我们的extend(),看看我们传入的函数 assignValue,这个函数的作用便是在迭代b的过程为a赋值

function extend(a, b, thisArg) {
   
  forEach(b, function assignValue(val, key) {
   
    // 对于函数,利用了上面说到的bind()
    if (thisArg && typeof val === 'function') {
   
      a[key] = bind(val, thisArg);
    } else {
   
      a[key] = val;
    }
  });
  return a;
}

所以最终我们的instance被如何扩展?很明显:

  • 第一个extend扩展了Axios.prototype, 即Axios对象的原型。
  • 第二个extend扩展了context, 即Axios对象,这个对象含有用户传入的一系列配置信息(这个对象的详细内容会在下面详细说明)。

这里有一个小问题,为什么我们在生成实例的时候不直接生成Axios对象,而是先以Axios.prototype.request为基础,然后基于上面说到的两部分进行扩展呢?个人认为是方便调用者调用,不用额外再手动新建对象。

其他扩展

在axios实例被export前,它被添加了几个静态方法

axios.create – 创建新实例的工厂

axios.create给用户提供了自定义配置的接口,通过调用mergeConfig()来合并用户配置和默认配置。从而我们可以得到一个自定义的instance

axios.create = function create(instanceConfig) {
   
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
axios.all – 同时执行多个请求

如果你了解过Promise.all(), 那么你一定可以猜出axios.all是个啥了。

axios.all = function all(promises) {
   
  return Promise.all(promises);
};
axios.spread – 用于调用函数并扩展参数数组的语法糖

axios.spread 常和 axios.all配合使用,例如:

function getUserAccount() {
   
  return axios.get('/user/12345');
}

function getUserPermissions() {
   
  return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
  .then(axios.spread(function (acct, perms) {
   
    // 两个请求现在都执行完成
  }));

我们来看看axios.spread()的实现:

'use strict';
module.exports = function spread(callback) {
   
  return function wrap(arr) {
   
    return callback.apply(null, arr);
  };
};

axios.spread()要求我们传入一个函数,最终promise.all()的结果会作为它的参数,并通过callback.apply()执行。

axios.Cancel – 支持主动取消请求

这个功能非常有意思,用户可以调用这个接口来随时取消请求,像这样:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('xxxxxxxxx', {
   
  cancelToken: source.token
}).catch(function(thrown) {
   
  if (axios.isCancel(thrown)) {
   
    console.log('Request canceled', thrown.message);
  } else {
   
     // 处理错误
  }
});

source.cancel('请求被用户取消!');
  • CancelToken.source()是一个工厂方法,它生产了一个对象,包含CancelToken()和一个默认的cancel(),于是调用者就可以用下面的方法注册CancelToken()

    // 方法1 - 默认的工厂方法
    const CancelToken = axios.CancelToken;
    const source = CancelToken.source();
    axios.get('xxxxxxxxxxxx', {
         
      cancelToken: source.token
    }).catch(function(thrown) {
         
      if (axios.isCancel(thrown)) {
         
        console.log('Request canceled', thrown.message);
      } else {
         
         // 处理错误
      }
    });
    
    // 方法2 - 自定义cancel
    let cancel;
    axios

你可能感兴趣的:(javascript)