深入讲解xhr(XMLHttpRequest)/jsonp请求之abort


Posted in Javascript onJuly 26, 2017

前言

相信大家在工作中经常需要使用AJAX,所以当大家看到文章标题的时候可能会觉得这是一个老生常谈的话题。

前端开发中向后端发起xhr(XMLHttpRequest)请求(代表性的就是熟悉的ajax)是再正常不过的事。

但在前端开发过程中,不怎么重视xhr的abort(中止掉xhr请求,及表示取消本次请求)。往往会带来一些不可意料的结果。

比如:切换tab,发起xhr请求,渲染同一个列表。就这么简单的拉取数据渲染列表的功能,并且可以根据tab切换。想想应该是很简单。但是假如你只顾着发起xhr请求,而没有abort掉它,想想会发生什么。很有可能就是当前选中的tab数据,并不是你想要的。说白了就是数据错了。这时候你可能就要考虑是不是xhr请求返回数据的顺序问题。

答案是肯定的,xhr请求返回数据顺序是不固定的。所以你要做的就是abort掉你之前的xhr请求,然后再发起一个新的xhr请求。

结合上面所说的例子可以知道xhr使用不当会存在以下问题:

  • 容易出现页面最终数据与状态不一致的问题,这可能再列表筛选是出现的概率比较大。
  • xhr请求达到一定数量之后,浏览器就会显得非常的慢。因为有太多的请求在请求服务器资源。

为了解决上面的问题,我们在进行页面的时候就必须考虑abort掉所有的xhr请求。

那么如何实现xhr的abort方法呢,或者通过何种方式abort掉xhr呢?

一个简单的xhr

我们都知道,现在的框架(例如:jQuery的ajax模块)对xhr都进行了封装,是为了让我们更好的使用xhr。但是也蒙蔽了我们的眼睛。让我们抛开框架,来看看一个简单的xhr怎么实现。

//仅供参考 xhr
function ajax(type ,url , data , successCallBack , errorCallBack){
 let xhr = new XMLHttpRequest();
 xhr.onload = ()=>{
 if(xhr.status === 200){
  return successCallBack(xhr.response||xhr.responseText);
 }
 return errorCallBack('请求失败');
 }
 xhr.onerror = ()=>{
 return errorCallBack('出错了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
}

这就是一个简单的xhr请求的实现,我把它命名为ajax,我们现在可以通过以下方式进行调用:

ajax('get','/test/getUserList' , undefined , function(result){
 console.log('成功了。', result);
} ,function(error){
 console.log(error);
});

如果使用这个方法我们是没办法abort掉xhr请求的。好吧,现在我们把它改造一下,让它支持abort方法:

//仅供参考 xhr.abort
function ajax(type ,url , data , successCallBack , errorCallBack){
 let xhr = new XMLHttpRequest();
 xhr.onload = ()=>{
 if(xhr.status === 200){
  return successCallBack(xhr.response||xhr.responseText);
 }
 return errorCallBack('请求失败');
 }
 xhr.onerror = ()=>{
 return errorCallBack('出错了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
 return xhr;//返回XMLHttpRequest实例对象
}

好像没有什么变化对吧。不错,只要在函数的末尾添加return xhr;将XMLHttpRequest实例对象返回即可。那我们在就已经可以如愿的abort掉xhr请求。

let xhr = ajax('get','/test/getUserList' , undefined , function(result){
 console.log('成功了。', result);
} ,function(error){
 console.log(error);
});
//abort
xhr.abort();

好像我们已经大功告成了。但是问题来了,现在Promise这么好用,为什么不把它加进来呢。像这样没法在我们的Promise链式调用上使用它。

Promise封装xhr

好了,现在的首要任务是封装出一个Promise版的ajax库。首要要确认的就是,ajax方法需要返回的是Promise实例对象,而不再是原生的XMLHttpRequest实例对象。知道了这一点那就可以进行封装了。

//仅供参考 promise
function ajax(type ,url , data ){
 let xhr = new XMLHttpRequest();
 let promise = new Promise(function(resolve , reject){
 xhr.onload = ()=>{
  if(xhr.status === 200){
  return resolve(xhr.response||xhr.responseText);
  }
  return reject('请求失败');
 }
 xhr.onerror = ()=>{
  return reject('出错了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
 });
 return promise;//返回Promise实例对象
}

使用了Promise之后我们不再需要传入回调函数。所以参数减少了。这样我们就可以愉快的进行链式调用了。

let promise = ajax('get','/test/getUserList');
promise.then((result)=>{
 console.log('成功了。', result);
},(error)=>{
 console.log(error);
})

可问题又来了,Promise实例是没有abort方法的。假如我们把ajax方法修改为返回xhr,我们是可以如期调用abort方法杀死请求,但是我们就不能使用Promise带给我们的好处了。

仔细思考,最后一句return promise; 这里是不能改。我们只能另外想办法。

最简单的解决方式就是创建一个xhr和promise的映射关系。也就是每一个promise对应一个唯一的xhr请求。有了思路之后,解决方案就来了。

let map = [];//用于保存promise和xhr之间的映射关系
//仅供参考 promise abort
function ajax(type ,url , data ){
 let xhr = new XMLHttpRequest();
 let promise = new Promise(function(resolve , reject){
 xhr.onload = ()=>{
  if(xhr.status === 200){
  return resolve(xhr.response||xhr.responseText);
  }
  return reject('请求失败');
 }
 xhr.onerror = ()=>{
  return reject('出错了');
 }
 xhr.open(type,url);
 xhr.send(data ? data:null);
 });
 map.push({promise:promise,request:xhr});//创建promise和xhr之间的映射关系,保存到全局的一个数组中。
 return promise;//返回Promise实例对象
}
//abort 请求
function abort(promise){
 for(let i = 0 ; i < map.length ; i++ ){
 if ( map[i].promise === promise ){
  map[i].request.abort();
 }
 }
}

通过在全局创建一个map保存所有的promise和xhr之间的映射关系。这样我们就可以在需要abort请求的时候根据映射关系找到xhr并abort请求。

let promise = ajax('get','/test/getUserList');
promise.then((result)=>{
 console.log('成功了。', result);
},(error)=>{
 console.log(error);
})
abort(promise);

好吧,到这里Promise版的ajax,我们已经实现了。是不是很简单啊。

何为jsonp

假如你还不明白jsonp是何物,那希望下面的篇幅能让你明白。可能你零星的知道跨越请求,但是可能没有在实战中碰到过。那么我们先来看看,一个简单的jsonp函数是怎么实现的吧。

let index = 0;
//仅供参考 jsonp
function jsonp(url,jsonp,successCallback , errorCallback){
 let script = document.createElement('script');
 let result ;
 script.onload = function(){
 successCallback(result);
 }
 script.onerror = function(){
 errorCallback('出错了');
 }
 let callBackName = 'jsonpCallback'+index++;
 script.src=url+(url.indexOf('?') >=0 ? '&':'?')+jsonp+'='+callBackName;
 window[callBackName]=function(){//拿给后端进行输出执行的。
 result = Array.prototype.slice.call(arguments);
 }
 document.head.append(script);
}

jsonp算起来应该就是通过script加载实现的跨域请求。其中重要的就是数据返回的接收,我们需要和后端开发同学协商回调函数的变量名。然后后端获取到回调函数名,并且在返回时把回调函数和数据拼接成字符串返回到前端。前端我们添加一个window对象的函数用于接收数据,在函数执行完成后,就会触发script.onload事件,这样就可以真正执行用户回调函数了。

可能你会觉得有点绕,其实细细的理一下,应该就明白了。

后端其实很简单,只要获取到jsonp函数变量名就可以了。然后把函数和数据拼接成字符串返回即可。

下面我们来看看Node.js中的实现:

let query = ctx.request.query;
let jsonp = query.jsonp;//与后端协商的回调参数
ctx.body = jsonp+'({code:0,msg:"success"})';

这个回调函数并不是用户输入的successCallback,而是jsonp函数内部的window[callBackName] ,为什么要这样。你细想一下JavaScript的作用域应该就会知道。这就好比你在script标签中执行一个函数一样。

有可能我们第一次调用jsonp函数服务器会返回如下结果:

<script >
 //只有这一行是服务器返回的,
 //script标签是document.head.append(script)时候加的
 jsonpCallback0({code:0,msg:"success"});
</script>

所以,得出结论就是:函数必须能通过window对象上访问到。不然执行时就会报错。这就是为什么我们不能直接把用户传入的回调直接用来当成回调接收数据的真正原因。

再次强调:JavaScript作用域。

一次成功的jsonp应该是:添加script标签到head,后端接收到jsonp数据,返回拼接好的函数名和数据字符串,执行window对象上的函数拿到数据,执行script.onload事件,执行成功回调。

jsonp的abort方法何去何从

现在你已经知道了jsonp的原理了。那么如何才能对script加载数据进行abort呢。

犯难的问题来了,script并没有真正的abort方法给我们使用。我们所做的就是尽最大的努力提供类似于abort功能的方法。

思路就是使用Event事件对象。触发script的error监听事件。所以我们得对jsonp函数添加一个trigger辅助函数进行触发error事件。

//[trigger 触发事件]
function trigger(element,event){
 if( !isString(event) ) {
 return;
 }
 if ( element.dispatchEvent ){
 let evt = document.createEvent('Events');// initEvent接受3个参数
 evt.initEvent(event, true, true);
 element.dispatchEvent(evt);
 }else if ( element.fireEvent ){ //IE
 element.fireEvent('on'+event);
 }else{
 element['on'+event]();
 }
}
let index = 0;
//仅供参考 jsonp.abort
function jsonp(url,jsonp,successCallback , errorCallback){
 let script = document.createElement('script');
 let result ;
 script.onload = function(){
 successCallback(result);
 }
 script.onerror = function(){
 errorCallback('出错了');
 }
 let callBackName = 'jsonpCallback'+index++;
 script.src=url+(url.indexOf('?') >=0 ? '&':'?')+jsonp+'='+callBackName;
 window[callBackName]=function(){//拿给后端进行输出执行的。
 result = Array.prototype.slice.call(arguments);
 }
 script.abort = ()=>{
 return trigger(script,'error');
 };
 document.head.append(script);
 return script;
}

我们把Promise也使用进来,那样的话,我们就可以脱离回调地狱了不是吗?

let index = 0;
//仅供参考 jsonp.abort
function jsonp(url,query,jsonp){
 let script = document.createElement('script');
 let result ;
 let promise = new Promise(function(resolve,reject){
 script.onload = function(){
  return resolve(result);
 }
 script.onerror = function(){
  return reject('出错了');
 }
 let callBackName = 'jsonpCallback'+index++;
 script.src=url+(url.indexOf('?') >=0 ? '&':'?')+jsonp+'='+callBackName;
 window[callBackName]=function(){//拿给后端进行输出执行的。
  result = Array.prototype.slice.call(arguments);
 }
 document.head.append(script);
 });
 script.abort = ()=>{
 return trigger(script,'error');
 };
 map.push({promise:promise,request:script});//创建promise和script之间的映射关系,保存到全局的一个数组中。
 return promise;
}

同样的我们套用上面的xhr的abort函数封装。这样我们就大功告成了。基本的功能我们就全部实现了。我们就可以开始进行调用了。

let promise = jsonp('/test/getUserList','jsonp');
promise.then((result)=>{
 console.log('成功了。', result);
},(error)=>{
 console.log(error);
})
abort(promise);

总结

虽然,我们已经完成了封装,但是还有很多的意外没有考虑,要想再实战中运用还必须进行封装和重构。我们必须重视abort方法在xhr/jsonp中的运用,但是也不能滥用,适可而止。存在多层服务器调用时,应该更需要慎重考虑。

要想了解更多,可以参考这是我封装好的一个Promise版本的ajax/jsonp库https://github.com/Yi-love/xhrp,大家也可以通过本地进行下载。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
Javascript学习笔记1 数据类型
Jan 11 Javascript
使用JQ来编写最基本的淡入淡出效果附演示动画
Oct 31 Javascript
JS使用post提交的两种方式
Dec 03 Javascript
解决node.js安装包失败的几种方法
Sep 02 Javascript
BootStrap 实现各种样式的进度条效果
Dec 07 Javascript
Node.js使用gm拼装sprite图片
Jul 04 Javascript
vue中计算属性(computed)、methods和watched之间的区别
Jul 27 Javascript
前端html中jQuery实现对文本的搜索功能并把搜索相关内容显示出来
Nov 14 jQuery
JS逻辑运算符短路操作实例分析
Jul 09 Javascript
node.js读取Excel数据(下载图片)的方法示例
Aug 02 Javascript
vue click.stop阻止点击事件继续传播的方法
Sep 04 Javascript
原生js实现瀑布流效果
Mar 09 Javascript
基于ExtJs在页面上window再调用Window的事件处理方法
Jul 26 #Javascript
Angular中自定义Debounce Click指令防止重复点击
Jul 26 #Javascript
JavaScript利用fetch实现异步请求的方法实例
Jul 26 #Javascript
深入探究angular2 UI组件之primeNG用法
Jul 26 #Javascript
WdatePicker.js时间日期插件的使用方法
Jul 26 #Javascript
关于Stream和Buffer的相互转换详解
Jul 26 #Javascript
JS 60秒后重新发送验证码的实例讲解
Jul 26 #Javascript
You might like
document.getElementById的简写方式(获取id对象的简略写法)
2010/09/10 Javascript
js 创建书签小工具之理论
2011/02/25 Javascript
javascript学习笔记(十五) js间歇调用和超时调用
2012/06/20 Javascript
js监听鼠标点击和键盘点击事件并自动跳转页面
2014/09/24 Javascript
JQuery DIV 动态隐藏和显示的方法
2016/06/23 Javascript
前端程序员必须知道的高性能Javascript知识
2016/08/24 Javascript
JavaScript基于对象去除数组重复项的方法
2016/10/09 Javascript
ES6概念 ymbol.for()方法
2016/12/25 Javascript
JS+CSS实现网页加载中的动画效果
2017/10/27 Javascript
vue.js中npm安装教程图解
2018/04/10 Javascript
ztree加载完成后显示勾选节点的实现代码
2018/10/22 Javascript
[04:48]DOTA2亚洲邀请赛林书豪为VGJ加油
2017/04/01 DOTA
Python 遍历子文件和所有子文件夹的代码实例
2016/12/21 Python
Python爬虫包 BeautifulSoup  递归抓取实例详解
2017/01/28 Python
Python之inspect模块实现获取加载模块路径的方法
2018/10/16 Python
使用python爬取微博数据打造一颗“心”
2019/06/28 Python
python读取并写入mat文件的方法
2019/07/12 Python
详解python中的数据类型和控制流
2019/08/08 Python
对django的User模型和四种扩展/重写方法小结
2019/08/17 Python
Python进程,多进程,获取进程id,给子进程传递参数操作示例
2019/10/11 Python
Python实现桌面翻译工具【新手必学】
2020/02/12 Python
python使用python-pptx删除ppt某页实例
2020/02/14 Python
几款好用的python工具库(小结)
2020/10/20 Python
详解Sticky Footer 绝对底部的两种套路
2017/11/03 HTML / CSS
世界领先的26岁以下学生和青少年旅行预订网站:StudentUniverse
2018/07/01 全球购物
AVI-8手表美国官方商店:AVI-8 USA
2019/04/10 全球购物
Booking.com德国:预订最好的酒店和住宿
2020/02/16 全球购物
100%法国制造的游戏和玩具:Les Jouets Français
2021/03/02 全球购物
名企HR怎样看待求职信
2014/02/23 职场文书
安全技术说明书
2014/05/09 职场文书
未受刑事制裁公证证明
2014/09/20 职场文书
检讨书怎么写
2015/01/23 职场文书
大学军训通讯稿(2016最新版)
2015/12/21 职场文书
幼儿园教师暑期培训心得体会
2016/01/09 职场文书
MySQL Shell的介绍以及安装
2021/04/24 MySQL
HashMap实现保存两个key相同的数据
2021/06/30 Java/Android