JSONP--解決ajax跨域問題
取不到資料!
上週客戶新買了伺服器,原本在舊的伺服器上放著客戶的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方法先初始化頁面,這樣才不會出錯。
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 //自定義的回撥函式result16 function result(data) { 17 //我們就簡單的獲取apple搜尋結果的第一條記錄中url資料18 alert(data.responseData.results[0].unescapedUrl); 19 } 20 </script>
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 //回撥函式person15 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方式訪問遠端服務。
利用jQuery可以很方便的實現JSONP來進行跨域訪問。
下面給出個栗子��:
一.客戶端
Html程式碼- <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <title>Insert title here</title>
- <script type="text/javascript" src="resource/js/jquery-1.7.2.js"></script>
- </head>
- <script type="text/javascript">
- $(function(){
- /*
- //簡寫形式,效果相同
- $.getJSON("http://app.example.com/base/json.do?sid=1494&busiId=101&jsonpCallback=?",
- function(data){
- $("#showcontent").text("Result:"+data.result)
- });
- */
- $.ajax({
- type : "get",