深入剖析 Axios 内部运行机制

发表于 2年以前  | 总阅读数:1772 次

前言

说到 JS HTTP 请求,就不得不提 Axios,作为前端网络请求库领域中的霸主,被广泛应用于众多的 web 项目中。

几款热门 HTTP 请求库在 GitHub 上的受欢迎程度

热门 JS HTTP 请求库 特性简介 Star Fork
Axios 基于 Promise,支持浏览器和 node 85.4k 8.3k
Request 不基于 Promise,简化版的 HTTP 25.2k 3.1k
Fetch 基于 Promise,不支持 node 调用 24.8k 3k
Superagent 15.7k 1.3k

虽然大家都是对 XMLHttpRequest 的封装,但是纵观 Axios 的热度,一骑绝尘啊!由此可见,Axios 真的是一个很优秀的开源项目。然而惭愧的是日常开发中总是拿来就用,一直没有静下心来好好拜读一番 Axios 的源码,会不会有很多人跟我一样呢?这里先列举一下 axios 项目的核心目录结构:

lib

└─ adapters

   ├─ http.js // node 环境下利用 http 模块发起请求

   ├─ xhr.js // 浏览器环境下利用 xhr 发起请求

└─ cancel

   ├─ Cancel.js

   ├─ CancelToken.js

   ├─ isCancel.js

└─ core

    ├─ Axios.js // 生成 Axios 实例

    ├─ InterceptorManager.js // 拦截器

    ├─ dispatchRequest.js  // 调用适配器发起请求

    ...

└─ helpers

    ├─ mergeConfig.js // 合并配置

    ├─ ...

├─ axios.js  // 入口文件

├─ defaults.js  // axios 默认配置项

├─ utils.js

简介

Axios 是一个基于 Promise 网络请求库,作用于 node.js 和浏览器中。在服务端它使用原生 node.jshttp模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。特性:

  • 从浏览器创建XMLHttpRequests

  • 从 node.js 创建http请求

  • 支持PromiseAPI

  • 拦截请求和响应

  • 转换请求和响应数据

  • 取消请求

  • 自动转换 JSON 数据

  • 客户端支持防御XSRF

Axios 内部运作流程

接下来我们结合 axios 的运作流程一起来剖析以下几个模块:

  • Axios 构造函数
  • 请求 / 响应拦截器
  • dispatchRequest 派发请求
  • 转换请求 / 响应数据
  • 适配器处理 HTTP 请求

Axios 如何支持不同的使用方式?

使用 axios 发起请求

我们先来回忆一下平时是如何使用 axios 的:

// 方式 1  axios(config)

axios({

    method: 'get',

    url: 'xxx',

    data: {}

});



// 方式 2  axios(url[, config]),默认 get 请求

axios('http://xxx');



// 方式 3 使用别名进行请求

axios.request(config)

axios.get(url[, config])

axios.post(url[, data[, config]])

axios.put(url[, data[, config]])

...



// 方式 4 创建 axios 实例,自定义配置

const instance = axios.create({

  baseURL: 'https://some-domain.com/api/',

  timeout: 1000,

  headers: {'X-Custom-Header': 'foobar'}

});



axios#request(config)

axios#get(url[, config])

axios#post(url[, data[, config]])

axios#put(url[, data[, config]])

...

源码分析

首先来看 axios 的入口文件, lib 目录下的axios.js:

// /lib/axios.js

function createInstance(defaultConfig) {

  // 创建 axios 实例

  var context = new Axios(defaultConfig);

  // 把 instance 指向 Axios.prototype.request 方法

  var instance = bind(Axios.prototype.request, context);

  // 把 Axios.prototype 上的方法扩展到 instance 上,指定上下文是 context

  utils.extend(instance, Axios.prototype, context);

  // 把 context 上的方法扩展到 instance 上

  utils.extend(instance, context);

  // 导出 instance 对象

  return instance;

}

var axios = createInstance(defaults);

// 添加 create 方法,返回 createInstance 函数,参数为自定义配置 + 默认配置

axios.create = function create(instanceConfig) {

  return createInstance(mergeConfig(axios.defaults, instanceConfig));

};



...



module.exports = axios;

// Allow use of default import syntax in TypeScript

module.exports.default = axios;

可见,当我们调用axios()时,实际上是执行了createInstance返回的一个指向Axios.prototype.request的函数;通过添加create方法支持用户自定义配置创建,并且最终也是执行了Axios.prototype.request方法;接下来我们看看Axios.prototype.request的源码是怎么写的:

// /lib/core/Axios.js

// 创建一个 Axios 实例

function Axios(instanceConfig) {

  ...

}

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

  // 判断 config 类型并赋值

  // 方式二:axios('https://xxxx') ,判断参数字符串,则赋值给 config.url

  if (typeof config === 'string') {

    config = arguments[1] || {};

    config.url = arguments[0];

  } else {

  // 方式一:axios({}) ,参数为对象,则直接赋值给 config

    config = config || {};

  }

  ...

}

...

// 方式三 & 方式四

// 遍历为请求设置别名

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

    }));

  };

});

// 遍历为请求设置别名

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

    }));

  };

});

到此,axios 支持了 4 中不同的使用方式,无论哪种使用方式,最后都是执行 Axios 实例上的核心方法:request

请求 / 响应拦截器是如何生效的?

设置拦截器

对于大多数 spa 的项目来说,通常会使用 token 进行用户的身份认证,这就要求每个请求都携带认证信息;接收到服务器信息之后,如果发现用户未登录,需要统一跳转登录页;遇到这种场景,就需要用到 axios 提供的拦截器,以下是拦截器的设置:

 // 添加请求拦截器

axios.interceptors.request.use(function (config) {

  config.headers.token = 'xxx';

  return config;

});



 // 添加响应拦截器

axios.interceptors.response.use(function (response) {

    if(response.code === 401) {

        login()

    }

    return response;

});

源码分析

通过拦截器的使用,可以知道实例 Axios 上添加了interceptors方法,接下来我们看看源码的实现:

// /lib/core/Axios.js

// 每个 Axios 实例上都有 interceptors 属性,该属性上有 request、response 属性,

// 分别都是一个 InterceptorManager 实例,而 InterceptorManager 构造函数就是

// 用来管理拦截器

function Axios(instanceConfig) {

  this.defaults = instanceConfig;

  this.interceptors = {

    request: new InterceptorManager(),

    response: new InterceptorManager()

  };

}



// /lib/core/InterceptorManager.js

function InterceptorManager() {

  this.handlers = []; // 拦截器

}

// 往拦截器里 push 拦截方法

InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {

  this.handlers.push({

    fulfilled: fulfilled,

    rejected: rejected,

    ...

  });

  // 返回当前索引,用于注销指定拦截器

  return this.handlers.length - 1;

};

Axios 与InterceptorManager的关系如图示: 现在我们已经有了拦截器,那么 axios 是如何保证发起请求的顺序执行呢?

  • 请求拦截器 => http 请求 => 响应拦截器

上源码:

// /lib/core/Axios.js

// request 方法中

// 省略部分代码

// 生成请求拦截队列

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 chain = [dispatchRequest, undefined];

Array.prototype.unshift.apply(chain, requestInterceptorChain);

chain.concat(responseInterceptorChain);



promise = Promise.resolve(config);

// 循环 chain ,不断从 chain 中取出设置的任务,通过 Promise 调用链执行

while (chain.length) {

  promise = promise.then(chain.shift(), chain.shift());

}



return promise;

用图示表示一下拦截器过程更清晰: 生成任务队列后,再通过promise.then(chain.shift(), chain.shift())调用 Promise 链去处理设置的任务。这里需要注意一点,请求拦截队列在生成时,是通过Array.unshift(fulfilled, rejected)设置的,也就是说在执行请求拦截时,先设置的拦截方法后执行,后设置的拦截方法先执行。

派发请求 dispatchRequest

源码分析

处理完请求拦截之后,总算开始步入整个请求链路的正轨,也就是上图中任务队列的中间步骤:dispatchRequest派发请求。

// /lib/core/dispatchRequest.js

module.exports = function dispatchRequest(config) {

  // 转换请求数据

  config.data = transformData.call(

    config,

    config.data,

    config.headers,

    config.transformRequest

  );

  ...

  // 适配器 可以自定义适配器,没有自定义,执行axios默认适配器

  var adapter = config.adapter || defaults.adapter;

  // 通过适配器处理 config 配置,返回服务端响应数据 response

  return adapter(config).then(function onAdapterResolution(response) {

    ...

    // 转换响应数据

    response.data = transformData.call(

      config,

      response.data,

      response.headers,

      config.transformResponse

    );

    ...

    return response;

  }, function onAdapterRejection(reason) {

    ...

    return Promise.reject(reason);

  })

}

dispatchRequest中主要做了两件事,先通过transformData对请求数据进行处理,然后定义适配器adapter并执行,通过 .then 方法 对adapter(适配器) resolve 出的响应数据进行处理(transformData)并返回 response,失败返回一个状态为rejected``的 Promise 对象。到此也就明白,当用户调用 axios()时,为什么可以链式调用 Promise 的 .then() 和 .catch() 来处理业务逻辑了。接下来我们从transformData`入手,看看 axios 是如何转换请求和响应数据的。

转换请求 / 响应数据

源码分析

// /lib/core/dispatchRequest.js

config.data = transformData.call(

    config,

    config.data,

    config.headers,

    config.transformRequest

);



// /lib/core/transformData.js

module.exports = function transformData(data, headers, fns) {

    utils.forEach(fns, function transform(fn) {

    data = fn(data, headers);

    });



    return data;

};

通过上述代码可以发现transformData方法主要是遍历config.transformRequest数组中的方法,config.dataconfig.headers作为参数。来看一下transformRequesttranformResponse的定义:

// /lib/default.js

var default = {

  ...

  // 转换请求数据

  transformRequest: [function transformRequest(data, headers) {

    // 判断 data 类型

    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;

    }

    if (utils.isURLSearchParams(data)) {

      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');

      return data.toString();

    }

    // 如果 data 是对象,或 Content-Type 设置为 application/json

    if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {

      // 设置 Content-Type 为 application/json

      setContentTypeIfUnset(headers, 'application/json');

      // 将 data 转换为 json 字符串返回

      return JSON.stringify(data);

    }

    return data;

  }],

  // 转换响应数据

  transformResponse: [function transformResponse(data) {

    ...

    if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {

      try {

        // 将 data 转换为 json 对象并返回

        return JSON.parse(data);

      } catch (e) {

        ...

      }

    }

    return data;

  }],

  ...

}

到此,请求数据和响应数据的转换过程已经结束了,顺便提一下,官方文档介绍的特性之一:自动转换 JSON 数据,应该就是转换过程中的JSON.stringify(data)JSON.parse(data)了;

重写 / 新增转换方法

发现transformRequest方法是default对象上的一个属性,那么我们是不是可以通过自定义配置来改写转换的过程呢?

import axios from 'axios';

// 重写转换请求数据的过程

axios.default.transformRequest = [(data, headers) => {

    ...

    return data

}];

// 增加对请求数据的处理

axios.default.transformRequest.push(

(data, headers) => {

    ...

    return data

});

适配器(adapter)处理请求

dispatchRequest方法做的第二件事:定义adapter,并执行。接下来,我们来揭开adapter的面纱,看看它具体是怎么处理 HTTP 请求的~

源码分析

下面的代码可以看出,适配器是可以自定义的,如果没有自定义,则执行 axios 提供的默认适配器。

// /lib/core/dispatchRequest.js (51行)

var adapter = config.adapter || defaults.adapter;

我们先来分析默认适配器,在default.js中:

function getDefaultAdapter() {

    var adapter;

    // 判断当前环境

    if (typeof XMLHttpRequest !== 'undefined') {

    // 浏览器环境,使用 xhr 请求

    adapter = require('./adapters/xhr');

    } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {

    // node 环境,使用 http 模块

    adapter = require('./adapters/http');

    }

    return adapter;

}

var defaults = {

    ...

    // 定义 adapter 属性

    adapter: getDefaultAdapter(),

    ...

}

可以看到,axios 之所以支持浏览器环境和 node 环境,就是getDefaultAdapter方法进行了环境判断,分别使用xhr 处理浏览器请求http 模块处理 node 请求。官方称之为isomorphic(同构)能力。这里定义了defaults对象,该对象定义了 axios 的一系列默认配置,还记得它是在哪被注入到 axios 中的吗?当然是在入口文件axios.js里了。

// /lib/axios.js

...

var defaults = require('./defaults');

...

function createInstance(defaultConfig) {

  ...

  // 创建 axios 实例

  var context = new Axios(defaultConfig);

  ...

}

var axios = createInstance(defaults);

...

哎呦,串起来了有没有~好的,重新说回到 xhr 请求,本文只分析浏览器环境中 axios 的运行机制,因此接下来,让我们打开./adapters/xhr文件来看一下:

module.exports = function xhrAdapter(config) {

  return new Promise(function dispatchXhrRequest(resolve, reject) {

    ...

    var request = new XMLHttpRequest();

    // 设置完整请求路径

    var fullPath = buildFullPath(config.baseURL, config.url);

    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true) ;

    // 请求超时

    request.timeout = config.timeout;

    request.ontimeout = function handleTimeout() {...}

    // 请求中断

    request.onabort = function handleAbort() {...}

    ...

    request.send(requestData);

  }

}

将 config 中的请求配置进行赋值处理,正式发起XMLHttpRequest请求。

自定义 adapter

通过上面对 adapter 的分析,可以发现如果自定义 adapter 的话,是可以接管 axios 的请求和响应数据的,因此可以自定义 adapter 实现 mock;

const mockUrl = {

    '/mock': {data: xxx}

};

const instance = Axios.create({

    adapter: (config) => {

        if (!mockUrl[config.url]) {

            // 调用默认的适配器处理需要删除自定义适配器,否则会死循环

            delete config.adapter

            return Axios(config)

        }

        return new Promise((resolve, reject) => {

            resolve({

                data: mockUrl[config.url],

                status: 200,

            })

        })

    }

})

结语

篇幅限制,本文只分析了 axios 中的部分源码,诸如 客户端支持防御 XSRF 攻击、取消请求 等模块没有提及,感兴趣的同学可以打开 GitHub 去读一读,相信一定会获益匪浅。

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/EHArzxwW-QJsZk_H72kUNw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
快速配置 Sign In with Apple 4年以前  |  7193次阅读
使用 GPUImage 实现一个简单相机 4年以前  |  5519次阅读
APP适配iOS11 5年以前  |  5492次阅读
 目录