【js】深度学习之Axios源码解析

817 阅读9分钟

【祖传开头】

早睡早起保平安

Ajax

在解析源码前,首先要了解一下ajax。ajax其实是一种技术方案,用来实现网页的异步请求。作用就是与服务器进行数据交换,刷新部分网页内容。避免全局页面的刷新,来提高交互体验。 而在游览器环境中实现这一技术方案的实现主要依赖于js中的XMLHttpRequest。

XMLHttpRequest

XMLHttpRequest对象用于与服务器的交互,也就是说可以发送一个http请求和接受http请求返回的数据。

一个简单的例子:

// 实例化
var xhr= new XMLHttpRequest(),
    method = "GET",
    url = "https://developer.mozilla.org/";

// 初始化一个js请求
xhr.open(method, url, true);
// 添加readystate变化后的回调事件
xhr.onreadystatechange = function () {
  if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
    console.log(xhr.responseText)
  }
}
// 发送请求
xhr.send();

简单的了解了XMLHttpRequest的使用,就可以开始axios的源码解析了!

Axios源码

游览器环境下,以XMLHttpRequest 为基础进行包装。

官网摘抄的的Axios特性:

• 从浏览器中创建 XMLHttpRequests
• 从 node.js 创建 http 请求
• 支持 Promise API
• 拦截请求和响应
• 转换请求数据和响应数据
• 取消请求

• 自动转换 JSON 数据

• 客户端支持防御 XSRF

可以看到axios两个大的优势,一个是对promise API的支持。另一个就是可以对游览器和node环境同时进行支持。可以说是一个很优秀的工具类库了。

开始进行代码解析

源码入口解析: 一个简单的使用例子:

import axios from 'axios';
// 发送 POST 请求
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});

上述例子可知 axios的入口默认抛出的axios是一个方法。可以让我们进行调用。再来看看lib/axios这个入口文件。

// lib/axios
// 简化版
/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  // new一个Axios的实例
  // Axios类具体有什么属性和方法呢
  var context = new Axios(defaultConfig);
  // 为Axios类上的request方法指定执行上下文
  // bind方法返回一个函数
  var instance = bind(Axios.prototype.request, context);
  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);
  // Copy context to instance
  utils.extend(instance, context);
  return instance;
}
// 创建默认配置的实例
var axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
axios.Axios = Axios;
// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// 导出axios方法
module.exports = axios;

入口文件主要做了什么?

  • 定义了函数createInstance
  • axios = createInstance()
  • 函数 createInstance 返回了方法 Axios.prototype.request
  • 在axios上定义了其他属性和方法,axios.create()、axios.timeout等等

在函数createInstance中首先实例化了一个Axios类,这个类提供了什么?

// lib/core/Axios
// 定义Axios类
// 构造函数中定义和初始化属性 defaults 和 interceptors
// defaults 默然配置
// interceptors 拦截器相关
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// 方法request:用于发送请求、接收请求返回的数据
Axios.prototype.request = function request(config) {
  // ...
  return promise;
};

// 根据url和params参数重新处理url
Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
  return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};

// 遍历数组在Axios类上增加以下方法
// 其实还是针对不同配置的request方法
// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: (config || {}).data
    }));
  };
});

// 遍历数组在Axios类上增加以下方法
// 其实还是针对不同配置的request方法
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
module.exports = Axios;

看看request方法,这个方法就是发送请求和实现拦截器的核心。

Axios.prototype.request = function request(config) {
  // 1.处理config 并合并默认配置
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  config = mergeConfig(this.defaults, config);
  
  // 2.处理 config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  
  // 3.过滤拦截器添加
  // 请求拦截器执行链
  var requestInterceptorChain = [];
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // 响应拦截器执行链
  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
  var promise;
  
  var chain = [dispatchRequest, undefined];
  
  // 数组前插入requestInterceptorChain
  // 数组后插入responseInterceptorChain
  Array.prototype.unshift.apply(chain, requestInterceptorChain);
  chain.concat(responseInterceptorChain);
  promise = Promise.resolve(config);
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
}

其中第三步,过滤拦截器方法的添加,先看requestInterceptorChainresponseInterceptorChain 默认添加两个方法,interceptor.fulfilledinterceptor.rejected。这两个方法分别对应外部使用拦截器时传入的两个回调函数。比如以下例子,axios.interceptors.request.use和axios.interceptors.response.use方法中传入了两个函数,在这里暂时称为request1,request2,response1,response2。这四个方法分别对应interceptor.fulfilled、interceptor.rejected 、interceptor.fulfilled、interceptor.rejected。

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response;
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error);
})

然后对于数组chain,初始长度是2,然后再数组前拼接requestInterceptorChain, 后面拼姐responseInterceptorChain

chain = [interceptor.fulfilled, interceptor.rejected, dispatchRequest, undefined,interceptor.fulfilled, interceptor.rejected]。

再往后看,开始对chain 进行循环,每次循环从数组前面取出两个方法,作为promise中then的两个方法。promise.then(),有两个可选参数分别对应resolved状态的回调和rejected状态的回调。 所以最终:

promise = Promise.resolve(config).then(interceptor.fulfilled, interceptor.rejected)
.then(dispatchRequest, undefined)
.then(interceptor.fulfilled, interceptor.rejected)

回到最开始的入口文件,我们看到var axios = createInstance(defaults),而createInstance函数中返回的就是Axios类的一个实例上的request方法,所以归根揭底,axios执行完后返回的就是一个peomise实例,可以继续调用then或者promise方法。这也就是为什么通过axios调用完接口后可以接续调用then或者catch方法。

dispatchRequest

另外再来看一下dispatchRequest方法,这个方法就是真正的触发请求的地方。(此处只对浏览器环境进行分析) 开头就说过在游览器环境下,ajax的实现主要依赖js的XMLHttpRequest。所以axios中也是基于XMLHttpRequest去实现发送请求和获取请求响应的数据。

dispatchRequest方法中,最重要的就是去获取adapter,这个adapter就是发送一个请求所使用的适配器

比如我在浏览器环境下的适配器就是基于XMLHttpRequest封装的一个适配器,在node环境就是基于另一种方式封装的适配器。

module.exports = function dispatchRequest(config) {
  // 获取当前环境下使用的适配器
  // 没有传入配置的adapter,则去获取默认的adapter
  var adapter = config.adapter,则去获取默认的 || defaults.adapter;
  // 执行
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    // 请求成功
    // 转换response数据
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );
    return response;
  }, function onAdapterRejection(reason) {
        // 请求失败就返回一个状态为rejected的一个promise实例
    return Promise.reject(reason);
  });
};

那么再来看看,defaults.adapter在哪里进行配置的。从入口文件可以得知,最开始就传入了一份默认配置defaults。

var defaults = require('./defaults');
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
  return instance;
}
var axios = createInstance(defaults);

在defaults.js文件中,可以看到回去调用一个getDefaultAdapter方法,去获取默认的适配器。

var defaults = {
  adapter: getDefaultAdapter()
    // ...
}

getDefaultAdapter中,判断完环境是游览器后会去引入require('./adapters/xhr');,那么axios发送http请求的核心就是在/adapters/xhr文件中了。

简化一下代码,以及XMLHttpRequest的一些属性事件,大致的代码逻辑如下。

module.exports = function xhrAdapter() {
  // adapter(config),返回一个promise实例
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var request = new XMLHttpRequest();
    // open的三个参数:请求方法、请求地址 和 默认开启异步
    request.open(config.method, url ,true);
    // 监听请求状态
    request.onreadystatechange = function handleLoad() {
      // 根据responseType获取responseData
      var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
      // 封装最终返回的response
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };
      // settle方法中根据response.status去判断要执行resolve还是 reject
      settle(resolve, reject, response);
      // Clean up request
      request = null;
  })
  // 调用send方法
  request.send(requestData);
}

到这里,游览器下axios的实现就基本清楚了。

总结

如何支持promise API?

在判断完当前环境是游览器后,确定当前去实现发送http请求方式是基于XMLHttpRequest封装的一个适配器。 执行发送请求的方法后,返回的是一个Promise实例。 在请求响应后,XMLHttpRequest的onstatereadychange对应的回调函数中会判断请求响应的状态,决定将peomise实例的状态更改为resloved或者rejected

另外在实现请求和响应拦截器的时候,也是使用了promise的链式调用。 当外部使用axios方法时,会根据拦截器的配置,生成一个执行方法链,这个链中的方法分别是请求发送前拦截器的执行方法及处理异常的方法,核心发送请的方法,undefined,正确响应执行的方法,处理异常响应执行的方法。这个6个方法按照顺序,两两组成then方法的参数的两个参数,通过then的链式调用,依次执行下去。 这样就是实现了请求拦截器功能。

拦截器实现的原理?

参照上述。另外补充一点,上述源码分析时,执行链中第四个参数为undefined,在请求前发生的异常,没有对应的异常回调方法,就不会继续往下执行,也不会发送请求。

axios中如何实现XSRF攻击?

XSRF,跨域请求伪造。通常是用户在A网站登录后,A网站的服务器返回一段认证身份的信息。然后在B网站,通过发送携带A网站中认证身份信息的请求,去做一些恶意的攻击或者其他操作。

解决XSRF攻击的方法:

  1. 利用http请求头中的Referer字段。这个字段表明这个请求来自那一个地址,通常和当前所在的网站地址一致。所以当网站B发起恶意请求时会发现Referer字段和当前A网站的地址不同,从而达到拦截XSRF攻击的作用。但是由于不同游览器处理Referer字段的不同方式,导致这种方式也并不是100%安全。 2.设置一个动态的token,每次发送请求后带上token和cookie信息,服务端进行对比验证后确认请求的安全性。

来看看axios是如何解决的。首先axios提供了xsrfCookieName和xsrfHeaderName这两个配置项来对应cookie token对应的字段和,以及需求配置到请求头哪个字段。

// lib/adapters/xhr.js
// 判断是否为标准的游览器环境
if (utils.isStandardBrowserEnv()) {
  // Add xsrf header
  // isURLSameOrigin判断当前请求和当前所在网站的地址是否同源
  // 即协议和host一致
  // 判断xsrfCookieName配置后读取
  var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
    cookies.read(config.xsrfCookieName) :
    undefined;
  // 获取到xsrfCookieName后,设置到请求头中
  if (xsrfValue) {
    requestHeaders[config.xsrfHeaderName] = xsrfValue;
  }
}

所以axios可以让我们通过配置,使用以上两种方式一起来防御XSRF攻击。

【祖传结尾】

有问题欢迎批评指正呀~