当前位置: 首页 > 科技观察

面试官不要再问我axios了?我可以手写一个简单版的axios

时间:2023-03-22 01:46:01 科技观察

作为我们工作中常用的ajax请求库。作为前端工程师,我们当然想知道axios是如何构建整个框架的,包括中间的拦截器、适配器、取消。请求这些是我们经常使用的。前言因为axios源码中有很多方法不是很重要,很多方法为了考虑兼容性,没有考虑用es6语法写。本文主要带大家梳理axios的主要流程,用es6重写简单版axios拦截器适配器取消请求拦截器一个axios实例上有两个拦截器,一个是请求拦截器,一个是响应拦截器.我们看一下官网的用法:addinterceptor//添加请求拦截器axios.interceptors.request.use(function(config){//在发送请求之前做一些事情returnconfig;},function(error){//什么处理请求错误returnPromise.reject(error);});删除拦截器constmyInterceptor=axios.interceptors.request.use(function(){/*...*/});axios.interceptors.request。弹出(我的拦截器);其实在源码中,所有的拦截器都会被执行,所以必须要有一个forEach方法。思路清晰了,下面开始写吧。我直接把代码发出来,然后在下面进行注释。exportclassInterceptorManager{constructor(){//存储所有拦截器的栈this.handlers=[]}use(fulfilled,rejected){this.handlers.push({fulfilled,rejected,})//返回id方便取消returnthis.handlers.length-1}//取消一个拦截器eject(id){if(this.handlers[id]){this.handlers[id]=null}}//执行所有的hanlderforEach(fn){this.handlers.forEach((item)=>{//这里为了过滤已经取消的拦截器,因为已经取消的拦截器设置为nullif(item){fn(item)}})}}Interceptor这个类我们已经初步实现了。现在我们要实现axios类。我们应该先看官方文档,先看用法,再分析。axios(config)//发送POST请求axios({method:'post',url:'/user/12345',data:{firstName:'Fred',lastName:'Flintstone'}});axios(url[,config])//发送GET请求(默认方式)axios('/user/12345');Axios类的核心方法其实就是request方法。先来看看实现吧!classAxios{constructor(config){this.defaults=configthis.interceptors={request:newInterceptorManager(),response:newInterceptorManager(),}}//发送请求request(config){//这里其实是处理axios(url[,config])if(typeofconfig=='string'){config=arguments[1]||{}config.url=arguments[0]}else{configconfig=config||{}}//默认获取请求,并转为小写if(config.method){configconfig.method=config.method.toLowerCase()}else{config.method='get'}//dispatchRequest是发送ajax请求constchain=[dispatchRequest,undefined]//在请求发生前添加拦截的fulfille和reject函数this.interceptors.request.forEach((item)=>{chain.unshift(item.fulfilled,item.rejected)})//在请求发生后添加fulfille和reject函数requestthis.interceptors.response.forEach((item)=>{chain.push(item.fulfilled,item.rejected)})//使用promise的链式调用将参数一一传递下去letpromise=Promise.resolve(config)//然后我去遍历chainwhile(chain.length){//这里一直弹出栈直到结束promisepromise=promise.then(chain.shift(),chain.shift())}returnpromise}}这里其实是体现了axios的巧妙设计,维护一个栈结构+promise的链式调用实现了拦截器的功能,有的朋友可能会来到这里还是不是很理解,我还是画个草图给大家模拟一下这个过程假设我有1个requestinterceptorhandler和1个responseinterceptorhandler一开始我们的stack里面只有两条数据,这是没有问题的,因为有一个拦截器,如果它存在,那么我们需要向堆栈添加数据。顾名思义,请求拦截器必须在请求之前,所以是unshift。添加请求拦截器之后,我们的栈就变成这样了。没有问题,然后请求结束后,我们要对请求后的数据进行处理,那么response中拦截到的数据自然就是push了。这时候栈结构就变成了这样:然后遍历整个栈结构,每次出栈就出一对,因为promise的then代表一个成功一个失败。遍历完成后,返回处理完的promise,即可得到最终的值。adapterAdapter:英文解释是adapter的意思。这里就不实现了,带大家去看源码。适配器做了一件很简单的事情,就是根据不同的环境使用不同的请求。如果用户自定义适配器,使用config.adapter。否则,默认为default.adpter。varadapter=config.adapter||defaults.adapter;returnadapter(config).then()...继续看deafults.adapter做了什么:functiongetDefaultAdapter(){varadapter;if(typeofXMLHttpRequest!=='undefined'){//ForbrowsersuseXHRadapteradapter=require('./adapters/xhr');}elseif(typeofprocess!=='undefined'&&Object.prototype.toString.call(process)==='[objectprocess]'){//FornodeuseHTTPadapteradapter=require('./adapters/http');}returnadapter;}其实就是一个选择:如果是浏览器环境:使用xhr,否则就是node环境。判断进程是否存在。从写代码的角度来看,axios源码的设计扩展性很强。有点像设计模式中的adapter模式,因为浏览器端和node端发送的请求其实是不一样的,不过我们无所谓,我们不关心它的内部实现,使用promise封装层实现对外统一。所以我们在使用axios自定义适配器的时候,必须要返回一个promise。ok请求的方法模拟写在下面。CancleToken先问大家一个问题,原生浏览器如何取消请求?有一个中止方法。可以取消请求。那么axios源码也必须使用这个来取消请求。现在浏览器居然支持fetch请求,fetch取消请求可以吗?很多同学说不可能吧?fetch可以结合abortController来取消fetch请求。让我们看一个例子:constcontroller=newAbortController();const{signal}=controller;fetch("http://localhost:8000",{signal}).then(response=>{console.log(`Request1iscomplete!`);}).catch(e=>{console.warn(`Fetch1error:${e.message}`);});//Wait2secondstoabortbothrequestssetTimeout(()=>controller.abort(),2000);但这是一个实验性功能,可恶即。所以这次我们还是使用原生浏览器的xhr,简单的基于promise进行封装。代码如下:status>=200&&xhr.status<=300&&xhr.readyState===4){resolve(xhr.responseText)}else{reject('failed')}}if(config.cancelToken){//处理取消config.cancelToken.promise.then(functiononCanceled(cancel){if(!xhr){return}xhr.abort()reject(cancel)//Cleanuprequestxhr=null})}xhr.send()})}axios源码做了很多处理,这里我只做Get处理,我的主要目的是axios如何取消请求。先看官方的用法:主要有两种用法:使用canceltoken取消请求constCancelToken=axios.CancelToken;constsource=CancelToken.source();axios.get('/user/12345',{cancelToken:source.token}).catch(function(thrown){if(axios.isCancel(thrown)){console.log('Requestcanceled',thrown.message);}else{//处理错误}});axios.post('/user/12345',{name:'newname'},{cancelToken:source.token})//取消请求(message参数可选)source.cancel('操作被用户取消。');也可以传递一个执行器函数给CancelToken的构造函数来创建一个取消令牌:constCancelToken=axios.CancelToken;letcancel;axios.get('/user/12345',{cancelToken:newCancelToken(functionexecutor(c){//执行器函数接收一个取消函数作为参数ccancel=c;})});//canceltherequestcancel();看了官方的用法,结合axios源码,我给出了如下实现:promise的控制权交给了cancel函数//同时,Redux和React源码在constresolvePromise的源码中也有类似的情况;this.promise=newPromise(resolve=>{resolvesolvePromise=resolve;})this.rseason=undefined;constcancel=(message)=>{if(this.reason){return;}this.reason='cancel'+message;resolvePromise(this.reason);}exactor(cancel)}throwIfRequested(){if(this.reason){throwthis.reason}}//source其实是一个语法糖封装的staticsource(){constcancel;consttoken=newcancelToken(functionexecutor(c){ccancel=c;});return{token:token,cancel:cancel};}}至此,axios的大致功能已经给出接下来测试一下我手写的axios,有什么问题吗?打开浏览器看结果:successok,那我就测试拦截器的功能,代码更新如下:importAxiosfrom'./axios.js';constconfig={url:'http://101.132.113.6:3030/api/mock'}constaxios=newAxios();//挂载属性consterr=()=>{}axios.interceptors.request.use((config)=>{console.log('我是请求拦截器1')config.id=1;axios实例上的returnconfig},err)axios.interceptors.request.use((config)=>{config.id=2console.log('我是请求拦截器2')returnconfig},err)axios.interceptors.response.use((data)=>{console.log('我是响应拦截器1',data)data+=1;returndata;},err)axios.interceptors.response.use((data)=>{console.log('我是响应拦截器2',data)returndata},err)axios.request(config).then(res=>{//console.log(res,'0000')//returnres;}).catch(err=>{console.log(错误)})console.log(错误)})ajax请求的结果是resolve(1),那么我们看一下输出路径:没问题,我在response后的数据上加了1,然后就来了两种取消请求的方式://第一种方式letcancelFun=undefined;constcancelInstance=newcancelToken((c)=>{ccancelFun=c;});config.cancelToken=cancelInstance;//50ms取消请求setTimeout(()=>{cancelFun('取消成功')},50)第二种方式:const{token,cancel}=cancelToken.source();config.cancelToken=token;setTimeout(()=>{cancel()},50)结果都OK,至此简单的源码axios的代码终于完成了。反思这篇文章,我只是走过了axios源代码的一般流程。axios源码还是做了很多兼容,比如:配置优先级:有mergeConfig方法和数据转换器。不过这些都不影响我们对axios源码的整体回顾。源码中其实有一个createInstance。为什么在那里?我认为这是为了更好的可扩展性。以后如果有新的功能,会直接添加到原来的axios实例的原型链中。代码更易于维护。公理。很简单,没什么。有兴趣的可以自行阅读。