在项目中遇到一个jsonp跨域的问题,因而仔细的研究了一番jsonp跨域的原理。搞明白了一些之前不是很懂的地方,好比:javascript
1)jsonp跨域只能是get请求,而不能是post请求;css
2)jsonp跨域的原理究竟是什么;html
3)除了jsonp跨域以外还有那些方法绕过“同源策略”,实现跨域访问;html5
4)jsonp和ajax,或者说jsonp和XMLHttpRequest是什么关系;java
虽然 jsonp 的实现跟 ajax 没有半毛钱关系,jsonp是经过 script的src实现的(具体看后面的解析),可是最终目的都是向服务器请求数据而后回调,并且为了方便,因此 jQuery把 jsonp 也封装在了 $.ajax 方法中,调用方式与 ajax 调用方式略有区别。jquery
等等。ajax
1.同源策略json
说到跨域,首先要明白“同源策略”。同源是指:js脚本只能访问或者请求相同协议,相同域名(网址/ip),相同端口的页面。跨域
咱们知道,js脚本能够访问所在页面的全部元素。经过ajax技术,js也能够访问同一协议,同一个domain(ip),同一端口的服务器上的其余页面,请求到浏览器端以后,利用js就能够进行任意的访问。可是对于协议不一样, 或者domain不一样或者端口不一样的服务器上的页面就无能为力了,彻底不能进行请求。浏览器
下面在本地搭建两个tomcat,分别将端口设为8080,和8888,进行相关实验。显然他们的端口是不一样的。演示以下:
http://localhost:8888/html4/ajax.html的代码以下:
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
|
<!
doctype
html>
<
html
>
<
head
>
<
meta
charset="utf-8">
<
meta
name="keywords" content="jsonp">
<
meta
name="description" content="jsonp">
<
title
>jsonp</
title
>
<
style
type="text/css">
*{margin:0;padding:0;}
a{display:inline-block;margin:50px 50px;}
</
style
>
</
head
>
<
body
>
<
a
href="javascript:;" onclick="myAjax();">click me</
a
>
<
script
type="text/javascript" src="js/jquery-1.11.1.min.js"></
script
>
<
script
type="text/javascript">
function myAjax(){
var xmlhttp;
if(window.XMLHttpRequest){
xmlhttp = new XMLHttpRequest();
}else{
xmlhttp = ActionXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange = function(){
if (xmlhttp.readyState==4 && xmlhttp.status==200){
console.log(xmlhttp.responseText);
}
}
var url = "http://localhost:8080/minisns/json.jsp" + "?r=" + Math.random();
xmlhttp.open("Get", url, true);
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlhttp.send();
}
</
script
>
</
body
>
</
html
>
|
这里为告终果不受其余js库的干扰,使用了原生的XMLHttpRequest来处理,结果以下:
咱们看到8080端口的js的ajax请求没法访问8888端口的页面。缘由是“同源策略不容许读取”。
既然普通的ajax不能访问,那么怎样才能访问呢?你们都知道,使用jsonp啊,那jsonp的原理是什么呢?他为何能跨域呢?
2.jsonp跨域的原理
咱们知道,在页面上有三种资源是能够与页面自己不一样源的。它们是:js脚本,css样式文件,图片,像taobao等大型网站,很定会将这些静态资源放入cdn中,而后在页面上链接,以下所示,因此它们是能够连接访问到不一样源的资源的。
1)<script type="text/javascript" src="某个cdn地址" ></script>
2)<link type="text/css" rel="stylesheet" href="某个cdn地址" />
3)<img src="某个cdn地址" alt=""/>
而jsonp就是利用了<script>标签能够连接到不一样源的js脚本,来到达跨域目的。当连接的资源到达浏览器时,浏览器会根据他们的类型来采起不一样的处理方式,好比,若是是css文件,则会进行对页面 repaint,若是是img 则会将图片渲染出来,若是是script 脚本,则会进行执行,好比咱们在页面引入了jquery库,为何就可使用 $ 了呢?就是由于 jquery 库被浏览器执行以后,会给全局对象window增长一个属性: $ ,因此咱们才能使用 $ 来进行各类处理。(另外为何要通常要加css放在头部,而js脚本放在body尾部呢,就是为了减小repaint的次数,另外由于js引擎是单线程执行,若是将js脚本放在头部,那么在js引擎在执行js代码时,会形成页面暂停。)
利用 页面上 script 标签能够跨域,而且其 src 指定的js脚本到达浏览器会执行的特性,咱们能够进行跨域取得数据。咱们用一个例子来讲明:
(1)访问js
8888端口的html4项目中的jsonp.html页面代码以下:
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
|
<!
doctype
html>
<
html
>
<
head
>
<
meta
charset="utf-8">
<
meta
name="keywords" content="jsonp">
<
meta
name="description" content="jsonp">
<
title
>jsonp</
title
>
</
head
>
<
body
>
<
script
type="text/javascript" src="js/jquery-1.11.1.js"></
script
>
<
script
type="text/javascript">
var url = "http://localhost:8080/html5/jsonp_data.js";
// 建立script标签,设置其属性
var script = document.createElement('script');
script.setAttribute('src', url);
// 把script标签加入head,此时调用开始
document.getElementsByTagName('head')[0].appendChild(script);
function callbackFun(data)
{
console.log(data.age);
console.log(data.name);
}
</
script
>
</
body
>
</
html
>
|
其访问的8080端口的html5项目中的jsonp_data.js代码以下:
1
|
callbackFun({
"age"
:100,
"name"
:
"yuanfang"
})
|
将两个tomcate启动,用浏览器访问8888端口的html4项目中的jsonp.html,结果以下:
上面咱们看到,咱们从8888 端口的页面经过 script 标签成功 的访问到了8080 端口下的jsonp_data.js中的数据。这就是 jsonp 的基本原理,利用script标签的特性,将数据使用json格式用一个函数包裹起来,而后在进行访问的页面中定义一个相同函数名的函数,由于 script 标签src引用的js脚本到达浏览器时会执行,而咱们有定义了一个同名的函数,因此json格式的数据,就作完参数传递给了咱们定义的同名函数了。这样就完成了跨域数据交换。jsonp的含义是:json with padding,而在json数据外包裹它的那个函数,就是所谓的 padding 啦^--^
(2)访问servlet---比较实用的例子
8080端口的html5项目中定义一个servlet:
package com.tz.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.alibaba.fastjson.JSON; @WebServlet("/JsonServlet") public class JsonServlet extends HttpServlet { private static final long serialVersionUID = 4335775212856826743L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String callbackfun = request.getParameter("mycallback"); System.out.println(callbackfun); // callbackFun response.setContentType("text/json;charset=utf-8"); User user = new User(); user.setName("yuanfang"); user.setAge(100); Object obj = JSON.toJSON(user); System.out.println(user); // com.tz.servlet.User@164ff87 System.out.println(obj); // {"age":100,"name":"yuanfang"} callbackfun += "(" + obj + ")"; System.out.println(callbackfun); // callbackFun({"age":100,"name":"yuanfang"}) response.getWriter().println(callbackfun); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); } }
在8888端口的html4项目中的jsonp.html来以下的跨域访问他:
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
42
43
44
|
<!
doctype
html>
<
html
>
<
head
>
<
meta
charset="utf-8">
<
meta
name="keywords" content="jsonp">
<
meta
name="description" content="jsonp">
<
title
>jsonp</
title
>
<
style
type="text/css">
*{margin:0;padding:0;}
div{width:600px;height:100px;margin:20px auto;}
</
style
>
</
head
>
<
body
>
<
div
>
<
a
href="javascript:;">jsonp测试</
a
>
</
div
>
<
script
type="text/javascript" src="js/jquery-1.11.1.js"></
script
>
<
script
type="text/javascript">
function callbackFun(data)
{
console.log(111);
console.log(data.name);
//data.age = 10000000;
//alert(0000);
}
$(function(){
$("a").on("click", function(){
$.ajax({
type:"post",
url:"http://localhost:8080/html5/JsonServlet",
dataType:'jsonp',
jsonp:'mycallback',
jsonpCallback:'callbackFun',
success:function(data) {
console.log(2222);
console.log(data.age);
}
});
})
});
</
script
>
</
body
>
</
html
>
|
结果以下:
咱们看到,咱们成功的跨域取到了servlet中的数据,并且在咱们指定的回调函数jsonpCallback:'callbackFun' 和 sucess 指定的回调函数中都进行了执行。并且老是callbackFun先执行,若是咱们打开注释://data.age = 10000000; //alert(0000);
就会发现:在callbackFun中对 data 进行修改以后,success指定的回调函数的结果也会发生变化,并且经过alert(0000),咱们肯定了若是alert(000)没有执行完,success指定的函数就不会开始执行,就是说两个回调函数是前后同步执行的。
结果以下:
3.jsonp 跨域与 ajax
从上面的介绍和例子,咱们知道了 jsonp 跨域的原理,是利用了script标签的特性来进行的,可是这和ajax有什么关系呢?显然script标签加载js脚本和ajax一点关系都没有,在没有ajax技术以前,script标签就存在了的。只不过是jquery的封装,使用了ajax来向服务器传递 jsonp 和 jsonpCallback 这两个参数而已。若是咱们再服务器端和客户端,对参数 jsonp 和 jsonpCallback 的值,协调好(也就是一致性),那么就没有必要使用ajax来传递着两个参数了,就像上面第二个例子那样,直接构造一个script标签就好了。不过实际上,咱们仍是会使用ajax的封装,由于它在调用完成以后,又将动态添加的script标签去掉了,咱们看下相关的源码:
上面的代码先构造一个script标签,而后注册一个onload的回调,最后将构造好的script标签insert进去。insert完成以后,会触发onload回调,其中又将前面插入的script标签去掉了。其中的 代码 callback( 200, "success" ) 其实就是触发 ajax 的jsonp成功时的success回调函数,callback函数实际上是一个 done 函数,其中包含了下面的代码:
由于传入的是 200 ,因此 isSuccess = true; 因此执行 "success"中的回调函数,response = ajaxHandleResponse(...) 就是咱们处理服务器servelt返回的数据,咱们能够调试:console.log(response.data.age); console.log(response.data.name); 看到结果。
3.jsonp 跨域与 get/post
咱们知道 script,link, img 等等标签引入外部资源,都是 get 请求的,那么就决定了 jsonp 必定是 get 的,那么为何咱们上面的代码中使用的 post 请求也成功了呢?这是由于当咱们指定dataType:'jsonp',不论你指定:type:"post" 或者type:"get",其实质上进行的都是 get 请求!!!从两个方面能够证实这一点:
1)若是咱们将JsonServlet中的 doGet()方法注释掉,那么上面的跨域访问就不能进行,或者在 doPost() 和 doGet() 方法中进行调试,均可以证实这一点;
2)咱们看下firebug中的“网络”选项卡:
咱们看到,即便咱们指定 type:"post",当dataType:"jsonp" 时,进行的也是 GET请求,而不是post请求,也就是说jsonp时,type参数始终是"get",而不论咱们指定他的值是什么,jquery在里面将它设定为了get. 咱们甚至能够将 type 参数注释掉,均可以跨域成功:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
$(
function
(){
$(
"a"
).on(
"click"
,
function
(){
$.ajax({
//type:"post",
url:
"http://localhost:8080/html5/JsonServlet"
,
dataType:
'jsonp'
,
jsonp:
'mycallback'
,
jsonpCallback:
'callbackFun'
,
success:
function
(data) {
console.log(2222);
console.log(data.age);
}
});
})
});
|
因此jsonp跨域只能是get,jquery在封装jsonp跨域时,不论咱们指定的是get仍是post,他统一换成了get请求,估计这样能够减小错误吧。其对应的query源码以下所示:
// Handle cache's special case and global jQuery.ajaxPrefilter( "script", function( s ) { if ( s.cache === undefined ) { s.cache = false; } if ( s.crossDomain ) { s.type = "GET"; s.global = false; } });
if( s.crossDomain){ s.type = "GET"; ...} 这里就是真相!!!!!!!!在ajax的过滤函数中,只要是跨域,jquery就将其type设置成"GET",真是那句话:在源码面前,一切了无秘密!jquery源码我本身不少地方读不懂,可是并不妨碍咱们去读,去探索!
4.除了jsonp跨域方法以外的其余跨域方法
其实除了jsonp跨域以外,还有其余方法绕过同源策略,
1)由于同源策略是针对客户端的,在服务器端没有什么同源策略,是能够随便访问的,因此咱们能够经过下面的方法绕过客户端的同源策略的限制:客户端先访问 同源的服务端代码,该同源的服务端代码,使用httpclient等方法,再去访问不一样源的 服务端代码,而后将结果返回给客户端,这样就间接实现了跨域。相关例子,参见博文:http://www.cnblogs.com/digdeep/p/4198643.html
2)在服务端开启cors也能够支持浏览器的跨域访问。cors即:Cross-Origin Resource Sharing 跨域资源共享。jsonp和cors的区别是jsonp几乎全部浏览器都支持,可是只能是get,而cors有些老浏览器不支持,可是get/post都支持,cors的支持状况,能够参见下图(来自:http://caniuse.com/#search=cors)
cors实例:
项目html5中的Cors servlet:
public class Cors extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "Content-Type"); response.getWriter().write("cors get"); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "Content-Type"); response.getWriter().write("cors post"); } }
在html4项目中访问他:
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="keywords" content="jsonp"> <meta name="description" content="jsonp"> <title>cors</title> <style type="text/css"> *{margin:0;padding:0;} div{width:600px;height:100px;margin:20px auto;} </style> </head> <body> <div> <a href="javascript:;">cors测试</a> </div> <script type="text/javascript" src="js/jquery-1.11.1.js"></script> <script type="text/javascript"> $(function(){ $("a").on("click", function(){ $.ajax({ type:"post", url:"http://localhost:8080/html5/cors", success:function(data) { console.log(data); alert(data); } }); }) }); </script> </body> </html>
访问结果以下:
5. 参数jsonp 和 jsonpCallback
jsonp指定使用哪一个名字将回调函数传给服务端,也就是在服务端经过 request.getParameter(""); 的那个名字,而jsonpCallback就是request.getParamete("")取得的值,也就是回调函数的名称。其实这两个参数均可以不指定,只要咱们是经过 success : 来指定回调函数的状况下,就能够省略这两个参数,jsnop若是不知道,默认是 "callback",jsnpCallback不指定,是jquery自动生成的一个函数名称,其对应源码以下:
1
2
3
4
5
6
7
8
9
10
11
12
|
var
oldCallbacks = [],
rjsonp = /(=)\?(?=&|$)|\?\?/;
// Default jsonp settings
jQuery.ajaxSetup({
jsonp:
"callback"
,
jsonpCallback:
function
() {
var
callback = oldCallbacks.pop() || ( jQuery.expando +
"_"
+ ( nonce++ ) );
this
[ callback ] =
true
;
return
callback;
}
});
|