浅谈JSONP跨域请求

第一次听说JSONP跨域请求还是去年刚入职实习的时候,也借那个机会在网上好好了解了下用法,但是对于其如何实现还是不太明白。昨天一同事问我当时对JSONP的使用情况,突然发现一年多没用,对于这个跨域请求技术又忘的差不多了,刚好不知道这个星期该写篇什么方面的文章,那就趁这个机会再把JSONP琢磨琢磨顺便把过程记录下来,免得下次不记得了又得到网上到处找资源查阅。

OK,进入正题!!!!

先看JSONP定义

JSONP(JSON with Padding)是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。由于同源策略,一般来说位于 server1.example.com的网页无法与不是 server1.example.com的服务器沟通,而 HTML 的<script> 元素是一个例外。利用 <script> 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 资料,而这种使用模式就是所谓的 JSONP。用 JSONP 抓到的资料并不是 JSON,而是任意的JavaScript,用 JavaScript 直译器执行而不是用 JSON 解析器解析。
JSONP它是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问(这仅仅是JSONP简单的实现形式)。
——来自百度

同源策略

同源策略是一种约定,也是浏览器本身最核心最基本的一个安全功能。
所谓同源是指协议、域名、端口相同,也就是说同源策略不允许一个站点的某个文档或脚本加载请求另一个站点的文档或脚本,具体看下如下实验:

我新建一个站点作为本地站点,端口号为8080,如下:

再建一个站点作为远程站点,端口号为8085,如下:

我在本地站点中添加两个按钮,一个请求远程站点文档,一个请求本地站点文档,我们来看看效果如何
请求代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//请求远程站点文档
function remoteBtnClick() {
$.get("http://localhost:8085/Remote/jslib/jquery-1.7.2.js",
function(data){
console.log(data);
}
);
}
//请求本地站点文档
function localBtnClick() {
$.get("http://localhost:8080/Local/jslib/jquery-1.7.2.js",
function(data){
console.log(data);
}
);
}

请求响应结果如下:

正如前面说的,根据同源策略浏览器不允许本地站点直接通过Http请求读取另外一个远程站点的资源信息。

看到这,相信大家很快会想到一种情况———引入外部js类库<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0"></script>比如这个引入百度地图JS类库的代码,很明显,这个地图JS类库和我们自己的站点明显不是在同一个域内,对于这种情况我也写一个测试程序验证一下。
我在本地站点8080的页面中添加这样一行代码<script type="text/javascript" src="http://localhost:8085/Remote/jslib/remote.js"></script>以希望在本地页面加载的时候同时加载远程站点8085里的remote.js文件,remote.js文件中的内容如下:

运行代码之后的效果图如下:

很显然,通过这种方式是完全可以读取远程站点的文档信息的,也就是说像这种src属性的加载方式是不受同源策略所约束可以访问任何站点的文档信息,另外像大家用的很多的图片加载标签,也经常通过其src属性加载网络图片。

实现基本的跨域请求

看了上面的例子和JSONP的定义相信大家已经明白了JSONP跨域请求的基本原理,利用script的这种加载方式我们就可以实现跨域请求。

我在本地站点中动态构造一个script标签,然后将其src的url指向远程站点的文档,最后将这个script标签添加到页面dom中,先写个例子看这种方式能不能行得通。

1
2
3
4
//远程站点8085上remote.js中的内容依然和上面一样,不做任何改变
function remoteBtnClick() {
$("<script><//script>").attr("src", "http://localhost:8085/Remote/jslib/remote.js").appendTo("body")
}

运行之后的结果和上面直接通过<script type="text/javascript" src="http://localhost:8085/Remote/jslib/remote.js"></script>加载的效果是一样,这里就不继续贴图,因为原理是一样,很显然结果必然相同,而且也没必要动态写这么麻烦,直接加载岂不是更直接。

接着我们来另外一个情况,既然JSONP定义中说了JSONP拿到的是JSON数据,那我们将远程站点请求文件remote.js换成一个JSON数据文件remote.json({"data":"来自远程站点的数据","time":"2014-12-20"})再试试。

1
2
3
function remoteBtnClick() {
$("<script><//script>").attr("src", "http://localhost:8085/Remote/remote.json").appendTo("body")
}

图1:

图2:

看图1中的请求响应结果,很明显,这个json数据已经从远程站点请求成功并且拿到了本地站点下。通过图2我们发现这里有个javascript的语法错误,因为我们是通过javascript标签的方式加载的,而这个标签是将文档加载完成后会立即把其当做js执行,而这个json数据很明显不是一个合法的js语句。既然这样,那我们就想办法让这个json数据变成一个合法的js语句,最简单的方法就是将这个json数据当做一个函数的参数给塞进去,例如:callbackHandler({"data":"来自远程站点的数据","time":"2014-12-20"}),如果本地站点中有一个callbackHandler函数,那么远程站点返回的这个数据就是一个合法的js函数,很显然这个时候单纯的通过js客户端来验证这个例子是无法实现的,因为在远程站点8085中无论是json文件还是js文件中都无法直接构造callbackHandler({"data":"来自远程站点的数据","time":"2014-12-20"})这么一段代码(语法不正确)给本地站点8080去远程调用。

1
2
3
function callbackHandler(json) {
console.log(json)
}

这个时候就需要远程站点的服务端进行配合,由于远程站点后端服务不知道本地客户端的回调函数名是callbackHandler,所以,我们需要在远程调用的时候告诉服务端本地的回调函数名是callbackHandler,此时的本地站点8080上的请求方式则换这样:

1
2
3
4
5
$("<script><//script>").attr("src", "http://localhost:8085/Remote/JSONPServlet?callback=callbackHandler").appendTo("body")
}
function callbackHandler(data) {
console.log(data);
}

远程站点8085上服务端代码如下:

1
2
3
4
5
6
7
8
9
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String callback = request.getParameter("callback");
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println(callback + "({'data':'The data from remote','time':'2014-12-20'})");
out.flush();
out.close();
}

服务端通过参数解析知道本地站点客户端的回调函数名是callbackHandler,远程服务端构造一段callbackHandler({"data":"The data from remote","time":"2014-12-20"})返回即可,这样在script标签加载完成后会直接将取得的json数据当做参数传入该回调函数中执行,这样整个跨域请求(请求服务端)就完成了。JSONP的跨域请求差不多就这样实现了,但是
运行效果如下:

运行结果正如我们所想,通过script方式加载远程服务返回javascript tags可以顺利实现跨域访问,这里我也继续试验下直接通过ajax方式访问远程后端服务,将remoteBtnClick()实现改为如下方式:

1
2
3
4
5
6
$.get("http://localhost:8085/Remote/JSONPServlet?callback=callbackHandler",
function(data){
console.log(data);
}
);
}

请求结果如下:

显然,请求的结果和前面的请求远程站点客户端文档信息是一样,因为同源策略而无法访问。

OK,JSONP的实现方式和相关验证基本上就爱完成了,现在也知道了JSONP的实现原理和实现方式,但是上面这种实现方式有点麻烦,既要自己添加script标签,同时还要自己定义一个回调函数,感觉略显麻烦,其实jQuery中已经直接提供类似的JSONP请求方式,我们只需要按照其定义好调用方式即可进行Http的跨域请求。现在我将remoteBtnClick()实现方式修改为如下,远程服务端代码不修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function remoteBtnClick() {
$.ajax({
url: 'http://localhost:8085/Remote/JSONPServlet',
dataType: "jsonp",
jsonp: "callback",
jsonpCallback: "callbackHandler",
success: function (data) {
console.log(data);
console.log("success");
}
});
}
//客户端回调
function callbackHandler(json) {
console.log(json);
console.log("callbackHandler");
}

请求结果如下:

可以看到,现在直接通过ajax请求远程站点服务也成功实现了跨域请求,这个是jQuery自己已经帮我们封装好的功能。对于ajax请求中的几个参数我简单说描述下作用:

  1. dataType:’jsonp’,这个是代表当前Http请求为jsonp的请求方式;
  2. jsonp:’callback’,这个代表的是远程服务端接收客户端回调函数名的参数名,即:String callback = request.getParameter("callback")这个参数,ajax请求中jsonp参数的默认值就是callback,这个也可以自己随便更换;
  3. jsonpCallback:’callbackHandler’,这个代表远程服务调用结束后的本地回调函数名,比如上面的代码中的那个客户端回调函数名,这个jsonpCallback的参数值也是可以自己随便定义的,也可以不给这个jsonpCallback参数,其实jQuery会自动为我们生成一个函数和函数名,从上面的结果图中我们可以看到,远程服务调用成功后,既执行了SUCCESS这个回调函数,也执行我们自己定义的callbackHandler这个回调函数,所以我们完全可以使用jQuery给我们生成的回调函数,在调用结束后在SUCCESS回调中做相应的处理即可,如下是不加该参数的调用方式:
    1
    2
    3
    4
    5
    6
    7
    8
    $.ajax({
    url: 'http://localhost:8085/Remote/JSONPServlet',
    dataType: "jsonp",
    jsonp: "callback",
    success: function (data) {
    console.log(data);
    }
    });

上面结果图中的圈中部分就是jQuery为我们自动生成的回调函数名。

OK,JSONP的实现方式及实现原理基本上介绍演示完了,至于jQuery中对于JSONP的实现封装方式等我有时间研究下了再继续吧~~

write by laohu
2015年3月20日


原创文章,本文采用知识共享署名 2.5(中国大陆许可协议)进行许可,欢迎转载,但转载请注明来自ittiger.cn,并保证转载后文章内容的完整性。本人(laohu)保留所有版权相关权利。



评论