|
|
|
|
公众号矩阵

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

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

作者:佚名来源:前端大全|2021-11-24 10:10

 

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

前言

由于axios源码中有很多不是很重要的方法,而且很多方法为了考虑兼容性,并没有考虑到用es6 的语法去写。本篇主要是带你去梳理axios的主要流程,并用es6重写简易版axios

  •  拦截器
  •  适配器
  •  取消请求

拦截器

一个axios实例上有两个拦截器,一个是请求拦截器, 然后响应拦截器。我们下看下官网的用法:添加拦截器

  1. // 添加请求拦截器  
  2. axios.interceptors.request.use(function (config) {  
  3.     // 在发送请求之前做些什么  
  4.     return config;  
  5.   }, function (error) {  
  6.     // 对请求错误做些什么  
  7.     return Promise.reject(error);  
  8.   }); 

移除拦截器

  1. const myInterceptor = axios.interceptors.request.use(function () {/*...*/});  
  2. axios.interceptors.request.eject(myInterceptor); 

其实源码中就是,所有拦截器的执行 所以说肯定有一个forEach方法。

思路理清楚了,现在我们就开始去写吧。代码我就直接发出来,然后我在下面注解。

  1. export class InterceptorManager {  
  2.   constructor() {  
  3.     // 存放所有拦截器的栈  
  4.     this.handlers = []  
  5.   }  
  6.   use(fulfilled, rejected) {  
  7.     this.handlers.push({  
  8.       fulfilled,  
  9.       rejected,  
  10.     })  
  11.     //返回id 便于取消  
  12.     return this.handlers.length - 1  
  13.   } 
  14.   // 取消一个拦截器  
  15.   eject(id) {  
  16.     if (this.handlers[id]) {  
  17.       this.handlers[id] = null  
  18.     }  
  19.   }  
  20.   // 执行栈中所有的hanlder  
  21.   forEach(fn) {  
  22.     this.handlers.forEach((item) => {  
  23.       // 这里为了过滤已经被取消的拦截器,因为已经取消的拦截器被置null  
  24.       if (item) {  
  25.         fn(item)  
  26.       }  
  27.     })  
  28.   }  

拦截器这个类我们已经初步实现了,现在我们去实现axios 这个类,还是先看下官方文档,先看用法,再去分析。

axios(config)

  1. // 发送 POST 请求  
  2. axios({  
  3.   method: 'post',  
  4.   url: '/user/12345',  
  5.   data: {  
  6.     firstName: 'Fred',  
  7.     lastName: 'Flintstone'  
  8.   }  
  9. }); 

axios(url[, config])

  1. // 发送 GET 请求(默认的方法)  
  2. axios('/user/12345'); 

Axios 这个类最核心的方法其实还是 request 这个方法。我们先看下实现吧!

  1. class Axios {  
  2.   constructor(config) {  
  3.     this.defaults = config  
  4.     this.interceptors = {  
  5.       request: new InterceptorManager(),  
  6.       response: new InterceptorManager(),  
  7.     }  
  8.   }  
  9.   // 发送一个请求  
  10.   request(config) {  
  11.     // 这里呢其实就是去处理了 axios(url[,config])  
  12.     if (typeof config == 'string') {  
  13.       config = arguments[1] || {}  
  14.       config.url = arguments[0]  
  15.     } else {  
  16.       configconfig = config || {}  
  17.     }  
  18.     // 默认get请求,并且都转成小写  
  19.     if (config.method) {  
  20.       configconfig.method = config.method.toLowerCase()  
  21.     } else {  
  22.       config.method = 'get'  
  23.     }  
  24.     // dispatchRequest 就是发送ajax请求  
  25.     const chain = [dispatchRequest, undefined]  
  26.     //  发生请求之前加入拦截的 fulfille 和reject 函数  
  27.     this.interceptors.request.forEach((item) => {  
  28.       chain.unshift(item.fulfilled, item.rejected)  
  29.     })  
  30.     // 在请求之后增加 fulfilled 和reject 函数  
  31.     this.interceptors.response.forEach((item) => {  
  32.       chain.push(item.fulfilled, item.rejected)  
  33.     })  
  34.     // 利用promise的链式调用,将参数一层一层传下去  
  35.     let promise = Promise.resolve(config)  
  36.     //然后我去遍历 chain  
  37.     while (chain.length) {  
  38.       // 这里不断出栈 直到结束为止  
  39.       promisepromise = promise.then(chain.shift(), chain.shift())  
  40.     }  
  41.     return promise  
  42.   }  

这里其实就是体现了axios设计的巧妙, 维护一个栈结构 + promise 的链式调用 实现了 拦截器的功能, 可能有的小伙伴到这里还是不是很能理解,我还是给大家画一个草图去模拟下这个过程。

假设我有1个请求拦截器handler和1个响应拦截器handler

一开始我们栈中的数据就两个

这个没什么问题,由于有拦截器的存在,如果存在的话,那么我们就要往这个栈中加数据,请求拦截器顾名思义要在请求之前所以是unshift。加完请求拦截器我们的栈变成了这样。

没什么问题,然后请求结束后,我们又想对请求之后的数据做处理,所以响应拦截的数据自然是push了。这时候栈结构变成了这样:

然后遍历整个栈结构,每次出栈都是一对出栈, 因为promise 的then 就是 一个成功,一个失败嘛。遍历结束后,返回经过所有处理的promise,然后你就可以拿到最终的值了。

adapter

Adapter: 英文解释是适配器的意思。这里我就不实现了,我带大家看一下源码。adapter 做了一件事非常简单,就是根据不同的环境 使用不同的请求。如果用户自定义了adapter,就用config.adapter。否则就是默认是default.adpter。

  1. var adapter = config.adapter || defaults.adapter;  
  2. return adapter(config).then() ...  

继续往下看deafults.adapter做了什么事情:

  1. function getDefaultAdapter() {  
  2.   var adapter;  
  3.   if (typeof XMLHttpRequest !== 'undefined') {  
  4.     // For browsers use XHR adapter  
  5.     adapter = require('./adapters/xhr');  
  6.   } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {  
  7.     // For node use HTTP adapter  
  8.     adapter = require('./adapters/http');  
  9.   }  
  10.   return adapter;  

其实就是做个选择:如果是浏览器环境:就是用xhr 否则就是node 环境。判断process是否存在。从写代码的角度来说,axios源码的这里的设计可扩展性非常好。有点像设计模式中的适配器模式, 因为浏览器端和node 端 发送请求其实并不一样, 但是我们不重要,我们不去管他的内部实现,用promise包一层做到对外统一。所以 我们用axios 自定义adapter 器的时候, 一定是返回一个promise。ok请求的方法我在下面模拟写出。

cancleToken

我首先问大家一个问题,取消请求原生浏览器是怎么做到的?有一个abort 方法。可以取消请求。那么axios源码肯定也是运用了这一点去取消请求。现在浏览器其实也支持fetch请求, fetch可以取消请求?很多同学说是不可以的,其实不是?fetch 结合 abortController 可以实现取消fetch请求。我们看下例子:

  1. const controller = new AbortController();  
  2. const { signal } = controller;  
  3. fetch("http://localhost:8000", { signal }).then(response => {  
  4.     console.log(`Request 1 is complete!`);  
  5. }).catch(e => {  
  6.     console.warn(`Fetch 1 error: ${e.message}`);  
  7. });  
  8. // Wait 2 seconds to abort both requests  
  9. setTimeout(() => controller.abort(), 2000); 

但是这是个实验性功能,可恶的ie。所以我们这次还是用原生的浏览器xhr基于promise简单的封装一下。代码如下:

  1. export function dispatchRequest(config) {  
  2.   return new Promise((resolve, reject) => {  
  3.     const xhr = new XMLHttpRequest()  
  4.     xhr.open(config.method, config.url)  
  5.     xhr.onreadystatechange = function () {  
  6.       if (xhr.status >= 200 && xhr.status <= 300 && xhr.readyState === 4) {  
  7.         resolve(xhr.responseText)  
  8.       } else {  
  9.         reject('失败了')  
  10.       }  
  11.     }  
  12.     if (config.cancelToken) {  
  13.       // Handle cancellation  
  14.       config.cancelToken.promise.then(function onCanceled(cancel) {  
  15.         if (!xhr) {  
  16.           return 
  17.         }  
  18.         xhr.abort()  
  19.         reject(cancel)  
  20.         // Clean up request  
  21.         xhr = null  
  22.       }) 
  23.     }  
  24.     xhr.send()  
  25.   })  

Axios 源码里面做了很多处理, 这里我只做了get处理,我主要的目的就是为了axios是如何取消请求的。先看下官方用法:

主要是两种用法:

使用 cancel token 取消请求

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

还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token:

  1. const CancelToken = axios.CancelToken;  
  2. let cancel;  
  3. axios.get('/user/12345', {  
  4.   cancelToken: new CancelToken(function executor(c) {  
  5.     // executor 函数接收一个 cancel 函数作为参数  
  6.     ccancel = c;  
  7.   })  
  8. });  
  9. // cancel the request  
  10. cancel(); 

看了官方用法 和结合axios源码,我给出以下实现:

  1. export class cancelToken {  
  2.     constructor(exactor) {  
  3.         if (typeof executor !== 'function') {  
  4.         throw new TypeError('executor must be a function.')  
  5.         }  
  6.         // 这里其实将promise的控制权 交给 cancel 函数  
  7.         // 同时做了防止多次重复cancel 之前 Redux 还有React 源码中也有类似的案列  
  8.         const resolvePromise;  
  9.         this.promise =  new Promise(resolve => {  
  10.             resolveresolvePromise = resolve;  
  11.         })  
  12.         this.reason = undefined      
  13.          const cancel  = (message) => {  
  14.             if(this.reason) {  
  15.                 return;  
  16.             }  
  17.             this.reason = 'cancel' + message;  
  18.             resolvePromise(this.reason);  
  19.         }  
  20.         exactor(cancel)  
  21.     }  
  22.     throwIfRequested() {  
  23.         if(this.reason) {  
  24.             throw this.reason  
  25.         }  
  26.     }   
  27.      // source 其实本质上是一个语法糖 里面做了封装  
  28.     static source() {  
  29.         const cancel;  
  30.         const token = new cancelToken(function executor(c) {  
  31.             ccancel = c;  
  32.         });  
  33.         return {  
  34.             token: token,  
  35.             cancel: cancel  
  36.         };  
  37.     }  

截止到这里大体axios 大体功能已经给出。

接下来我就测试下我的手写axios,有没有什么问题?

  1. <script type="module" >  
  2.    import Axios from './axios.js';  
  3.    const config = { url:'http://101.132.113.6:3030/api/mock' }  
  4.    const axios =  new Axios();  
  5.    axios.request(config).then(res => {  
  6.        console.log(res,'0000')  
  7.    }).catch(err => { 
  8.       console.log(err)  
  9.    })  
  10. /script> 

打开浏览器看一下结果:

成功了ok, 然后我来测试一下拦截器的功能,代码更新成下面这样:

  1. import Axios from './axios.js';  
  2. const config = { url:'http://101.132.113.6:3030/api/mock' }  
  3. const axios =  new Axios();  
  4. // 在axios 实例上挂载属性  
  5. const err = () => {}  
  6. axios.interceptors.request.use((config)=> {  
  7.     console.log('我是请求拦截器1')  
  8.     config.id = 1 
  9.     return  config  
  10. },err )  
  11. axios.interceptors.request.use((config)=> {  
  12.     config.id = 2  
  13.     console.log('我是请求拦截器2')  
  14.     return config  
  15. },err)  
  16. axios.interceptors.response.use((data)=> {  
  17.     console.log('我是响应拦截器1',data )  
  18.     data += 1;  
  19.     return data;  
  20. },err)  
  21. axios.interceptors.response.use((data)=> {  
  22.     console.log('我是响应拦截器2',data )  
  23.     return  data  
  24. },err)  
  25. axios.request(config).then(res => {  
  26.     // console.log(res,'0000')  
  27.     // return res;  
  28. }).catch(err => {  
  29.     console.log(err)  
  30. })  console.log(err)}) 

ajax 请求的结果 我是resolve(1) ,所以我们看下输出路径:

没什么问题, 响应后的数据我加了1。

接下来我来是取消请求的两种方式 :

  1. // 第一种方式  
  2. let  cancelFun = undefined 
  3. const cancelInstance = new cancelToken((c)=> 
  4.     ccancelFun = c;  
  5. });  
  6. config.cancelToken = cancelInstance 
  7. // 50 ms 就取消请求  
  8. setTimeout(()=> 
  9.     cancelFun('取消成功')  
  10. },50)  
  11. 第二种方式:  
  12. const { token, cancel }  = cancelToken.source();  
  13. config.cancelToken = token 
  14. setTimeout(()=> 
  15.     cancel()  
  16. },50) 

结果都是OK的,至此axios简单源码终于搞定了。

反思

本篇文章只是把axios源码的大体流程走了一遍, axios源码内部还是做了很多兼容比如:配置优先级:他有一个mergeConfig 方法, 还有数据转换器。不过这些不影响我们对axios源码的整体梳理, 源码中其实有一个createInstance,至于为什么有?我觉得就是为了可扩展性更好, 将来有啥新功能,直接在原有axios的实例的原型链上去增加,代码可维护性强, axios.all spread 都是实例new出来再去挂的,不过都很简单,没啥的。有兴趣大家自行阅读。

鸿蒙官方战略合作共建——HarmonyOS技术社区

【责任编辑:庞桂玉 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

带你轻松入门 RabbitMQ

带你轻松入门 RabbitMQ

轻松入门RabbitMQ
共4章 | loong576

57人订阅学习

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

14人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

42人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微