深入讲解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 contains和compareDocumentPosition 方法来确定是否HTML节点间的关系
Feb 04 Javascript
jquery批量控制form禁用的代码
Aug 06 Javascript
将HTML的左右尖括号等转义成实体形式的两种实现方式
May 04 Javascript
js propertychange和oninput事件
Sep 28 Javascript
javascript实现简单的省市区三级联动
May 14 Javascript
angularJS与bootstrap结合实现动态加载弹出提示内容
Oct 16 Javascript
Vue-Router实现页面正在加载特效方法示例
Feb 12 Javascript
Vue 表单控件绑定的实现示例
Aug 11 Javascript
JavaScript内置对象math,global功能与用法实例分析
Jun 10 Javascript
Layui 导航默认展开和菜单栏选中高亮设置的方法
Sep 04 Javascript
继承行为在 ES5 与 ES6 中的区别详解
Dec 24 Javascript
vue实现瀑布流组件滑动加载更多
Mar 10 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
关于session在PHP5的配置文件中的详细设置参数说明
2011/04/20 PHP
PHP插入排序实现代码
2013/04/04 PHP
解析如何屏蔽php中的phpinfo()函数
2013/06/06 PHP
取得单条网站评论以数组形式进行输出
2014/07/28 PHP
从零开始学YII2框架(三)扩展插件yii2-gird
2014/08/20 PHP
基于swoole实现多人聊天室
2018/06/14 PHP
php中的explode()函数实例介绍
2019/01/18 PHP
用JavaScript实现单继承和多继承的简单方法
2009/03/29 Javascript
js 页面关闭前的出现提示的实现代码
2011/05/25 Javascript
jQuery 选择器项目实例分析及实现代码
2012/12/28 Javascript
Jquery实现页面加载时弹出对话框代码
2013/04/19 Javascript
js给onclick赋值传参数的两种方法
2013/11/25 Javascript
JS实现OCX控件的事件响应示例
2014/09/17 Javascript
Javascript函数式编程简单介绍
2015/10/11 Javascript
JS实现的新浪微博大厅文字内容滚动效果代码
2015/11/05 Javascript
举例说明如何为JavaScript的方法参数设置默认值
2015/11/17 Javascript
jquery拖拽排序简单实现方法(效果增强版)
2016/02/16 Javascript
JavaScript浏览器对象模型BOM(BrowserObjectModel)实例详解
2016/11/29 Javascript
jQuery EasyUI Accordion可伸缩面板组件使用详解
2017/02/28 Javascript
vue绑定设置属性的多种方式(5)
2017/08/16 Javascript
JS实现图片上传多次上传同一张不生效的处理方法
2018/08/06 Javascript
JavaScript设计模式之享元模式实例详解
2019/01/17 Javascript
详解VS Code使用之Vue工程配置format代码格式化
2019/03/20 Javascript
使用Bootstrap做一个朝代历史表
2019/12/10 Javascript
vue实现PC端分辨率适配操作
2020/08/03 Javascript
python将.ppm格式图片转换成.jpg格式文件的方法
2018/10/27 Python
解决python3运行selenium下HTMLTestRunner报错的问题
2018/12/27 Python
使用Python的OpenCV模块识别滑动验证码的缺口(推荐)
2019/05/10 Python
tesserocr与pytesseract模块的使用方法解析
2019/08/30 Python
Python 实现opencv所使用的图片格式与 base64 转换
2020/01/09 Python
兼职业务员岗位职责
2014/01/01 职场文书
便利店投资创业计划书
2014/02/08 职场文书
心理健康日活动总结
2014/05/08 职场文书
Django开发RESTful API实现增删改查(入门级)
2021/05/10 Python
你真的了解PHP中的引用符号(&)吗
2021/05/12 PHP
Windows中Redis安装配置流程并实现远程访问功能
2021/06/07 Redis