原文位址:http://www.cnblogs.com/chopper/archive/2012/03/24/2403945.html
取不到資料!
上周客戶新買了伺服器,原本在舊的伺服器上放着客戶的Web首頁資訊和一個背景程式(asp.net),在客戶的首頁中有一個動态顯示最新消息的處理,這個處理就是通過ajax異步從那個背景程式中取得的。由于又購買了新的伺服器,客戶想把web首頁和那個背景程式分開來,背景程式被部署到了新的伺服器上。不過這個項目是我的同僚小福同志開發的,也就由他來把程式分開部署,然後進行一些小改動。
"怎麼最新消息取不到了,異步處理的url也已經添加上新伺服器的位址(http://xxxx.com/.../news.ashx),奇怪了..."小福在一邊抱怨,我看了看IE7下還出了個腳本錯誤"アクセスが拒否されました"的錯誤(環境是日文的,意思是通路被拒絕了)。網上查了下中文環境應該是"沒有權限"吧。在Firefox和Chrome上是看不到任何腳本錯誤的,不過可以通過Firebug工具測出這個錯誤("Permission denied to call method XMLHttpRequest.open")。
同源政策
為什麼會出這樣的錯誤呢?這是因為所有支援Javascript的浏覽器都會使用同源政策這個安全政策。看看百度的解釋:
同源政策,它是由Netscape提出的一個著名的安全政策。現在所有支援JavaScript 的浏覽器都會使用這個政策。所謂同源是指,域名,協定,端口相同。當一個浏覽器的兩個tab頁中分别打開來 百度和谷歌的頁面當一個百度浏覽器執行一個腳本的時候會檢查這個腳本是屬于哪個頁面的,即檢查是否同源,隻有和百度同源的腳本才會被執行。
這就是引起為何取不到資料的原因了,那如何才能解決跨域的問題呢?沒錯,我們現在可以進入正題,來了解下什麼是JSONP了。
JSON和JSONP
JSONP和JSON好像啊,他們之間有什麼聯系嗎?
JSON(JavaScript Object Notation) 是一種輕量級的資料交換格式。對于JSON大家應該是很了解了吧,不是很清楚的朋友可以去json.org上了解下,簡單易懂。
JSONP是JSON with Padding的略稱。它是一個非官方的協定,它允許在伺服器端內建Script tags傳回至用戶端,通過javascript callback的形式實作跨域通路(這僅僅是JSONP簡單的實作形式)。--來源百度
JSONP就像是JSON+Padding一樣(Padding這裡我們了解為填充), 我們先看下面的小例子然後再詳細介紹。
跨域的簡單原理
光看定義還不是很明白,那首先我們先來手動做個簡單易懂的小測試。建立一個asp.net的web程式,添加sample.html網頁和一個test.js檔案,代碼如下:
sample.html的代碼:
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml" >
3 <head>
4 <title>test</title>
5 <script type="text/javascript" src="test.js"></script>
6 </head>
7 <body>
8 </body>
9 </html>
test.js的代碼:
1 alert("success");
打開sample.html後會跳出"success”這樣的這樣的資訊框,這似乎并不能說明什麼, 跨域問題到底怎麼解決呢?好,現在我們模拟下非同源的環境,剛才我們不是已經用Visual Studio建立了一個Web程式嗎(這裡我們叫A程式),現在我們再打開一個新的Visual Studio再建立一個Web程式(B程式),将我們的之前的test.js檔案從A程式中移除然後拷貝到B程式中。兩個程式都運作起來後,Visual Studio會啟動内置伺服器,假設A程式是localhost:20001,B程式是localhost:20002,這就模拟了一個非同源的環境了(雖然域名相同但端口号不同,是以是非同源的)。
OK,我們接下來應該改下sample.html裡的代碼,因為test.js檔案在B程式上了,url也就變成了localhost:20002。
sample.html部分代碼:
1 <script type="text/javascript" src="http://localhost:20002/test.js"></script>
請保持AB兩個Web程式的運作狀态,當你再次重新整理localhost:20001/sample.html的時候,和原來一樣跳出了"success"的對話框,是的,成功通路到了非同源的localhost:20002/test.js這個所謂的遠端服務了。到了這一步,相信大家應該已經大概明白如何跨域通路了的原理了。
<script>标簽的src屬性并不被同源政策所限制,是以可以擷取任何伺服器上腳本并執行。
JSONP的實作模式--CallBack
剛才的小例子講解了跨域的原理,我們回上去再看看JSONP的定義說明中講到了javascript callback的形式。那我們就來修改下代碼,如何實作JSONP的javascript callback的形式。
程式A中sample的部分代碼:
1 <script type="text/javascript">
2 //回調函數
3 function callback(data) {
4 alert(data.message);
5 }
6 </script>
7 <script type="text/javascript" src="http://localhost:20002/test.js"></script>
程式B中test.js的代碼:
1 //調用callback函數,并以json資料形式作為闡述傳遞,完成回調
2 callback({message:"success"});
這其實就是JSONP的簡單實作模式,或者說是JSONP的原型:建立一個回調函數,然後在遠端服務上調用這個函數并且将JSON 資料形式作為參數傳遞,完成回調。
将JSON資料填充進回調函數,這就是JSONP的JSON+Padding的含義吧。
一般情況下,我們希望這個script标簽能夠動态的調用,而不是像上面因為固定在html裡面是以沒等頁面顯示就執行了,很不靈活。我們可以通過javascript動态的建立script标簽,這樣我們就可以靈活調用遠端服務了。
程式A中sample的部分代碼:
1 <script type="text/javascript">
2 function callback(data) {
3 alert(data.message);
4 }
5 //添加<script>标簽的方法
6 function addScriptTag(src){
7 var script = document.createElement('script');
8 script.setAttribute("type","text/javascript");
9 script.src = src;
10 document.body.appendChild(script);
11 }
12
13 window.onload = function(){
14 addScriptTag("http://localhost:20002/test.js");
15 }
16 </script>
程式B的test.js代碼不變,我們再執行下程式,是不是和原來的一樣呢。如果我們再想調用一個遠端服務的話,隻要添加addScriptTag方法,傳入遠端服務的src值就可以了。這裡說明下為什麼要将addScriptTag方法放入到window.onload的方法裡,原因是addScriptTag方法中有句document.body.appendChild(script);,這個script标簽是被添加到body裡的,由于我們寫的javascript代碼是在head标簽中,document.body還沒有初始化完畢呢,是以我們要通過window.onload方法先初始化頁面,這樣才不會出錯。
上面的例子是最簡單的JSONP的實作模型,不過它還算不上一個真正的JSONP服務。我們來看一下真正的JSONP服務是怎麼樣的,比如Google的ajax搜尋接口:http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=?&callback=?
q=?這個問号是表示你要搜尋的内容,最重要的是第二個callback=?這個是正如其名表示回調函數的名稱,也就是将你自己在用戶端定義的回調函數的函數名傳送給服務端,服務端則會傳回以你定義的回調函數名的方法,将擷取的json資料傳入這個方法完成回調。有點羅嗦了,還是看看實作代碼吧:
1 <script type="text/javascript">
2 //添加<script>标簽的方法
3 function addScriptTag(src){
4 var script = document.createElement('script');
5 script.setAttribute("type","text/javascript");
6 script.src = src;
7 document.body.appendChild(script);
8 }
9
10 window.onload = function(){
11 //搜尋apple,将自定義的回調函數名result傳入callback參數中
12 addScriptTag("http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=apple&callback=result");
13
14 }
15 //自定義的回調函數result
16 function result(data) {
17 //我們就簡單的擷取apple搜尋結果的第一條記錄中url資料
18 alert(data.responseData.results[0].unescapedUrl);
19 }
20 </script>
像這樣的JSONP服務還有很多(以下資訊來自使用 JSONP 實作跨域通信,第 1 部分: 結合 JSONP 和 jQuery 快速建構強大的 mashup):
Digg API:來自 Digg 的頭條新聞:
http://services.digg.com/stories/top?appkey=http%3A%2F%2Fmashup.com&type=javascript&callback=?
Geonames API:郵編的位置資訊:
http://www.geonames.org/postalCodeLookupJSON?postalcode=10504&country=US&callback=?
Flickr JSONP API:載入最新貓的圖檔:
http://api.flickr.com/services/feeds/photos_public.gne?tags=cat&tagmode=any&format=json&jsoncallback=?
Yahoo Local Search API:在郵編為 10504 的地區搜尋比薩:
http://local.yahooapis.com/LocalSearchService/V3/localSearch?appid=YahooDemo&query=pizza&zip=10504&results=2&output=json&callback=?
接下來我們自己來建立一個簡單的遠端服務,實作和上面一樣的JSONP服務。還是利用Web程式A和程式B來做示範,這次我們在程式B上建立一個MyService.ashx檔案。
程式B的MyService.ashx代碼:
1 public class MyService : IHttpHandler
2 {
3 public void ProcessRequest(HttpContext context)
4 {
5 //擷取回調函數名
6 string callback = context.Request.QueryString["callback"];
7 //json資料
8 string json = "{\"name\":\"chopper\",\"sex\":\"man\"}";
9
10 context.Response.ContentType = "application/json";
11 //輸出:回調函數名(json資料)
12 context.Response.Write(callback + "(" + json + ")");
13 }
14
15 public bool IsReusable
16 {
17 get
18 {
19 return false;
20 }
21 }
22 }
程式A的sample代碼中的調用:
1 <script type="text/javascript">
2 function addScriptTag(src){
3 var script = document.createElement('script');
4 script.setAttribute("type","text/javascript");
5 script.src = src;
6 document.body.appendChild(script);
7 }
8
9 window.onload = function(){
10 //調用遠端服務
11 addScriptTag("http://localhost:20002/MyService.ashx?callback=person");
12
13 }
14 //回調函數person
15 function person(data) {
16 alert(data.name + " is a " + data.sex);
17 }
18 </script>
這就完成了一個最基本的JSONP服務調用了,是不是很簡單,下面我們來了解下JQuery是如何調用JSONP的。
jQuery對JSONP的實作
jQuery架構也當然支援JSONP,可以使用$.getJSON(url,[data],[callback])方法(詳細可以參考http://api.jquery.com/jQuery.getJSON/)。那我們就來修改下程式A的代碼,改用jQuery的getJSON方法來實作(下面的例子沒用用到向服務傳參,是以隻寫了getJSON(url,[callback])):
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript">
$.getJSON("http://localhost:20002/MyService.ashx?callback=?",function(data){
alert(data.name + " is a a" + data.sex);
});
</script>
結果是一樣的,要注意的是在url的後面必須添加一個callback參數,這樣getJSON方法才會知道是用JSONP方式去通路服務,callback後面的那個問号是内部自動生成的一個回調函數名。這個函數名大家可以debug一下看看,比如jQuery17207481773362960666_1332575486681。
當然,加入說我們想指定自己的回調函數名,或者說服務上規定了固定回調函數名該怎麼辦呢?我們可以使用$.ajax方法來實作(參數較多,詳細可以參考http://api.jquery.com/jQuery.ajax)。先來看看如何實作吧:
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript">
$.ajax({
url:"http://localhost:20002/MyService.ashx?callback=?",
dataType:"jsonp",
jsonpCallback:"person",
success:function(data){
alert(data.name + " is a a" + data.sex);
}
});
</script>
沒錯,jsonpCallback就是可以指定我們自己的回調方法名person,遠端服務接受callback參數的值就不再是自動生成的回調名,而是person。dataType是指定按照JSOPN方式通路遠端服務。