上两期我们讲解了axios的源码:axios源码解读-request解读axios源码解读-网络请求今天我们来实现一个简单的axiosfor在Node端实现网络请求,支持一些基本的配置,比如baseURL、url、请求方法、拦截器、取消请求……这个实现的所有源码都放在这里,有需要的可以看看感兴趣的。axios实例本次我们将使用typescript+node来实现相关代码,这样大家看懂代码会更加清晰。这里先实现一个axios类。输入AxiosConfig={url:string;方法:字符串;baseURL:字符串;标头:{[键:字符串]:字符串};参数:{};数据:{};适配器:函数;cancelToken?:number;}classAxios{公共默认值:AxiosConfig;公共创建实例!:功能;构造函数(配置:AxiosConfig){this.defaults=config;this.createInstance=(cfg:AxiosConfig)=>{returnnewAxios({...config,...cfg});};}}constdefaultAxios=newAxios(defaultConfig);导出默认defaultAxios;上面我们主要实现了axios类,使用defaults存放默认配置,声明了createInstance方法。该方法创建一个新的Axios实例,并继承之前Axios实例的配置。请求方式接下来我们向https://mbd.baidu.com/newspage/api/getpcvoicelist发起网络请求,并将响应返回的数据输出到控制台。我们请求的语法如下:importaxiosfrom'./Axios';constservice=axios.createInstance({baseURL:'https://mbd.baidu.com'});(async()=>{constreply=awaitservice.get('/newspage/api/getpcvoicelist');console.log(reply);})();requestmethod让我们首先向我们的Axios类添加一个请求和获取方法。import{dispatchRequest}from'./request';classAxios{//...publicrequest(configOrUrl:AxiosConfig|string,config?:AxiosConfig){if(typeofconfigOrUrl==='string'){config!.url=配置或网址;}else{config=configOrUrl;}constcfg={...this.defaults,...config};返回调度请求(cfg);}publicget(configOrUrl:AxiosConfig|string,config?:AxiosConfig){returnthis.request(configOrUrl,{...(config||{}asany),method:'get'});}}request方法的实现和axios自带的方法区别不大。现在,让我们编辑发出实际请求的dispatchRequest方法。exportconstdispatchRequest=(config:AxiosConfig)=>{const{adapter}=config;返回适配器(配置);};和axios一样,调用配置中的adapter发起网络请求,我们在defaultConfig中配置了默认的adapter。(如下)constdefaultConfig:AxiosConfig={url:'',method:'get',baseURL:'',headers:{},params:{},data:{},adapter:getAdapter()};适配器方法访问接下来,让我们关注我们的适配器实现。//这里偷懒,直接用一个fetch库importfetchfrom'isomorphic-fetch';import{AxiosConfig}from'./defaults';//检查是否是超链接constgetEffectiveUrl=(config:AxiosConfig)=>/^https?/.test(config.url)?config.url:config.baseURL+config.url;//获取查询字符串constgetQueryStr=(config:AxiosConfig)=>{const{params}=config;如果(!Object.keys(params).length)返回'';让queryStr='';for(constkeyinparams){queryStr+=`&${key}=${(paramsasany)[key]}`;}返回config.url.indexOf('?')>-1?queryStr:'?'+queryStr.slice(1);};constgetAdapter=()=>async(config:AxiosConfig)=>{const{method,headers,data}=config;让url=getEffectiveUrl(配置);url+=getQueryStr(配置);constresponse=awaitfetch(url,{method,//只为非GET方法发送正文body:method!=='get'?JSON.stringify(data):null,headers});//组装响应数据constreply={data:awaitresponse.json(),status:response.status,statusText:response.statusText,headers:response.headers,config:config,};返回回复;};导出默认getAdapter;到这里,我们的实现就比较简单简单了,就是几步拼装url发起请求拼装响应数据看效果现在让我们在控制台运行我们的代码,就是下面这样,看控制台输出。importaxiosfrom'./Axios';constservice=axios.createInstance({baseURL:'https://mbd.baidu.com'});(async()=>{constreply=awaitservice.get('/newspage/api/getpcvoicelist');console.log(reply);})();从上图可以看出,我们的axios最基本的功能已经实现了(虽然我们比较懒,用的是fetch)。接下来,让我们提高它的能力。拦截器现在,我想让我的axios能够添加拦截器。我将向请求添加一个拦截器,并为每个请求添加一些自定义标头。我会在响应中添加一个拦截器,直接提取响应数据体(data)和配置信息(config),去除冗余信息。代码实现如下://添加请求拦截器service.interceptors.request.use((config:AxiosConfig)=>{config.headers.test='A';config.headers.check='B';returnconfig;});//添加响应拦截器service.interceptors.response.use((response:any)=>({data:response.data,config:response.config}));改造Axios类,添加拦截器,我们先创建一个InterceptorManager类来管理我们的拦截器。(下)类InterceptorManager{私有处理程序:any[]=[];//注册拦截器publicuse(handler:Function):number{this.handlers.push(handler);返回this.handlers.length-1;}//移除拦截器publiceject(id:number){this.handlers[id]=null;}//获取所有拦截器publicgetAll(){returnthis.handlers.filter(h=>h);}}导出默认拦截器管理器;定义好拦截器之后,我们需要在axios类中添加拦截器——interceptors,如下:classAxios{publicinterceptors:{request:InterceptorManager;响应:拦截器管理器;}constructor(config:AxiosConfig){//...this.interceptors={request:newInterceptorManager(),response:newInterceptorManager()}}}接下来我们在请求方法中处理这些拦截器的调用。(下)publicasyncrequest(configOrUrl:AxiosConfig|string,config?:AxiosConfig){if(typeofconfigOrUrl==='string'){config!.url=configOrUrl;}else{config=configOrUrl;}constcfg={...this.defaults,...config};//将拦截器和真实请求组合在一个数组中constrequestInterceptors=this.interceptors.request.getAll();constresponseInterceptors=this.interceptors.response.getAll();consthandlers=[...requestInterceptors,dispatchRequest,...responseInterceptors];//使用Promises连接数组调用letpromise=Promise.resolve(cfg);while(handlers.length){promise=promise.then(handlers.shift()asany);}returnpromise;}这里的主要目的是将拦截器和真正的请求组合成一个数组,然后使用Promise进行拼接。在这里我发现了一个我还不知道的Promise知识点。在Promise.resolve中,不需要显式返回一个Promise对象。Promise内部会将返回值包装成一个Promise对象,并支持.then语法调用。现在,再次运行我们的代码,看看添加拦截器的效果。(如下图所示)从上图可以看出,返回的内容中只剩下data和config字段(响应拦截器)。并且在配置字段中,您还可以看到我们在请求拦截器中添加的自定义标头也可以使用!取消请求最后我们来实现CancelToken类,这个类是用来取消axios请求的。在实际应用中,我经常使用CancelToken来自动检测重复请求(来自频繁点击),然后取消较早的请求,只将最后一次请求作为有效请求。因此,CancelToken其实是我非常喜欢的一个函数。它的实现本身并不复杂。下面开始实现吧。我们先来看看调用方法。接下来我会在发起请求10ms后取消请求(使用setTimeout)。也就是说,只有在10毫秒内完成的请求才会成功。importaxios,{CancelToken}from'./Axios';//...(async()=>{constsource=CancelToken.source();//10ms后,取消请求setTimeout(()=>{source.cancel('操作被用户取消。');},10);constreply=awaitservice.get('/newspage/api/getpcvoicelist',{cancelToken:source.token});console.log(reply);})();先梳理一下思路吧。首先,我们使用CancelToken.source()获取一个cancelToken,传递给对应的请求函数。接下来,我们应该使用这个令牌来查询请求是否被取消,如果被取消,则抛出错误以结束请求。CancelTokenok,思路清晰了,下面开始实现吧,先从CancelToken开始吧。classCancelErrorextendsError{constructor(...options:any){super(...options);this.name='CancelError';}}classCancelToken{私有静态列表:any[]=[];//每返回一个CancelToken实例一次以取消请求publicstaticsource():CancelToken{constcancelToken=newCancelToken();CancelToken.list.push(cancelToken);返回取消令牌;}//判断是否有message字段是否取消请求publicstaticcheckIsCancel(token:number|null){if(typeoftoken!=='number')returnfalse;constcancelToken:CancelToken=CancelToken.list[token];如果(!cancelToken.message)返回false;//抛出一个CancelError类型,在后续请求中处理该类型的错误thrownewCancelError(cancelToken.message);}公共令牌=0;私信:string='';constructor(){//使用列表的长度作为tokenidthis.token=CancelToken.list.length;}//取消请求,写入消息publiccancel(message:string){this.message=message;}}exportdefaultCancelToken;CancelToken基本完成,主要功能就是用一个CancelToken实例对应一个需要处理的请求,然后在被取消的请求中抛出一个CancelError类型的错误处理CancelError接下来我们需要在对应的请求(dispatchRequest)处添加取消请求检测,最后添加上一个对应的response拦截器可以处理相应的错误。exportconstdispatchRequest=(config:AxiosConfig)=>{//在发起请求之前,检查是否取消请求CancelToken.checkIsCancel(config.cancelToken??null);const{适配器}=配置;returnadapter(config).then((response:any)=>{//请求响应成功后,检查是否取消请求CancelToken.checkIsCancel(config.cancelToken??null);returnresponse;});};由于我们的拦截器实现太粗糙,没有添加失败响应拦截器(这里应该会处理),所以我直接将整个请求包裹在try...catch中。尝试{constreply=awaitservice.get('/newspage/api/getpcvoicelist',{cancelToken:source.token});console.log(reply);}catch(e){if(e.name==='CancelError'){//如果请求被取消,不会抛出任何错误,只有控制台输出提示console.log(`请求被取消,取消原因:${e.message}`);返回;}throwe;}接下来,让我们运行我们的程序并查看控制台输出!(如下图)大功告成!总结至此,我们的axios简易版就完成了。可以用来在Node端实现网络请求,支持一些基本的配置,比如baseURL、url、请求方法、拦截器、取消请求……不过还有很多不完善的地方,有兴趣的朋友可以找下面的源码代码地址,继续往下写。源码地址,建议练习最后的东西。如果你已经看过这篇文章,希望你在离开之前还喜欢它~你的点赞是对作者最大的鼓励,也能让更多人看到这篇文章!如果您觉得本文对您有帮助,请帮忙点亮github上的star,鼓励一下!
