解决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 相关文章推荐
使用正则替换变量
May 05 Javascript
Javascript 写的简单进度条控件
Jan 22 Javascript
javascript取消文本选定的实现代码
Nov 14 Javascript
根据身份证号自动输出相关信息(籍贯,出身日期,性别)
Nov 15 Javascript
JS的document.all函数使用示例
Dec 30 Javascript
js实现文字截断功能
Sep 14 Javascript
高效的jQuery代码编写技巧总结
Feb 22 Javascript
node通过npm写一个cli命令行工具
Oct 12 Javascript
微信小程序实现文字从右向左无限滚动
Nov 18 Javascript
详解利用eventemitter2实现Vue组件通信
Nov 04 Javascript
vue+vant使用图片预览功能ImagePreview的问题解决
Apr 10 Javascript
javascript实现左右缓动动画函数
Nov 25 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的ASP防火墙
2006/10/09 PHP
laravel 5.3中自定义加密服务的方案详解
2017/05/09 PHP
php 中的closure用法详解
2017/06/12 PHP
详解php协程知识点
2018/09/21 PHP
仿中关村在线首页弹出式广告插件(jQuery版)
2012/05/03 Javascript
js克隆对象、数组的常用方法介绍
2013/09/26 Javascript
Node.js中创建和管理外部进程详解
2014/08/16 Javascript
js使用post 方式打开新窗口
2015/02/26 Javascript
BootStrap3中模态对话框的使用
2017/01/06 Javascript
深入理解angular2启动项目步骤
2017/07/15 Javascript
在Create React App中启用Sass和Less的方法示例
2019/01/16 Javascript
浅谈JavaScript闭包
2019/04/09 Javascript
Net微信网页开发 使用微信JS-SDK获取当前地理位置过程详解
2019/08/26 Javascript
基于vue和websocket的多人在线聊天室
2020/02/01 Javascript
详解webpack的文件监听实现(热更新)
2020/09/11 Javascript
python实现问号表达式(?)的方法
2013/11/27 Python
Python中的测试模块unittest和doctest的使用教程
2015/04/14 Python
Python自动调用IE打开某个网站的方法
2015/06/03 Python
用python制作游戏外挂
2018/01/04 Python
PyQt5实现拖放功能
2018/04/25 Python
用pandas中的DataFrame时选取行或列的方法
2018/07/11 Python
Appium Python自动化测试之环境搭建的步骤
2019/01/23 Python
Django项目后台不挂断运行的方法
2019/08/31 Python
Python中的引用和拷贝实例解析
2019/11/14 Python
Python3监控疫情的完整代码
2020/02/20 Python
基于python实现计算两组数据P值
2020/07/10 Python
Python实现PS滤镜中的USM锐化效果
2020/12/04 Python
matplotlib之pyplot模块之标题(title()和suptitle())
2021/02/22 Python
html5指南-3.如何实现html元素拖拽功能
2013/01/07 HTML / CSS
优衣库澳大利亚官网:UNIQLO澳大利亚
2017/01/18 全球购物
澳大利亚个性化儿童礼品网站:Bright Star Kids
2019/06/14 全球购物
高中生学期学习自我评价
2014/02/24 职场文书
计算机网络专业自荐信
2014/07/04 职场文书
高考诚信考试承诺书
2015/04/29 职场文书
redis复制有可能碰到的问题汇总
2022/04/03 Redis
css弧边选项卡的项目实践
2023/05/07 HTML / CSS