likes
comments
collection
share

axios源码解析

作者站长头像
站长
· 阅读数 40

by: 洋芋丝

一、axios 简介

axios 是什么?

axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

axios 有什么特性 ?

  • 从浏览器中创建 XMLHttpRequest
  • 从 node.js 中创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

阅读完这篇文章,大家可以了解到一下内容:

axios 为什么可以使用 axios({ }) 和 axios.get()两种方式发送请求

axios 响应拦截器是如何实现的

axios 如何实现

如何使用 axios 避免 XSRF 攻击

二、 源码解析

下面这张图是整个源码的结构,可以很详细的看到 axios 中主要包含了哪些内容:

axios源码解析

接下来我们就按照上面四个问题来依次分析,找到答案。

2.1 创建实例

1.axios 为什么可以使用 axios({ }) 和 axios.get()两种方式发送请求

创建 axios 实例,也是我们通过 import axios from 'axios' 时的 axios 对象,这个对象实际上是 Axios 类的原型上的 request 方法, 方法中的 this 指向 一个新的基于默认配置创建的 axios 实例。

/**
* @param {Object} defaultConfig 默认配置
* @return {Axios} 一个 axios 的实例对象
*/
function createInstance(defaultConfig) {
     // 基于默认配置创建一个Axios实例上下文。
     var context = new Axios(defaultConfig);

     // bind方法返回一个函数,执行这个函数,相当于执行 Axios.prototype.request,方法中的 this 指向 context,
     // 这就是我们引入 axios 后可以直接通过 axios({...}) 发送请求的原因,
     var instance = bind(Axios.prototype.request, context);

     // 将 axios 的原型对象 Axios.prototype 上的属性依次赋值给这个实例对象
     // 这样操作后我们就可以通过 axios.get()发送请求,实际上调用原型对象上的方法
     utils.extend(instance, Axios.prototype, context);

     // 将 axios 实例的私有属性赋值给当前的 instance
     // 这样我们可以获取到实例上的属性,例如 通过 axios.defaultConfig 获取默认配置
     utils.extend(instance, context);
     return instance;
}

// 创建一个 axios 实例,实际上就是上述函数中的 instance;
var axios = createInstance(defaults);

module.exports.default = axios;

暴露的axios上挂载了基于默认配置创建的Axios实例属性,也挂载了原型上的方法。这里就解答了一个问题,使用 axios(config) 发送请求调用的是 Axios.prototype.request 方法,使用 axios.get(url[, config] )方法发送请求,调用的是 Axios.prototype.get 方法。

2.2 Axios 构造函数

这里介绍一下Axios构造函数的内容。

/**
* Create a new instance of Axios
* @param {Object} instanceConfig 默认配置
*/
function Axios(instanceConfig) {
   this.defaults = instanceConfig;
   this.interceptors = {
     request: new InterceptorManager(),
     response: new InterceptorManager()
   };
}

Axios.prototype.request = function request() {}

2.3 发送请求的 request 方法

这个方法做了两件事

1. 获取发送 HTTP 请求的参数

2.编排请求的 Promise 链,并执行该 Promise链

2.4 请求响应拦截器的实现

2.4.1 拦截器简介

在开发中经常会遇到需要在请求头中添加 token 字段用于登录身份验证,针对不同的请求方法做不同的处理,或者是对响应做统一的错误处理,例如统一报错处理。

所以 axios 提供了请求拦截器和响应拦截器,分别处理请求和响应,它们的作用如下:

  • 请求拦截器:该类拦截器的作用是在请求发送前统一执行某些操作,比如在请求头中添加 token 字段。
  • 响应拦截器:该类拦截器的作用是在接收到服务器响应后统一执行某些操作,比如发现响应状态码为 401 时,自动跳转到登录页。

2.4.2 如何使用

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

请求相应拦截器本质上都是实现特定功能的函数,类似于webpack 的 loader, 上一个函数的执行结果作为下一个函数的参数处理,执行顺序如下

请求拦截器 -> 分发http请求 -> 响应拦截器

要实现这个过程,我们首先需要注册请求拦截器和响应拦截器,然后axios会按照顺序给我们注册的函数排序,最后会依次执行排好序的函数,下面我们分三个步骤讲解具体实现过程

2.4.3 注册请求拦截器和相应拦截器

要知道拦截器是如何注册的,就需要看一下 Axios的构造函数

function Axios(instanceConfig) { 
    this.defaults = instanceConfig;  
    this.interceptors = {    
        request: new InterceptorManager(),    
        response: new InterceptorManager() 
    }; 
}

axios 上挂载了 axios 类的一个实例,这个实例有一个interceptors 属性,属性值是一个对象,包含request 和 response 两个属性,分别是用来注册和管理 请求拦截器和相应拦截器。我们来看一下是如何进行管理的

// 拦截器的构造函数 
function InterceptorManager() {  
    this.handlers = []; 
} 

// 注册拦截器函数,注意:这里拦截器可以注册多个, 按照注册的先后顺序排列
InterceptorManager.prototype.use = function use(fulfilled, rejected) {  
    this.handlers.push({ fulfilled: fulfilled,  rejected: rejected });  
    return this.handlers.length - 1; 
}; 
// 用于移除拦截器 
InterceptorManager.prototype.eject = function eject(id) { 
    if (this.handlers[id]) {   
        this.handlers[id] = null; 
    } 
}; 

可以看到当我们调用 axios.interceptors.request.use(fulfilled, rejected),就成功注册了一个请求拦截器。注意,后注册的请求拦截器会先执行,响应拦截器是按照注册顺序执行的。

2.4.4 如何给拦截器排序,让他们能按照我们预想的顺序执行

在 axios.prototype.request 完成参数的处理和合并之后,接下来就是执行请求拦截器 => 发送 HTTP 请求 => 执行响应拦截器,我们来看下具体是如何实现这个顺序的。

var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
});

首先是声明了分发HTPP请求的数组,接下来声明了一个resolve状态的Promise,然后依次取出请求拦截器,放在数组的头部,响应拦截器放在数组的尾部。就组成了这样一个链条:

[request.fullfilled, request.rejected, ..., dispatchRequest, null, ..., response.fullfileed, response.rejected]

2.4.5 执行请求响应拦截器

当我们排好顺序之后,接下来就是按照顺序执行。我们来看下是如何执行的

while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
}

return promise;

chain是一个数组,只要 length 不为0,就会一直执行,直到最后一个响应拦截器执行完,返回的还是一个promise,这里需要注意,我们的自定义请求和响应拦截器一定要有返回值,否则请求结束后,我们无法获取最后的结果。

通过响应拦截器我们很方便在各个阶段执行自定义的行为。

2.5 发送HTTP 请求的 dispatchRequest 方法

终于到了真正发送请求的阶段,这个阶段需要关注的点是axios会根据环境的不同调用不同的请求处理函数,如果是浏览器环境,会基于XMLHttpRequest创建,如果是node环境下,会基于HTTP模块创建。这里涉及到一个设计模式,适配器模式,用来解决已有接口之间不匹配的问题。 dispatchRequest 的执行流程:

config.header → transformRequestData → adapter → transformResponseData

在 adapter 中,除了发送 HTTP 请求之外,还有了一些有意思的处理,接下来我们就来看两个点:

1.可以通过cancel token (abort函数)来取消请求

2.可以设置 XSRF-TOKEN 来避免 CSRF 攻击

2.6 使用 cancel token 取消请求

首先我们来看开头提出的第三个问题,如何取消请求?想象一个场景,当我们正在上传文件的时候,突然切换了页面,那么上一页面的上传请求就应该被取消掉,或者我们在进行接口轮询时,有可能上次的请求还在,在进行下一次请求之前应该取消掉上次的请求,这时候就可以用 cancel token 取消请求。

2.6.1 如何使用

import axios from 'axios' 
const CancelToken = axios.CancelToken 
/**  
* 文件上传方法 
* @param url  
* @param file 
* @param config  
* @returns {AxiosPromise<any>}  
*/
export default function uploadFile(url, file, config = {}) {  
   const source = CancelToken.source();   
   const axiosConfig = Object.assign({ cancelToken: source.token }, config);   
   
   const formData = new FormData();   
   formData.append('file', file);  
   
   const instance = axios.create();   
   const request = instance.post(url, formData, axiosConfig);    
   request.cancel= source.cancel;   
   return request; 
} 

然后把 request 保存在 viewData 中,方法返回成功就把 request.cancel 置为 null,在页面销毁的时候,判断request.cancel 的值,存在就执行request.cancel 取消请求。

2.6.2 cancel token 是如何实现的

我们先看下 axios.CancelToken. source 是什么

axios.CancelToken = require('./cancel/CancelToken'); 

接下来就进入到 CancelToken 的文件夹中看看

/**
 * @class 构造函数 CancelToken
 * @param {Function} executor The executor function.
 */
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }
    
    // 这里的 reason 是一个对象,对象有一个 message 属性,是一个字符串
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

// 上面我们例子中的 axios.CancelToken.source
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;

axios.CancelToken.source 是一个对象,这个对象有两个属性,分别是 token 和 cancel,token的作用是提供一个Promise,cancel用于中断请求。 token 需要传递到 config 里,当cancel函数被执行时,token的状态由 Pending 变为 Resolve

function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }
    
    // 这里的 reason 是一个对象,对象有一个 message 属性,是一个字符串
    token.reason = new Cancel(message);
    
    // 这里的 resolvePromise 就是用来将取消请求的Promise状态由pending置为resolve
    resolvePromise(token.reason);
}

看完了 CancelToken 的实现,我们再去看一下它是如何中断请求的

在 xhr.js 中有一段关键性代码,如下:

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // Handle browser request cancellation (as opposed to a manual cancellation)
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }
      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      // Clean up request
      request = null;
    };
    
    if (config.cancelToken) {
      // 这里的config.cancelToken.promise就是我们之前说的pending状态的promise,
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
  });
};

当我们执行 axios.cancelToken.source.cancel('取消请求') 的时候,就会将pending状态的 Promise置为resolve状态,会接着执行.then后面的回调函数,即执行request.abort()终止请求。接着会执行request.onabort注册的函数,将 xhrAdapter 中的 request Promise 状态置为 reject,我们就可以在catch中捕获到错误。

2.7 简单介绍一下 CSRF 攻击

跨站请求伪造(Cross-site request forgery),简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。

之前大家应该都都类似的经历,点击一个链接密码就被盗了,为什么会出现这样的情况呢?

发生 csrf 的条件有三个,满足这三个条件,就会发生 CSRF 攻击

第一个,目标站点一定要有 CSRF 漏洞;

第二个,用户要登录过目标站点,并且在浏览器上保持有该站点的登录状态;

第三个,需要用户打开一个第三方站点

当用户在一个网站登陆时,比如说是一个论坛,点击一张图片,src实际上是一个http请求地址(script src, 或者 img src 等都不受同源策略限制)

这样就向服务器发送了类似转账或者修改密码的请求,如果该服务器存在漏洞,就会发生 CSRF 攻击。

那么如何避免让服务器避免遭受到 CSRF 攻击呢?

  1. 充分利用好 Cookie 的 SameSite 属性,SameSite 选项通常有 Strict、Lax 和 None 三个值。
  2. 验证请求的来源站点
  3. CSRF Token,这个也是 axios 防止 CSRF 攻击的使用方式

Axios 提供了 xsrfCookieName 和 xsrfHeaderName 两个属性来分别设置 CSRF 的 Cookie 名称和 HTTP 请求头的名称,它们的默认值如下所示:

// lib/defaults.js 
var defaults = {   
    adapter: getDefaultAdapter(),  
    xsrfCookieName: 'XSRF-TOKEN', 
    xsrfHeaderName: 'X-XSRF-TOKEN',
}; 

接下来我们来看下Axios 如何防御 CSRF 攻击

// lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {   
    return new Promise(function dispatchXhrRequest(resolve, reject) {     
    var requestHeaders = config.headers;          
    var request = new XMLHttpRequest();         
    // 添加xsrf头部     
    if (utils.isStandardBrowserEnv()) {       
        var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName 
        ? cookies.read(config.xsrfCookieName) 
        : undefined;       
        if (xsrfValue) {         
            requestHeaders[config.xsrfHeaderName] = xsrfValue;      
        }     
    } 
    request.send(requestData);  
    }); 
}; 

看完以上代码,我们知道了 Axios 是将 token 设置在 Cookie 中,在提交(POST、PUT、PATCH、DELETE)等请求时提交 Cookie,并通过请求头或请求体带上 Cookie 中已设置的 token,服务端接收到请求后,再进行对比校验。

三、 总结

此次分享主要分享了请求响应拦截器和取消请求的实现原理。本次分享到此结束,有写的不好的地方,欢迎指正~

四、 参考资料

使用这些思路与技巧,我读懂了多个优秀的开源项目

XMLHttpRequest.abort()

Axios中文文档

适配器模式

招贤纳士

青藤前端团队是一个年轻多元化的团队,坐落在有九省通衢之称的武汉。我们团队现在由 20+ 名前端小伙伴构成,平均年龄26岁,日常的核心业务是网络安全产品,此外还在基础架构、效率工程、可视化、体验创新等多个方面开展了许多技术探索与建设。在这里你有机会挑战类阿里云的管理后台、多产品矩阵的大型前端应用、安全场景下的可视化(网络、溯源、大屏)、基于Node.js的全栈开发等等。

如果你追求更好的用户体验,渴望在业务/技术上折腾出点的成果,欢迎来撩~ yan.zheng@qingteng.cn