解决jQuery使用JSONP时产生的错误


Posted in Javascript onDecember 02, 2015

什么是域,简单来说就是协议+域名或地址+端口,3者只要有任何一个不同就表示不在同一个域。跨域,就是在一个域中访问另一个域的数据。

如果只是加载另一个域的内容,而不需要访问其中的数据的话,跨域是很简单的,比如使用iframe。但如果需要从另一个域加载并使用这些数据的话,就会比较麻烦。为了安全性,浏览器对这种情况有着严格的限制,需要在客户端和服务端同时做一些设置才能实现跨域请求。

JSONP简介
JSONP(JSON with Padding)是一种常用的跨域手段,但只支持JS脚本和JSON格式的数据。顾名思义,JSONP是利用JSON作为垫片,从而实现跨域请求的一种技术手段。其基本原理是利用HTML的<script>标签天生可以跨域这一特点,用其加载另一个域的JSON数据,加载完成后会自动运行一个回调函数通知调用者。此过程需要另一个域的服务端支持,所以这种方式实现的跨域并不是任意的。

JQuery对JSONP的支持
JQuery的Ajax对象支持JSONP方式的跨域请求,方法是将crossDomain参数指定为true并且将dataType参数指定为jsonp[1],或者使用简写形式:getJSON()方法[2]。例如:

// 设置crossDomain和dataType参数以使用JSONP
$.ajax({
 dataType: "jsonp",
 url: "http://www.example.com/xxx",
 crossDomain: true,
 data: {
  
 }
}).done(function() {
 // 请求完成时的处理函数
});

// 使用getJSON
$.getJSON("http://www.example.com/xxx?jsoncallback=?", {
 // 参数
}, function() {
 // 请求完成时的处理函数
});

使用getJSON时,需要在参数中指定jsoncallback=?,这个就是前面所说的回调函数,JQuery会自动以一个随机生成的值(回调函数名)来替换该参数中的问号部分,从而形成jsoncallback=jQueryxxxxxxx这种形式的参数,然后和其他参数一起使用GET方式发出请求。

使用第一种方式时,只要将dataType参数的值指定为jsonp,JQuery就会自动在请求地址后面加上jsoncallback参数,因此无需手动添加。

JQuery跨域请求的缺陷:错误处理
跨域请求可能会失败,比如对方服务器的安全设置拒绝接受来自我方的请求(我方不在对方的信任列表中),或者网络不通,或对方服务器已关闭,或者请求地址或参数不正确导致服务器报错等等。

在JQuery中,当使用ajax或getJSON发送请求后会返回一个jqXHR对象[3]。该对象实现了Promise协议,所以我们可以使用它的done、fail、always等接口来处理回调。例如我们可以用在它的fail回调中进行请求失败时的错误处理:

var xhr = $.getJSON(...);
xhr.fail(function(jqXHR, textStatus, ex) {
  alert('request failed, cause: ' + ex.message);
});

这种方式能够处理“正常的错误”,例如超时、请求被中止、JSON解析出错等等。但它对那些“非正常的错误”,例如网络不通、服务器已关闭等情况的支持并不好。

例如当对方服务器无法正常访问时,在Chrome下你会在控制台看到一条错误信息:

解决jQuery使用JSONP时产生的错误

JQuery不会处理该错误,而是选择“静静地失败”:fail回调不会执行,你的代码也不会得到任何反馈,所以你没有处理这种错误的机会,也无法向用户报告错误。

一个例外是在IE8。在IE8中,当网络无法访问时,<script>标签一样会返回加载成功的信息,所以JQuery无法根据<script>标签的状态来判断是否已成功加载,但它发现<script>标签“加载成功”后回调函数却没有执行,所以JQuery以此判断这是一个“解析错误”(回调代码没有执行,很可能是返回的数据不对导致没有执行或执行失败),因此返回的错误信息将是“xxxx was not called”,其中的xxxx为回调函数的名称。

也就是说,由于IE8(IE7也一样)的这种奇葩特性,导致在发生网络不通等“非正常错误”时,JQuery反而无法选择“静默失败”策略,于是我们可以由此受益,得到了处理错误的机会。例如在这种情况下,上面的例子将会弹出“xxxx was not called”的对话框。

解决方案
当遇到“非正常错误”时,除了IE7、8以外,JQuery的JSONP在较新的浏览器中全部会“静默失败”。但很多时候我们希望能够捕获和处理这种错误。

实际上在这些浏览器中,<script>标签在遇到这些错误时会触发error事件。例如如果是我们自己来实现JSONP的话可以这样:

var ele = document.createElement('script');
ele.type = "text/javascript";
ele.src = '...';
ele.onerror = function() {
  alert('error');
};
ele.onload = function() {
  alert('load');
};
document.body.appendChild(ele);

在新浏览器中,当发生错误时将会触发error事件,从而执行onerror回调弹出alert对话框:

解决jQuery使用JSONP时产生的错误

但是麻烦在于,JQuery不会把这个<script>标签暴露给我们,所以我们没有机会为其添加onerror事件处理器。

下面是JQuery实现JSONP的主要代码:

jQuery.ajaxTransport( "script", function(s) {
 if ( s.crossDomain ) {
  var script,
   head = document.head || jQuery("head")[0] || document.documentElement;
  return {
   send: function( _, callback ) {
    script = document.createElement("script");
    script.async = true;
    ...
    script.src = s.url;
    script.onload = script.onreadystatechange = ...;
    head.insertBefore( script, head.firstChild );
   },
   abort: function() {
    ...
   }
  };
 }
});

可以看到script是一个局部变量,从外部无法获取到。

那有没有解决办法呢?当然有:

  • 自己实现JSONP,不使用JQuery提供的
  • 修改JQuery源码(前提是你不是使用的CDN方式引用的JQuery)
  • 使用本文介绍的技巧

前两种不说了,如果愿意大可以选择。下面介绍另一种技巧。

通过以上源码可以发现,JQuery虽然没有暴露出script变量,但是它却“暴露”出了<script>标签的位置。通过send方法的最后一句:

head.insertBefore( script, head.firstChild );
可以知道这个动态创建的新创建标签被添加为head的第一个元素。而我们反其道而行之,只要能获得这个head元素,不就可以获得这个script了吗?head是什么呢?继续看源码,看head是怎么来的:

head = document.head || jQuery("head")[0] || document.documentElement;
原来如此,我们也用同样的方法获取就可以了,所以补全前面的那个例子,如下:

var xhr = $.getJSON(...);
// for "normal error" and ie 7, 8
xhr.fail(function(jqXHR, textStatus, ex) {
  alert('request failed, cause: ' + ex.message);
});
// for 'abnormal error' in other browsers
var head = document.head || $('head')[0] || document.documentElement; // code from jquery
var script = $(head).find('script')[0];
script.onerror(function(evt) {
  alert('error');
});

这样我们就可以在所有浏览器(严格来说是绝大部分,因为我没有测试全部浏览器)里捕获到“非正常错误”了。

这样捕获错误还有一个好处:在IE7、8之外的其他浏览器中,当发生网络不通等问题时,JQuery除了会静默失败,它还会留下一堆垃圾不去清理,即新创建的<script>标签和全局回调函数。虽然留在那也没什么大的危害,但如果能够顺手将其清理掉不是更好吗?所以我们可以这样实现onerror:

// handle error
alert('error');

// do some clean

// delete script node
if (script.parentNode) {
  script.parentNode.removeChild(script);
}
// delete jsonCallback global function
var src = script.src || '';
var idx = src.indexOf('jsoncallback=');
if (idx != -1) {
  var idx2 = src.indexOf('&');
  if (idx2 == -1) {
  idx2 = src.length;
  }
  var jsonCallback = src.substring(idx + 13, idx2);
  delete window[jsonCallback];
}

这样一来就趋于完美了。

完整代码

function jsonp(url, data, callback) {
  var xhr = $.getJSON(url + '?jsoncallback=?', data, callback);

  // request failed
  xhr.fail(function(jqXHR, textStatus, ex) {
    /*
     * in ie 8, if service is down (or network occurs an error), the arguments will be:
     * 
     * testStatus: 'parsererror'
     * ex.description: 'xxxx was not called' (xxxx is the name of jsoncallback function)
     * ex.message: (same as ex.description)
     * ex.name: 'Error'
     */
    alert('failed');
  });

  // ie 8+, chrome and some other browsers
  var head = document.head || $('head')[0] || document.documentElement; // code from jquery
  var script = $(head).find('script')[0];
  script.onerror = function(evt) {
    alert('error');

    // do some clean

    // delete script node
    if (script.parentNode) {
      script.parentNode.removeChild(script);
    }
    // delete jsonCallback global function
    var src = script.src || '';
    var idx = src.indexOf('jsoncallback=');
    if (idx != -1) {
      var idx2 = src.indexOf('&');
      if (idx2 == -1) {
        idx2 = src.length;
      }
      var jsonCallback = src.substring(idx + 13, idx2);
      delete window[jsonCallback];
    }
  };
}

以上代码在IE8、IE11、Chrome、FireFox、Opera、360下测试通过,其中360是IE内核版本,其他浏览器暂时未测。

希望本文对大家学习,帮助大家解决jQuery使用JSONP时产生的错误。

Javascript 相关文章推荐
JQuery 选择器 xpath 语法应用
May 13 Javascript
javascript实现上传图片并预览的效果实现代码
Apr 11 Javascript
解析js如何获取当前url中的参数值并复制给input
Jun 23 Javascript
JavaScript实现select添加option
Jul 03 Javascript
在AngularJS框架中处理数据建模的方式解析
Mar 05 Javascript
JS实现图片延迟加载并淡入淡出效果的简单方法
Aug 25 Javascript
jQuery实现的无限级下拉菜单功能示例
Sep 12 Javascript
JS设置时间无效问题的解决办法
Feb 18 Javascript
js实现4个方向滚动的球
Mar 06 Javascript
vue2.0 兄弟组件(平级)通讯的实现代码
Jan 15 Javascript
详解React之key的使用和实践
Sep 29 Javascript
Openlayers实现扩散的动态点(水纹效果)
Aug 17 Javascript
jquery实现触发时更新下拉列表内容的方法
Dec 02 #Javascript
jQuery简单实现input文本框内灰色提示文本效果的方法
Dec 02 #Javascript
jquery实现简单文字提示效果
Dec 02 #Javascript
分享15个大家都熟知的jquery小技巧
Dec 02 #Javascript
jQuery实现为控件添加水印文字效果(附源码)
Dec 02 #Javascript
jqGrid中文文档之选项设置
Dec 02 #Javascript
javascript实现自动填写表单实例简析
Dec 02 #Javascript
You might like
php Try Catch异常测试
2009/03/01 PHP
PHP删除HTMl标签的三种解决方法
2013/06/30 PHP
PHP读取大文件的多种方法介绍
2016/04/04 PHP
利用php-cli和任务计划实现订单同步功能的方法
2017/05/03 PHP
javascript实现滑动解锁功能
2014/12/31 Javascript
jquery实现可横向和竖向展开的动态下滑菜单效果
2015/08/24 Javascript
jQuery实现的个性化返回底部与返回顶部特效代码
2015/10/30 Javascript
微信小程序购物商城系统开发系列-工具篇的介绍
2016/11/21 Javascript
Bootstrap modal使用及点击外部不消失的解决方法
2016/12/13 Javascript
JS设计模式之策略模式概念与用法分析
2018/02/05 Javascript
微信小程序用户信息encryptedData详解
2018/08/24 Javascript
jQuery实现的自定义轮播图功能详解
2018/12/28 jQuery
JavaScript数据结构与算法之基本排序算法定义与效率比较【冒泡、选择、插入排序】
2019/02/21 Javascript
Javascript Worker子线程代码实例
2020/02/20 Javascript
js实现淘宝浏览商品放大镜功能
2020/10/28 Javascript
[01:10]DOTA2次级职业联赛 - Fly战队宣传片
2014/12/01 DOTA
python使用pyhook监控键盘并实现切换歌曲的功能
2014/07/18 Python
python构建深度神经网络(DNN)
2018/03/10 Python
windows下python和pip安装教程
2018/05/25 Python
在Python中使用Neo4j的方法
2019/03/14 Python
Python实现的栈、队列、文件目录遍历操作示例
2019/05/06 Python
Python切图九宫格的实现方法
2019/10/10 Python
pandas中read_csv的缺失值处理方式
2019/12/19 Python
python中封包建立过程实例
2021/02/18 Python
HTML5播放实现rtmp流直播
2020/06/16 HTML / CSS
Hotter Shoes美国官网:英国最受欢迎的舒适鞋
2018/08/02 全球购物
俄罗斯首家面向中国消费者的一站式购物网站:Wruru
2020/05/08 全球购物
奥利奥广告词
2014/03/20 职场文书
“六查”、“三学”、“三干”查摆问题整改措施
2014/09/27 职场文书
2014年大学教师工作总结
2014/12/02 职场文书
自主招生自荐信范文
2015/03/04 职场文书
公务员岗前培训心得体会
2016/01/08 职场文书
2016大一新生入学教育心得体会
2016/01/23 职场文书
《伯牙绝弦》教学反思
2016/02/16 职场文书
python turtle绘图命令及案例
2021/11/23 Python
Nginx防盗链与服务优化配置的全过程
2022/01/18 Servers