刚出锅的 Axios 网络请求源码阅读笔记,你会吗?

开发 开发工具
项目中一直都有用到 Axios 作为网络请求工具,用它更要懂它,因此为了更好地发挥 Axios 在项目的价值,以及日后能够得心应手地使用它,笔者决定从源码层面好好欣赏一下它的美貌!

[[435862]]

项目中一直都有用到 Axios 作为网络请求工具,用它更要懂它,因此为了更好地发挥 Axios 在项目的价值,以及日后能够得心应手地使用它,笔者决定从源码层面好好欣赏一下它的美貌!

Axios是一款基于 Promise 并可用于浏览器和 Node.js 的网络请求库。

  • Github:https://github.com/axios/axios
  • NPM:https://www.npmjs.com/package/axios
  • Docs:https://axios-http.com/docs/intro

最近,Axios 官方文档终于变好看了,支持多语言切换,阅读更清晰,使用起来也更加舒适!作为一款受全球欢迎的网络请求库,有必要偷学一下其中的架构设计、编码方式。

本篇文章从源码层面主要分析 Axios 的功能实现、设计模式、以及分享 Axios 中一些笔者认为比较“精彩”的地方!

本文主要内容结构如下,大家按需食用:

一、Axios 项目概况

本次分析的 Axios 版本是:v0.24.0

通过简单的浏览 package.json、文件及目录,可以得知 axios 工程采用了如下三方依赖:

名称 说明
Grunt[1] JavaScript 任务运行器
dtslint[2] TypeScript 类型声明&样式校验工具
TypeScript[3] 支持TS环境下开发
Webpack[4] JavaScript 模块打包工具
karma[5] 测试用例检查器
mocha[6] 多功能的 JavaScript 测试框架
sinojs[7] 提供spies, stub, mock,推荐文章《Sinon 入门,看这篇文章就够了[8]
follow-redirects[9] http(s)重定向,NodeJS模块

这里省略了对一些工具介绍,但可以发现,Axios 开发项目的主功能依赖并不多,换句话说是只有 follow-redirects作为了“使用依赖”,其他都是编译、测试、框架层面的东西,可以看出官方团队在对于 Axios 有多么注质量和稳定性,毕竟是全球都在用的工具。

Axios 中相关代码都在 lib/ 目录下(建议逐行阅读):

  1. ├── adapters  // 网络请求,NodeJS 环境使用 NodeJS 的 http 模块,浏览器使用 XHR 
  2. │   ├── README.md 
  3. │   ├── http.js  // Node.js 环境使用 
  4. │   └── xhr.js  // 浏览器环境使用 
  5. ├── helpers  // 一些功能辅助工具函数,看文件名可基本知道干啥的 
  6. │   ├── README.md 
  7. │   ├── bind.js 
  8. │   ├── buildURL.js 
  9. │   ├── combineURLs.js 
  10. │   ├── cookies.js 
  11. │   ├── deprecatedMethod.js 
  12. │   ├── isAbsoluteURL.js 
  13. │   ├── isAxiosError.js 
  14. │   ├── isURLSameOrigin.js 
  15. │   ├── normalizeHeaderName.js 
  16. │   ├── parseHeaders.js 
  17. │   ├── spread.js 
  18. │   └── validator.js 
  19. ├── cancel  // 取消网络请求的处理 
  20. │   ├── Cancel.js  // 取消请求 
  21. │   ├── CancelToken.js  // 取消 Token 
  22. │   └── isCancel.js  // 判断是否取消请求的函数方法 
  23. ├── core  // 核心功能 
  24. │   ├── Axios.js  // Axios 对象 
  25. │   ├── InterceptorManager.js  // 拦截器管理 
  26. │   ├── README.md 
  27. │   ├── buildFullPath.js  // 构造完成的请求 URL 
  28. │   ├── createError.js  // 创建错误,抛出异常 
  29. │   ├── dispatchRequest.js  // 请求分发,用于区分调用 http 还是 xhr 
  30. │   ├── enhanceError.js  // 增强错误??????????????? 
  31. │   ├── mergeConfig.js  // 合并配置参数 
  32. │   ├── settle.js  // 根据请求响应状态,改变 Promise 状态 
  33. │   └── transformData.js  // 数据格式转换 
  34. ├── env  // 无关紧要,没啥用,与发包版本有关 
  35. │   ├── README.md 
  36. │   └── data.js 
  37. ├── defaults.js  // 默认参数/初始化参数配置 
  38. ├── utils.js  // 提供简单的通用的工具函数 
  39. └── axios.js  // 入口文件,初始化并导出 axios 对象 

有了一个简单的代码功能组织架构熟悉后,对于串联 Axios 的功能很有好处,另外,从上述文件和文件夹的命名,很容易让人意识到这是一个什么功能的文件。

“高内聚、低耦合”的真言,在 Axios 中应该算是一个运用得很好的例子。

二、Axios 网络请求流程图

梳理了一张 Axios 发起请求、响应请求的执行流程图,希望可以给大家一个完整流程的概念,便于理解后续的源码分析。

Axios 网络请求流程图

三、Axios API 设计

我们在使用 Axios 的时候,会觉得 Axios 的使用特别方便,其原因就是 Axios 中针对同一功能实现了不同的 API,便于大家在各种场景下的变通扩展使用。

例如,发起一个 GET 请求的写法有:

  1. // 第一种 
  2. axios('https://xxx.com/api/userInfo?uid=1'
  3.  
  4. // 第二种 
  5. axios.get('https://xxx.com/api/userInfo?uid=1'
  6.  
  7. // 第三种 
  8. axios({ 
  9.   method: 'GET'
  10.   url: 'https://xxx.com/api/userInfo?uid=1' 
  11. }) 

Axios 请求的核心方法仅两种:

  1. axios(config) 
  2.  
  3. // or 
  4.  
  5. axios(url[, config]) 

我们知道一个网络请求的方式会有 GET、POST、PUT、DELETE 等,为了使用更加语义化,Axios 对外暴露了别名 API:

  1. axios.request(config) 
  2. axios.get(url[, config]) 
  3. axios.delete(url[, config]) 
  4. axios.head(url[, config]) 
  5. axios.options(url[, config]) 
  6.  
  7. axios.post(url[, data[, config]]) 
  8. axios.put(url[, data[, config]]) 
  9. axios.patch(url[, data[, config]]) 

通过遍历扩展axios对象原型链上的方法:

  1. // Provide aliases for supported request methods 
  2. utils.forEach(['delete''get''head''options'], function forEachMethodNoData(method) { 
  3.   /*eslint func-names:0*/ 
  4.   Axios.prototype[method] = function(url, config) { 
  5.     return this.request(mergeConfig(config || {}, { 
  6.       method: method, 
  7.       url: url, 
  8.       data: (config || {}).data 
  9.     })); 
  10.   }; 
  11. }); 
  12.  
  13. utils.forEach(['post''put''patch'], function forEachMethodWithData(method) { 
  14.   /*eslint func-names:0*/ 
  15.   Axios.prototype[method] = function(url, data, config) { 
  16.     return this.request(mergeConfig(config || {}, { 
  17.       method: method, 
  18.       url: url, 
  19.       data: data 
  20.     })); 
  21.   }; 
  22. }); 

能够如上的直接循环列表赋值,得益于 Axios 将核心的请求功能单独放到了 Axios.prototype.request 方法中,该方法的 TS 定义为:

  1. Axios.request(config: any, ...args: any[]): any 

在其方法(Axios.request())内会对外部传参数类型做判断,并选择组装正确的请求参数:

  1. // 生成规范的 config,抹平 API(函数入参)差异 
  2. if (typeof config === 'string') { 
  3.   // 处理了第一个参数是 url 字符串的情况 request(url[, config]) 
  4.   config = arguments[1] || {}; 
  5.   config.url = arguments[0]; 
  6. else { 
  7.   config = config || {}; 
  8.  
  9. // 合并默认配置 
  10. config = mergeConfig(this.defaults, config); 
  11.  
  12. // 将请求方法转小写字母,默认为 get 方法 
  13. if (config.method) { 
  14.   config.method = config.method.toLowerCase(); 
  15. else if (this.defaults.method) { 
  16.   config.method = this.defaults.method.toLowerCase(); 
  17. else { 
  18.   config.method = 'get'

以此来抹平了各种类型请求以及所需传入参数之间的差异性!

四、Axios 工厂模式创建实例

默认 Axios 导出了一个单例,导出了一个实例化后的单例,所以我们可以直接引入后就可以调用 Axios 的方法。

在某些场景下,我们的项目中可能对接了多个业务方,那么请求中的 base URL 就不一样,因此有没有办法创建多个 Axios 实例?

那就是使用 axios.create([config]) 方法创建多个实例。

考虑到多实例这样的实际需求,Axios 对外暴露了 create() 方法,在 Axios 内部中,往导出的 axios 实例上绑定了用于创建本身实例的工厂方法:

  1. /** 
  2.  * Create an instance of Axios 
  3.  * 
  4.  * @param {Object} defaultConfig The default config for the instance 
  5.  * @return {Axios} A new instance of Axios 
  6.  */ 
  7. function createInstance(defaultConfig) { 
  8.   var context = new Axios(defaultConfig); 
  9.   var instance = bind(Axios.prototype.request, context); 
  10.  
  11.   // Copy axios.prototype to instance 
  12.   utils.extend(instance, Axios.prototype, context); 
  13.  
  14.   // Copy context to instance 
  15.   utils.extend(instance, context); 
  16.  
  17.   // Factory for creating new instances 
  18.   instance.create = function create(instanceConfig) { 
  19.     return createInstance(mergeConfig(defaultConfig, instanceConfig)); 
  20.   }; 
  21.  
  22.   return instance; 

这里的实现值得一说的地方在于:

  1. instance.create = function create(instanceConfig) { 
  2.  
  3. return createInstance(mergeConfig(defaultConfig, instanceConfig)); 
  4.  
  5. }; 

在创建 axios 实例的工厂方法内,绑定工厂方法到实例的 create 属性上。为什么不是在工厂方法外绑定呐?这是我们可能的习惯做法,Axios 之前确实也是这么做的。

为什么挪到了内部?可以看看这条 PR: Allow axios.create(options) to be used recursively[10]

原因简单来说就是,用户自己创建的实例依然可以调用 create 方法创建新的实例,例如:

  1. const axios = require('axios'); 
  2.  
  3. const jsonClient = axios.create({ 
  4.   responseType: 'json' // 该项配置可以在后续创建的实例中复用,而不必重复编码 
  5. }); 
  6.  
  7. const serviceOne = jsonClient.create({ 
  8.   baseURL: 'https://service.one/' 
  9. }); 
  10.  
  11. const serviceTwo = jsonClient.create({ 
  12.   baseURL: 'https://service.two/' 
  13. }); 

这样有助于复用实例的公共参数复用,减少重复编码。

五、网络请求适配器

在文件 ./defaults.js 中生成了默认完整的 Request Config 参数。

其中 config.adapter 字段表明当前应该使用 ./adapters/目录下的 http.js 还是 xhr.js 模块。

  1. // 根据当前使用环境,选择使用的网络请求适配器 
  2. function getDefaultAdapter() { 
  3.   var adapter; 
  4.   if (typeof XMLHttpRequest !== 'undefined') { 
  5.     // For browsers use XHR adapter 
  6.     adapter = require('./adapters/xhr'); 
  7.   } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { 
  8.     // For node use HTTP adapter 
  9.     adapter = require('./adapters/http'); 
  10.   } 
  11.   return adapter; 

这里使用了设计模式中的适配器模式,通过判断不同环境下是否支持方法的方式,选择正确的网络请求模块,便可以实现官网所说的支持 NodeJS 和浏览器环境。

六、转换请求体和响应体数据

这是 Axios 贴在官网的核心功能之一,且提到了可以自动转换响应体内容为 JSON 数据。

默认请求配置中初始化的请求/响应转换器数组。

自动尝试转换响应数据为 JSON 格式

transformRequest 和 transformResponse 字段是一个数组类型,因此我们还可以向其中增加自定义的转换器。

一般来讲我们只会通过复写 transitional 字段来控制响应数据的转换与否,但可以作为扩展 Axios 的一个点,留了口子,这一点考虑得也很到位。

七、请求拦截器&响应拦截器

可以通过拦截器来提前处理请求前和收到响应前的一些处理方法。

7.1 拦截器的使用

拦截器用于在 .then() 和 .catch() 前注入并执行的一些方法。

  1. // 通过 use 方法,添加一个请求拦截器 
  2. axios.interceptors.request.use(function (config) { 
  3.     // 在发送请求前干点啥,.then() 处理之前,比如修改 request config 
  4.     return config; 
  5.   }, function (error) { 
  6.     // 在发起请求发生错误后,.catch() 处理之前干点啥 
  7.     return Promise.reject(error); 
  8.   }); 
  9.  
  10. // 通过 use 方法,添加一个响应拦截器 
  11. axios.interceptors.response.use(function (response) { 
  12.     // 只要响应网络状态码是 2xx 的都会触发 
  13.     // 干点啥 
  14.     return response; 
  15.   }, function (error) { 
  16.     // 状态码不是 2xx 的会触发 
  17.     // 发生错误了,干点啥 
  18.     return Promise.reject(error); 
  19.   }); 

7.2 拦截管理器

Axios 将请求和响应的过程包装成了 Promise,那么 Axios 是如何实现拦截器在 .then() 和 .catch() 执行前执行呐?

可以很容易猜到通过组装一条 Promise 执行链即可!

来看看 Axios 在请求函数中如何实现:

首先是 Axios 对象中初始化了 拦截管理器:

  1. function Axios(instanceConfig) { 
  2.   this.defaults = instanceConfig; 
  3.   this.interceptors = { 
  4.     request: new InterceptorManager(), 
  5.     response: new InterceptorManager() 
  6.   }; 

来到 ./lib/core/InterceptorManager.js 文件下,对于拦截管理器:

  1. // 拦截管理器对象 
  2. function InterceptorManager() { 
  3.   this.handlers = []; 
  4.  
  5. /** 
  6.  * 添加新的管理器,定义了 use 方法 
  7.  * 
  8.  * @param {Function} fulfilled 处理 `Promise` 执行 `then` 的函数方法 
  9.  * @param {Function} rejected 处理 `Promise` 执行 `reject` 的函数方法 
  10.  * 
  11.  * @return {Number} 返回一个 ID 值用于移除拦截器 
  12.  */ 
  13. InterceptorManager.prototype.use = function use(fulfilled, rejected, options) { 
  14.   this.handlers.push({ 
  15.     fulfilled: fulfilled, 
  16.     rejected: rejected, 
  17.     // 默认不同步 
  18.     synchronous: options ? options.synchronous : false
  19.     // 定义是否执行当前拦截器的函数或布尔值 
  20.     runWhen: options ? options.runWhen : null  
  21.   }); 
  22.   return this.handlers.length - 1; // ID 值实际就是当前拦截器的数组索引 
  23. }; 
  24.  
  25. /** 
  26.  * 从栈中移除指定 id 的拦截器 
  27.  * 
  28.  * @param {Number} id use 方法返回的 id 值 
  29.  */ 
  30. InterceptorManager.prototype.eject = function eject(id) { 
  31.   if (this.handlers[id]) { 
  32.     this.handlers[id] = null; // 删除拦截器,但索引会保留 
  33.   } 
  34. }; 
  35.  
  36. /** 
  37.  * 迭代所有注册的拦截器 
  38.  * 该方法会跳过因拦截器被删除而值为 null 的索引 
  39.  * 
  40.  * @param {Function} 调用每个有效拦截器的函数 
  41.  */ 
  42. InterceptorManager.prototype.forEach = function forEach(fn) { 
  43.   utils.forEach(this.handlers, function forEachHandler(h) { 
  44.     if (h !== null) { 
  45.       fn(h); 
  46.     } 
  47.   }); 
  48. }; 

迭代所有注册的拦截器是一个 FIFS(first come first served,先到先服务)队列执行顺序的方法。

7.3 组装拦截器与请求执行链

在 ./lib/core/Axios.js 文件中,Axios 对象定义了 request 方法,其中将网络请求、请求拦截器和响应拦截器组装。

默认返回一个还未执行网络请求的 Promise 执行链,如果设置了同步,则会立即执行请求过程,并返回请求结果的 Promise 对象,也就是官方文档中提到的 Axios 还支持 Promise API。

函数详细的分析,都已经注释在如下代码中:

  1. /** 
  2.  * Dispatch a request 
  3.  * 
  4.  * @param {Object} config 传入的用户自定义配置,并和默认配置 merge 
  5.  */ 
  6. Axios.prototype.request = function request(config) { 
  7.   // 省略 ... 
  8.  
  9.   // 请求拦截器执行链 
  10.   var requestInterceptorChain = []; 
  11.   // 同步请求拦截器 
  12.   var synchronousRequestInterceptors = true
  13.   // 遍历请求拦截器 
  14.   this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { 
  15.     // 判断 runWhen 如果是函数,则执行函数,结果若为 false,则不执行当前拦截器 
  16.     if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { 
  17.       return
  18.     } 
  19.     // 判断当前拦截器是否同步 
  20.     synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; 
  21.     // 插入 requestInterceptorChain 数组首位 
  22.     // 效果:[interceptor.fulfilled, interceptor.rejected, ...] 
  23.     requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); 
  24.   }); 
  25.  
  26.   // 响应拦截器执行链 
  27.   var responseInterceptorChain = []; 
  28.   // 遍历所有的响应拦截器 
  29.   this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { 
  30.     // 插入 responseInterceptorChain 尾部 
  31.     // 效果:[ ..., interceptor.fulfilled, interceptor.rejected] 
  32.     responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); 
  33.   }); 
  34.  
  35.   var promise; 
  36.  
  37.   // 如果非同步 
  38.   // 一般大家在使用 axios.interceptors.request.use 都没有传递第三个配置参数 
  39.   // 所以一般情况下会走这个逻辑 
  40.   if (!synchronousRequestInterceptors) { 
  41.     var chain = [dispatchRequest, undefined]; 
  42.     // 将请求拦截器执行链放到 chain 数组头部 
  43.     Array.prototype.unshift.apply(chain, requestInterceptorChain); 
  44.     // 将响应拦截器执行链放到 chain 数组末尾 
  45.     chain = chain.concat(responseInterceptorChain); 
  46.     // 给 promise 赋值 Promise 对象,并注入 request config 
  47.     promise = Promise.resolve(config); 
  48.     // 循环 chain 数组,组合成 Promise 执行链 
  49.     while (chain.length) { 
  50.       // 正好 resolve 和 reject 对应方法,两两一组 
  51.       promise = promise.then(chain.shift(), chain.shift()); 
  52.     } 
  53.     // 返回 Promise 执行链 
  54.     return promise; 
  55.   } 
  56.  
  57.   // 同步方式 
  58.   var newConfig = config; 
  59.   // 循环并执行所有请求拦截器 
  60.   while (requestInterceptorChain.length) { 
  61.     var onFulfilled = requestInterceptorChain.shift(); 
  62.     var onRejected = requestInterceptorChain.shift(); 
  63.     try { 
  64.       // 执行定义请求前的“请求拦截器” then 处理方法 
  65.       newConfig = onFulfilled(newConfig); 
  66.     } catch (error) { 
  67.       // 执行定义请求前的“请求拦截器” catch 处理方法 
  68.       onRejected(error); 
  69.       break; 
  70.     } 
  71.   } 
  72.  
  73.   try { 
  74.     // 执行网络请求 
  75.     promise = dispatchRequest(newConfig); 
  76.   } catch (error) { 
  77.     return Promise.reject(error); 
  78.   } 
  79.  
  80.   // 循环并执行所有响应拦截器 
  81.   while (responseInterceptorChain.length) { 
  82.     promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift()); 
  83.   } 
  84.   // 返回 Promise 对象 
  85.   return promise; 
  86. }; 

可以看到由于请求拦截器和响应拦截器使用了 unshift 和 push,那么 use 拦截器的先后顺序就有变动。

通过如上代码的分析,可以得知若有多个拦截器的执行顺序规则是:

  • 请求拦截器:先 use,后执行
  • 响应拦截器:先 use,先执行

关于拦截器执行这部分,涉及到一个 PR改动: Requests unexpectedly delayed due to an axios internal promise[11],推荐大家阅读一下,有助于熟悉微任务和宏任务。

改动的原因:如果请求拦截器中存在一些长时间的任务,会使得使用 axios 的网络请相较于不使用 axios 的网络请求会延后,为此,通过为拦截管理器增加 synchronous 和 runWhen 字段,来实现同步执行请求方法。

八、取消网络请求

在网络请求中,会遇到许多非预期的请求取消,当然也有主动取消请求的时候,例如,用户获取 id=1 的新闻数据,需要耗时 30s,用户等不及了,就返回查看 id=2 的新闻详情,此时我们可以在代码中主动取消 id=1 的网络请求,节省网络资源。

8.1 如何取消 Axios 请求

通过 CancleToken.source() 工厂方法创建取消请求的实例 source

在发起请求的 request Config 中设置 cancelToken 值为 source.token

在需要主动取消请求的地方调用:source.cancle()

  1. const CancelToken = axios.CancelToken; 
  2. const source = CancelToken.source(); 
  3.  
  4. axios.get('/user/12345', { 
  5.   cancelToken: source.token 
  6. }).catch(function (thrown) { 
  7.   if (axios.isCancel(thrown)) { 
  8.     console.log('Request canceled', thrown.message); 
  9.   } else { 
  10.     // handle error 
  11.   } 
  12. }); 
  13.  
  14. axios.post('/user/12345', { 
  15.   name'new name' 
  16. }, { 
  17.   cancelToken: source.token 
  18. }) 
  19.  
  20. // 主动取消请求 (提示信息是可选的参数) 
  21. source.cancel('Operation canceled by the user.'); 

同一个 source 实例调用取消 cancle() 方法时,会取消所有含有当前实例 source.token 的请求。

8.2 取消请求功能的原理

想必大家也很好奇是怎么实现取消网络请求功能的,实际上有了上述的基础,把 Axios 的请求想象成为一条事件执行链,执行链中任意一处发生了异常,都会中断整个请求。

整个请求执行链中的设计了,首先来看:axios.CancelToken.source():

  1. /** 
  2.  * Returns an object that contains a new `CancelToken` and a function that, when called, 
  3.  * cancels the `CancelToken`. 
  4.  */ 
  5. CancelToken.source = function source() { 
  6.   var cancel; 
  7.   var token = new CancelToken(function executor(c) { 
  8.     cancel = c; 
  9.   }); 
  10.   return { 
  11.     token: token, 
  12.     cancel: cancel 
  13.   }; 
  14. }; 

该工厂方法返回了一个对象,该对象包含了一个 token(取消令牌,CancleToken 对象的实例),以及一个取消与 token 映射绑定的取消请求方法 cancle()。

其中 new CancelToken() 会创建 CancleToken 的单例,通过传入函数方式,拿到了取消请求的回调函数,该函数内会构造 token 取消的原因,并通过执行 resolvePromise(),主动 reslove。

同样是一个微任务,当主动调用 cancle() 方法后,会调用 resolvePromise(reason),此时就会给当前 cancleToken 实例的 reason 字段赋值“请求取消的原因”:

  1. function CancelToken(executor) { 
  2.   if (typeof executor !== 'function') { 
  3.     throw new TypeError('executor must be a function.'); 
  4.   } 
  5.  
  6.   // 初始化一个 promise 属性,resolvePromise 变量指向 resolve 
  7.   var resolvePromise; 
  8.   this.promise = new Promise(function promiseExecutor(resolve) { 
  9.     resolvePromise = resolve; 
  10.   }); 
  11.  
  12.   // 赋值 token 为当前对象的实例 
  13.   var token = this; 
  14.  
  15.   // 省略... 
  16.  
  17.   // 执行外部传入的初始化方法,将取消请求的方法,赋值给返回对象的 cancel 属性 
  18.   executor(function cancel(message) { 
  19.     if (token.reason) { 
  20.       // Cancellation has already been requested 
  21.       return
  22.     } 
  23.  
  24.     token.reason = new Cancel(message); 
  25.     resolvePromise(token.reason); 
  26.   }); 

在 ./lib/core/dispatchRequest.js 文件中:

  1. function throwIfCancellationRequested(config) { 
  2.   // 当 request config 中有实例化 cancelToken 时 
  3.   // 执行 throwIfRequested() 方法 
  4.   // throwIfRequested() 方法在 cancleToken 实例的 reason 字段有值时 
  5.   // 抛出异常 
  6.   if (config.cancelToken) { 
  7.     config.cancelToken.throwIfRequested(); 
  8.   } 
  9.   // 判断 config.signal.aborted 值为真的时候抛出异常 
  10.   // 该值时通过 new AbortController().signal,不过目前暂时未用到 
  11.   // 官方文档上暂也暂未更新相关内容 
  12.   if (config.signal && config.signal.aborted) { 
  13.     throw new Cancel('canceled'); 
  14.   } 
  15.  
  16. module.exports = function dispatchRequest(config) { 
  17.   // 准备发起请求前检查 
  18.   throwIfCancellationRequested(config); 
  19.    
  20.   // 省略... 
  21.    
  22.   var adapter = config.adapter || defaults.adapter; 
  23.   return adapter(config).then(function onAdapterResolution(response) { 
  24.     // 请求成功后检查 
  25.     throwIfCancellationRequested(config); 
  26.     // 省略... 
  27.     return response; 
  28.   }, function onAdapterRejection(reason) { 
  29.     if (!isCancel(reason)) { 
  30.       // 请求发生错误时候检查 
  31.       throwIfCancellationRequested(config); 
  32.       // 省略... 
  33.     } 
  34.     // 省略... 
  35.  
  36.     return Promise.reject(reason); 
  37.   }); 

在文章前边分析拦截器的时候讲到了 dispatchRequest() 在请求拦截器之后执行。

在请求前,请求成功、失败后三个时机点,都会通过 throwIfCancellationRequested() 函数检查是否取消了请求,throwIfCancellationRequested() 函数判断了 cancleToken.reason 是否有值,如果有则抛出异常并中断请求 Promise 执行链。

九、CSRF 防御

Axios 支持防御 CSRF(Cross-site request forgery,跨站请求伪造)攻击,而防御 CSRF 攻击的最简单方式就是加 Token。

CSRF 的攻击可以简述为:服务器错把攻击者的请求当成了正常用户的请求。

加一个 Token 为什么就能解决呐?首先 Token 是服务端随用户每次请求动态生成下发的,用户在提交表单、查询数据等行为的时候,需要在网络请求体加上这个临时性的 Token 值,攻击者无法在三方网站中获取当前 Token,因此服务端就可以通过验证 Token 来区分是否是正常用户的请求。

Axios 在请求配置中提供了两个字段:

  1. // cookie 中携带的 Token 名称,通过该名称可以从 cookie 中拿到 Token 值 
  2. xsrfCookieName: 'XSRF-TOKEN'
  3. // 请求 Header 中携带的 Token 名称,通过该成名可从 Header 中拿到 Token 值 
  4. xsrfHeaderName: 'X-XSRF-TOKEN'

用于附加验证防御 CSRF 攻击的 Token。

十、值得一说的自定义工具库

在 Axios 内,没有引入其他例如 lodash 的工具函数依赖,都在自己内部按需实现了工具函数,提供给整个项目使用。

个人非常喜欢这种做法,尤其是在一个 ES5 的工具库下,这样做不仅代码易读,与此同时还显得非常得纯粹、干净、清晰!

如果团队内有这种诉求,建议可以写一个 ESM 模块的工具库,这样做以后,在打包 Tree Shaking 时,打包的结果应该能更加干净。

总结

总体来说,Axios 涉及到的设计模式就有:单例模式、工厂模式、职责链模式、适配器模式,因此绝对是值得学习的一个工具库,梳理之后不仅利于我们灵活使用其 API,更有助于根据业务去自定义扩展封装网络请求,将网络请求统一收口。 

与此同时,Axios 绝对是一个可以作为软件工程编码的学习范本,其中的文件夹结构,功能设计,功能解耦,按需封装工具类,以及灵活运用设计模式都是值得揣度回味。

责任编辑:武晓燕 来源: DYBOY
相关推荐

2021-11-22 16:12:34

Axios Axios-Retry前端

2013-12-24 10:05:04

memcached

2018-07-30 16:31:00

javascriptaxioshttp

2018-12-29 14:14:32

2019-05-07 15:49:27

AI人工智能艺术

2010-07-13 10:40:30

唐骏

2021-08-19 15:36:09

数据备份存储备份策略

2014-07-03 15:40:09

Apache Spar

2024-03-29 12:50:00

项目分层模型

2022-03-15 08:36:46

递归查询SQL

2021-02-15 14:48:31

Hive语法sql

2024-02-22 08:31:26

数据恢复工具MySQL回滚SQL

2021-04-14 06:53:52

C# 修饰符 Public

2021-04-16 15:02:11

CAP理论分布式

2012-06-20 10:47:25

Team Leader

2012-06-20 15:01:25

iOS开发

2023-02-27 10:45:16

2021-09-26 06:43:07

封装网络请求

2018-11-16 16:35:19

Java源码编程语言

2017-03-16 11:39:33

Openstack源码姿势
点赞
收藏

51CTO技术栈公众号