专栏首页前端技术江湖Axios 源码解析-完整篇

Axios 源码解析-完整篇

背景

日常开发中我们经常跟接口打交道,而在现代标准前端框架(Vue/React)开发中,离不开的是 axios,出于好奇阅读了一下源码。

阅读源码免不了枯燥无味,容易被上下文互相依赖的关系搞得一头露水,我们可以抓住主要矛盾,忽略次要矛盾,可结合 debugger 调试模式,先把主干流程梳理清楚,在慢慢啃细节比较好,以下是对源码和背后的设计思想进行解读,不足之处请多多指正。

axios 是什么

  1. 基于 promise 封装的 http 请求库(避免回调地狱)
  2. 支持浏览器端和 node
  3. 丰富的配置项:数据转换器,拦截器等等
  4. 客户端支持防御 XSRF
  5. 生态完善(支持 Vue/React,周边插件等等)

另外两条数据证明 axios 使用之广泛

1.截至 2021 年 6月底,githubstar 数高达 85.4k

2.npm 的周下载量达到千万级别

Axios 的基本使用

源码目录结构

先看看目录说明,如下

执行流程

先看看整体执行流程,有大体的概念,后面会细说 整体流程有以下几点:

  1. axios.create 创建单独实例,或直接使用 axios 实例(axios/axios.get…)
  2. request 方法是入口,axios/axios.get 等调用都会走进 request 进行处理
  3. 请求拦截器
  4. 请求数据转换器,对传入的参数 dataheader 做数据处理,比如 JSON.stringify(data)
  5. 适配器,判断是浏览器端还是 node 端,执行不同的方法
  6. 响应数据转换器,对服务端的数据进行处理,比如 JSON.parse(data)
  7. 响应拦截器,对服务端数据做处理,比如 token 失效退出登陆,报错 dialog 提示
  8. 返回数据给开发者

入口文件(lib/axios.js)

从下面这段代码可以得出,导出的 axios 就是实例化后的对象,还在其上挂载 create 方法,以供创建独立实例,从而达到实例之间互不影响,互相隔离。

...
// 创建实例过程的方法
function createInstance(defaultConfig) {
  return instance;
}
// 实例化
var axios = createInstance(defaults);

// 创建独立的实例,隔离作用域
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
...
// 导出实例
module.exports = axios;

可能大家对 createInstance 方法感到好奇,下面一探究竟。

function createInstance(defaultConfig) {
  // 实例化,创建一个上下文
  var context = new Axios(defaultConfig);

  // 平时调用的 get/post 等等请求,底层都是调用 request 方法
  // 将 request 方法的 this 指向 context(上下文),形成新的实例
  var instance = bind(Axios.prototype.request, context);

  // Axios.prototype 上的方法 (get/post...)挂载到新的实例 instance 上,
  // 并且将原型方法中 this 指向 context
  utils.extend(instance, Axios.prototype, context);

  // Axios 属性值挂载到新的实例 instance 上
  // 开发中才能使用 axios.default/interceptors
  utils.extend(instance, context);

  return instance;
}

从上面代码可以看得出,Axios 不是简单的创建实例 context,而且进行一系列的上下文绑定和属性方法挂载,从而去支持 axios(),也支持 axios.get() 等等用法;

createInstance 函数是一个核心入口,我们在把上面流程梳理一下:

  1. 通过构造函数 Axios 创建实例 context,作为下面 request 方法的上下文(this 指向)
  2. Axios.prototype.request 方法作为实例使用,并把 this 指向 context,形成新的实例 instance
  3. 将构造函数 Axios.prototype 上的方法挂载到新的实例 instance 上,然后将原型各个方法中的 this 指向 context,开发中才能使用 axios.get/post… 等等
  4. 将构造函数 Axios 的实例属性挂载到新的实例 instance 上,我们开发中才能使用下面属性 axios.default.baseUrl = 'https://…' axios.interceptors.request.use(resolve,reject)

大家可能对上面第 2request 方法感到好奇,createInstance 方法明明可以写一行代码 return new Axios() 即可,为什么大费周章使用 request 方法绑定新实例,其实就只是为了支持 axios() 写法,开发者可以写少几行代码。。。

默认配置(lib/defaults.js)

createInstance 方法调用发现有个默认配置,主要是内置的属性和方法,可对其进行覆盖

var defaults = {
  ...
  // 请求超时时间,默认不超时
  timeout: 0,
  // 请求数据转换器
  transformRequest: [function transformRequest(data, headers) {...}],
  // 响应数据转换器
  transformResponse: [function transformResponse(data) {...}],
  ...
};
...
module.exports = defaults;

构造函数 Axios(lib/core/Axios.js)

主要有两点:

  1. 配置:外部传入,可覆盖内部默认配置
  2. 拦截器:实例后,开发者可通过 use 方法注册成功和失败的钩子函数,比如 axios.interceptors.request.use((config)=>config,(error)=>error);
function Axios(instanceConfig) {
  // 配置
  this.defaults = instanceConfig;
  // 拦截器实例
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

在看看原型方法 request 做了什么

  1. 支持多类型传参
  2. 配置优先级定义
  3. 通过 promise 链式调用,依次顺序执行
// 伪代码
Axios.prototype.request = function request(config) {
  // 为了支持 request(url, {...}), request({url, ...})
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  // 配置优先级: 调用方法的配置 > 实例化axios的配置 > 默认配置
  // 举个例子,类似:axios.get(url, {}) > axios.create(url, {}) > 内部默认设置
  config = mergeConfig(this.defaults, config);
  // 拦截器(请求和响应)
  var requestInterceptorChain = [{
    fulfilled: interceptor.request.fulfilled,
    rejected: interceptor.request.rejected
  }];
  var responseInterceptorChain = [{
    fulfilled: interceptor.response.fulfilled,
    rejected: interceptor.response.rejected
  }];
  var promise;
  // 形成一个 promise 链条的数组
  var chain = [].concat(requestInterceptorChain, chain, responseInterceptorChain);
  // 传入配置
  promise = Promise.resolve(config);
  // 形成 promise 链条调用
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  ...
  return promise;
};

通过对数组的遍历,形成一条异步的 promise 调用链,是 axiospromise 的巧妙运用,用一张图表示

拦截器 (lib/core/InterceptorManager.js)

上面说到的 promise 调用链,里面涉及到拦截器,拦截器比较简单,挂载一个属性和三个原型方法

  • handler: 存放 use 注册的回调函数
  • use: 注册成功和失败的回调函数
  • eject: 删除注册过的函数
  • forEach: 遍历回调函数,一般内部使用多,比如:promise 调用链那个方法里,循环遍历回调函数,存放到 promise 调用链的数组中
function InterceptorManager() {
  // 存放 use 注册的回调函数
  this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  // 注册成功和失败的回调函数
  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;
  }
};
InterceptorManager.prototype.forEach = function forEach(fn) {
  // 遍历回调函数,一般内部使用多
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

dispatchRequest(lib/core/dispatchRequest.js)

上面说到的 promise 调用链中的 dispatchRequest 方法,主要做了以下操作:

  1. transformRequest: 对 config 中的 data 进行加工,比如对 post 请求的 data 进行字符串化 (JSON.stringify(data))
  2. adapter:适配器,包含浏览器端 xhrnode 端的 http
  3. transformResponse: 对服务端响应的数据进行加工,比如 JSON.parse(data)

dispatchRequest 局部图

module.exports = function dispatchRequest(config) {
  ...
  // transformRequest 方法,上下文绑定 config,对 data 和 headers 进行加工
  config.data = transformData.call(
    config, // 上下文环境,即 this 指向
    config.data, // 请求 body 参数
    config.headers, // 请求头
    config.transformRequest // 转换数据方法
  );
  // adapter 是一个适配器,包含浏览器端 xhr 和 node 端的 http
  // 内置有 adapter,也可外部自定义去发起 ajax 请求
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    // transformResponse 方法,上下文绑定 config,对 data 和 headers 进行加工
    response.data = transformData.call(
      config, // 上下文环境,即 this 指向
      response.data, // 服务端响应的 data
      response.headers, // 服务端响应的 headers
      config.transformResponse // 转换数据方法
    );
    return response;
  }, function onAdapterRejection(reason) {
    ...
    return Promise.reject(reason);
  });
};

数据转换器(lib/core/transformData.js)

上面说到的数据转换器,比较好理解,源码如下

module.exports = function transformData(data, headers, fns) {
  var context = this || defaults;
  // fns:一个数组,包含一个或多个方法转换器方法
  utils.forEach(fns, function transform(fn) {
    // 绑定上下文 context,传入 data 和 headers 参数进行加工
    data = fn.call(context, data, headers);
  });
  return data;
};

fns 方法即(请求或响应)数据转换器方法,在刚开始 defaults 文件里定义的默认配置,也可外部自定义方法,源码如下:

Axios(lib/defaults.js)

var defaults = {
  ...
  transformRequest: [function transformRequest(data, headers) {
    // 对外部传入的 headers 进行规范纠正,比如 (accept | ACCEPT) => Accept
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    ...
    if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
      // post/put/patch 请求携带 data,需要设置头部 Content-Type
      setContentTypeIfUnset(headers, 'application/json');
      // 字符串化
      return JSON.stringify(data);
    }
    return data;
  }],
  transformResponse: [function transformResponse(data) {
    ...
    try {
      // 字符串解析为 json
      return JSON.parse(data);
    } catch (e) {
      ...
    }
    return data;
  }],
}

可以看得出,(请求或响应)数据转换器方法是存放在数组里,可定义多个方法,各司其职,通过遍历器对数据进行多次加工,有点类似于 node 的管道传输 src.pipe(dest1).pipe(dest2)

适配器(lib/defaults.js)

主要包含两部分源码,即浏览器端 xhr 和 node 端的 http 请求,通过判断环境,执行不同端的 api

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // 浏览器
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // node
    adapter = require('./adapters/http');
  }
  return adapter;
}

对外提供统一 api,但底层兼容浏览器端和 node 端,类似 sdk,底层更改不影响上层 api,保持向后兼容

发起请求(lib/adapters/xhr.js)

平时用得比较多的是浏览器端,这里只讲 XMLHttpRequest 的封装,node 端有兴趣的同学自行查看源码(lib/adapters/http.js)

简易版流程图表示大致内容:

源码比较长,使用伪代码表示重点部分

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    ...
    // 初始化一个 XMLHttpRequest 实例对象
    var request = new XMLHttpRequest();
    // 拼接url,例如:https://www.baidu,com + /api/test
    var fullPath = buildFullPath(config.baseURL, config.url);
    // 初始化一个请求,拼接url,例如:https://www.baidu,com/api/test + ?a=10&b=20
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    // 超时断开,默认 0 永不超时
    request.timeout = config.timeout;
    // 当 readyState 属性发生变化时触发,readyState = 4 代表请求完成
    request.onreadystatechange = resolve;
    // 取消请求触发该事件
    request.onabort = reject;
    // 一般是网络问题触发该事件
    request.onerror = reject;
    // 超时触发该事件
    request.ontimeout = reject;
    // 标准浏览器(有 window 和 document 对象)
    if (utils.isStandardBrowserEnv()) {
      // 非同源请求,需要设置 withCredentials = true,才会带上 cookie
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;
      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }
    // request对象携带 headers 去请求
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          // data 为 undefined 时,移除 content-type,即不是 post/put/patch 等请求
          delete requestHeaders[key];
        } else {
          request.setRequestHeader(key, val);
        }
      });
    }
    // 取消请求,cancelToken 从外部传入
    if (config.cancelToken) {
      // 等待一个 promise 响应,外部取消请求即执行
      config.cancelToken.promise.then(function onCanceled(cancel) { 
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
    // 发送请求
    request.send(requestData);
  });
};

取消请求(lib/cancel/CancelToken.js)

先看看 axios 中文文档使用

var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // 处理错误
  }
});
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');

可以猜想,CancelToken 对象挂载有 source 方法,调用 source 方法返回 {token, cancel},调用函数 cancel 可取消请求,但 axios 内部怎么知道取消请求,只能通过 { cancelToken: token } ,那 tokencancel 必然有某种联系

看看源码这段话

  1. CancelToken 挂载 source 方法用于创建自身实例,并且返回 {token, cancel}
  2. token 是构造函数 CancelToken 的实例,cancel 方法接收构造函数 CancelToken 内部的一个 cancel 函数,用于取消请求
  3. 创建实例中,有一步是创建处于 pengding 状态的 promise,并挂在实例方法上,外部通过参数 cancelToken 将实例传递进 axios 内部,内部调用 cancelToken.promise.then 等待状态改变
  4. 当外部调用方法 cancel 取消请求,pendding 状态就变为 resolve,即取消请求并且抛出 reject(message)
function CancelToken(executor) {
  var resolvePromise;
  /**
   * 创建处于 pengding 状态的 promise,将 resolve 存放在外部变量 resolvePromise
   * 外部通过参数 { cancelToken: new CancelToken(...) } 传递进 axios 内部,
   * 内部调用 cancelToken.promise.then 等待状态改变,当外部调用方法 cancel 取消请求,
   * pendding 状态就变为 resolve,即取消请求并且抛出 reject(message)
   */
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });
  // 保留 this 指向,内部可调用
  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // 取消过的直接返回
      return;
    }
    // 外部调用 cancel 取消请求方法,Cancel 实例化,保存 message 并增加已取消请求标示
    //  new Cancel(message) 后等于 { message,  __CANCEL__ : true}
    token.reason = new Cancel(message);
    // 上面的 promise 从 pedding 转变为 resolve,并携带 message 传递给 then
    resolvePromise(token.reason);
  });
}
// 挂载静态方法
CancelToken.source = function source() {
  var cancel;
  /**
   * 构造函数 CancelToken 实例化,用回调函数做参数,并且回调函数
   * 接收 CancelToken 内部的函数 c,保存在变量 cancel 中,
   * 后面调用 cancel 即取消请求
  */
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;

总结

上述分析概括成以下几点:

  1. 为了支持 axios() 简洁写法,内部使用 request 函数作为新实例
  2. 使用 promsie 链式调用的巧妙方法,解决顺序调用问题
  3. 数据转换器方法使用数组存放,支持数据的多次传输与加工
  4. 适配器通过兼容浏览器端和 node 端,对外提供统一 api
  5. 取消请求这块,通过外部保留 pendding 状态,控制 promise 的执行时机

参考文献

Github Axios 源码(https://github.com/axios/axios)

Axios 文档说明(http://www.axios-js.com/zh-cn/docs)

一步一步解析Axios源码,从入门到原理(https://blog.csdn.net/qq_27053493/article/details/97462300

本文分享自微信公众号 - 前端技术江湖(bigerfe)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-07-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • axios源码分析

    念念不忘
  • 中了源码的毒,给你一副良药

    近期阿宝哥在团队内搞了一个 「如何读源码」 的专题,主要目的是让团队的小伙伴们了解读源码的思路与技巧。在此期间,阿宝哥也写了 77.9K 的 Axios 项目有...

    若川
  • 看完这篇CopyOnWriteArrayList源码解析,和阿里面试官扯了整整一个小时!

    我们知道 ArrayList 非线程安全,需要自己加锁或者使用 Collections.synchronizedList 包装. 从JDK1.5开始JUC里提...

    JavaEdge
  • 如何实现一个HTTP请求库——axios源码阅读与分析

    在前端开发过程中,我们经常会遇到需要发送异步请求的情况。而使用一个功能齐全,接口完善的HTTP请求库,能够在很大程度上减少我们的开发成本,提高我们的开发效率。

    黄Java
  • 面试官不要再问我axios了?我能手写简易版的axios

    axios作为我们工作中的常用的ajax请求库,作为前端工程师的我们当然是想一探究竟,axios究竟是如何去架构整个框架,中间的拦截器、适配器、 取消请求这些都...

    @超人
  • Axios 如何缓存请求数据?

    大家好,我是若川。欢迎加我微信 ruochuan12,长期交流学习。今天推荐这篇Axios缓存请求数据的文章,相信是常见的业务场景,感兴趣的读者可以看看 umi...

    若川
  • 你不知道的前后端分离之交互(2)

    上一篇文章前后端分离之交互(1)我们讲到了如何使用JQuery发起ajax请求,从后端接口获取前端需要的数据。JQuery封装好的ajax请求确实很好用,对比原...

    创译科技
  • 【JS】625- Axios 如何缓存请求数据?

    在 Axios 如何取消重复请求? 这篇文章中,阿宝哥介绍了在 Axios 中如何取消重复请求及 CancelToken 的工作原理。本文将介绍在 Axios ...

    pingan8787
  • 浅入深出Vue:代码整洁之封装

    这句话来自20世纪中期注明现代建筑大师 路德维希·密斯·范·德·罗所说,他秉承的是少即是多的建筑设计哲学,玻璃幕墙等现代建筑结构便是由他缔造的。

    若羽
  • 全栈“食”代:Django + Nuxt 实现美食分享网站(下)

    在上篇[1]中,我们分别用 Django 和 Nuxt 实现了后端和前端的雏形。在这一部分,我们将实现前后端之间的通信,使得前端可以从后端获取数据,并且将进一步...

    一只图雀
  • nginx upstream模块完整逻辑源码分析

    1.启动upstream。 2.连接上游服务器。 3.向上游发送请求。 4.接收上游响应(包头/包体)。 5.结束请求。

    xiaomei
  • 从项目中由浅入深的学习vue,微信小程序和快应用 (1)

    2.技术栈 vue+vue-router+vuex+axios+element-UI+iconfont(阿里)

    火狼1
  • VUE必会知识(一)---axios基础

    因为是异步操作,有些时候数据不能及时渲染,就要用到$nextTick来拿数据或者forceUpdate更新 上篇文章我们讲过 传送门

    代码哈士奇
  • 【Web技术】920- Axios 如何取消重复请求?

    在 Web 项目开发过程中,我们经常会遇到重复请求的场景,如果系统不对重复的请求进行处理,则可能会导致系统出现各种问题。比如重复的 post 请求可能会导致服务...

    pingan8787
  • 77.9K 的 Axios 项目有哪些值得借鉴的地方

    Axios 是一个基于 Promise 的 HTTP 客户端,同时支持浏览器和 Node.js 环境。它是一个优秀的 HTTP 客户端,被广泛地应用在大量的 W...

    ConardLi
  • 框架源码中用来提高扩展性的设计模式

    我们写的代码都是为了一定的需求服务的,但是这些需求并不是一成不变的,当需求变更了,如果我们代码的扩展性很好,我们可能只需要简单的添加或者删除模块就行了,如果扩展...

    蒋鹏飞
  • Javascript[0x08] -- axios基础应用

    对于axios的学习,大致可以分为三部曲,第一就是基础知识你能够灵魂运用,第二就是能够根据自己项目需要封装一下axios库,第三个就是看源码吧,能看得懂,讲的出...

    璀错
  • 深入解析Node.js中5种发起HTTP请求的方法

    创建HTTP请求使现代编程语言的核心功能之一,也是很多程序员在接触到新的开发环境时最先遇到的技术之一。在Node.js中有相当多的解决方案,其中有语言内置功能,...

    疯狂的技术宅
  • axios请求封装和异常统一处理

    当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异。笔者前几天刚好在负责一个项目的权限管理模块,现在权限管理模块已经做完了,我想通过5-6篇文章,来...

    江南一点雨

扫码关注云+社区

领取腾讯云代金券