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

什么是域,简单来说就是协议+域名或地址+端口,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]。例如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 设置crossDomain和dataType参数以使用JSONP
$.ajax({
  dataType: "jsonp" ,
  crossDomain: true ,
  data: {
   
  }
}).done( function () {
  // 请求完成时的处理函数
});
 
// 使用getJSON
  // 参数
}, function () {
  // 请求完成时的处理函数
});

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

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

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

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

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

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

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

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的话可以这样:

?
1
2
3
4
5
6
7
8
9
10
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不会把这个<script>标签暴露给我们,所以我们没有机会为其添加onerror事件处理器。

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
原来如此,我们也用同样的方法获取就可以了,所以补全前面的那个例子,如下:

?
1
2
3
4
5
6
7
8
9
10
11
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:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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];
}

这样一来就趋于完美了。

完整代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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时产生的错误。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值