在項目中遇到一個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;
}
});
|