由 yangyi 于 2009-05-31 22:28:36 提供


2009 年 5 月 25 日

隨著公開提供的 Web 服務(wù) API 不斷增加,現(xiàn)在可以輕松地從不同 Web 源獲取資源并構(gòu)建 mashup —— 只要您能訪問正確的 API 和工具。探究如何能夠結(jié)合高深的跨域調(diào)用技術(shù)(JSONP)和靈活的 JavaScript 庫(jQuery),以快速構(gòu)建強(qiáng)大的 mashup。

簡介

Asynchronous JavaScript and XML (Ajax) 是驅(qū)動新一代 Web 站點(diǎn)(流行術(shù)語為 Web 2.0 站點(diǎn))的關(guān)鍵技術(shù)。Ajax 允許在不干擾 Web 應(yīng)用程序的顯示和行為的情況下在后臺進(jìn)行數(shù)據(jù)檢索。使用 XMLHttpRequest 函數(shù)獲取數(shù)據(jù),它是一種 API,允許客戶端 JavaScript 通過 HTTP 連接到遠(yuǎn)程服務(wù)器。Ajax 也是許多 mashup 的驅(qū)動力,它可將來自多個(gè)地方的內(nèi)容集成為單一 Web 應(yīng)用程序。

不過,由于受到瀏覽器的限制,該方法不允許跨域通信。如果嘗試從不同的域請求數(shù)據(jù),會出現(xiàn)安全錯(cuò)誤。如果能控制數(shù)據(jù)駐留的遠(yuǎn)程服務(wù)器并且每個(gè)請求都前往同一域,就可以避免這些安全錯(cuò)誤。但是,如果僅停留在自己的服務(wù)器上,Web 應(yīng)用程序還有什么用處呢?如果需要從多個(gè)第三方服務(wù)器收集數(shù)據(jù)時(shí),又該怎么辦?

理解同源策略限制

同源策略阻止從一個(gè)域上加載的腳本獲取或操作另一個(gè)域上的文檔屬性。也就是說,受到請求的 URL 的域必須與當(dāng)前 Web 頁面的域相同。這意味著瀏覽器隔離來自不同源的內(nèi)容,以防止它們之間的操作。這個(gè)瀏覽器策略很舊,從 Netscape Navigator 2.0 版本開始就存在。

克服該限制的一個(gè)相對簡單的方法是讓 Web 頁面向它源自的 Web 服務(wù)器請求數(shù)據(jù),并且讓 Web 服務(wù)器像代理一樣將請求轉(zhuǎn)發(fā)給真正的第三方服務(wù)器。盡管該技術(shù)獲得了普遍使用,但它是不可伸縮的。另一種方式是使用框架要素在當(dāng)前 Web 頁面中創(chuàng)建新區(qū)域,并且使用 GET 請求獲取任何第三方資源。不過,獲取資源后,框架中的內(nèi)容會受到同源策略的限制。

克服該限制更理想方法是在 Web 頁面中插入動態(tài)腳本元素,該頁面源指向其他域中的服務(wù) URL 并且在自身腳本中獲取數(shù)據(jù)。腳本加載時(shí)它開始執(zhí)行。該方法是可行的,因?yàn)橥床呗圆蛔柚箘討B(tài)腳本插入,并且將腳本看作是從提供 Web 頁面的域上加載的。但如果該腳本嘗試從另一個(gè)域上加載文檔,就不會成功。幸運(yùn)的是,通過添加 JavaScript Object Notation (JSON) 可以改進(jìn)該技術(shù)。

JSON 和 JSONP

JSON 是用于在瀏覽器和服務(wù)器之間交換信息的輕量級數(shù)據(jù)格式(與 XML 相比)。JOSON 依賴于 JavaScript 開發(fā)人員,因?yàn)樗?JavaScript 對象的字符串表示。例如,假設(shè)有一個(gè)含兩個(gè)屬性的 ticker 對象:symbol 和 price。這是在 JavaScript 中定義 ticker 對象的方式:

var ticker = {symbol: 'IBM', price: 91.42};

并且這是它的 JSON 表示方式:

{symbol: 'IBM', price: 91.42}

從 參考資料 查找更多有關(guān) JSON 和將其作為數(shù)據(jù)內(nèi)部交換格式的信息。清單 1 定義了一個(gè) JavaScript 函數(shù),調(diào)用該函數(shù)時(shí)會顯示 IBM 的股價(jià)。(我們沒有詳細(xì)介紹如何將該函數(shù)添加到 Web 頁面)。


清單 1. 定義 showPrice 函數(shù)
function showPrice(data) {     alert("Symbol: " + data.symbol + ", Price: " + data.price); }                 

可以將 JSON 數(shù)據(jù)作為參數(shù)傳遞,以調(diào)用該函數(shù):

showPrice({symbol: 'IBM', price: 91.42}); // alerts: Symbol: IBM, Price: 91.42                 

現(xiàn)在準(zhǔn)備將這兩個(gè)步驟包含到 Web 頁面,如清單 2 所示。


清單 2. 在 Web 頁面中包含 showPrice 函數(shù)和參數(shù)
<script type="text/javascript"> function showPrice(data) {     alert("Symbol: " + data.symbol + ", Price: " + data.price); } </script> <script type="text/javascript">showPrice({symbol: 'IBM', price: 91.42});</script> 

加載頁面后,應(yīng)該看如圖 1 所示的警告。


圖 1. IBM ticker
IBM ticker 

至此,本文已展示了如何將靜態(tài) JSON 數(shù)據(jù)作為參數(shù)調(diào)用 JavaScript 函數(shù)。不過,通過在函數(shù)調(diào)用中動態(tài)包裝 JSON 數(shù)據(jù)可以用動態(tài)數(shù)據(jù)調(diào)用函數(shù),這是一種動態(tài) JavaScript 插入的技術(shù)。要查看其效果,將下面一行放入名為 ticker.js 的獨(dú)立 JavaScript 文件中。

showPrice({symbol: 'IBM', price: 91.42});

現(xiàn)在改變 Web 頁面中的腳本,使其和清單 3 一樣。


清單 3. 動態(tài) JavaScript 插入代碼
<script type="text/javascript"> // This is our function to be called with JSON data function showPrice(data) {     alert("Symbol: " + data.symbol + ", Price: " + data.price); } var url = “ticker.js”; // URL of the external script // this shows dynamic script insertion var script = document.createElement('script'); script.setAttribute('src', url);  // load the script document.getElementsByTagName('head')[0].appendChild(script);  </script> 				

在清單 3 所示的例子中,動態(tài)插入的 JavaScript 代碼位于 ticker.js 文件中,它將真正的 JSON 數(shù)據(jù)作為參數(shù)調(diào)用showPrice()函數(shù)。

前面已經(jīng)提到,同源策略不阻止將動態(tài)腳本元素插入文檔中。也就是說,可以動態(tài)插入來自不同域的 JavaScript,并且這些域都攜帶 JSON 數(shù)據(jù)。這其實(shí)是真正的 JSONP(JSON with Padding):打包在函數(shù)調(diào)用中的 JSON 數(shù)據(jù)。注意,為了完成該操作,Web 頁面必須在插入時(shí)具有已經(jīng)定義好的回調(diào)函數(shù),也就是我們例子中的 showPrice()

不過,所謂的 JSONP 服務(wù)(或 Remote JSON Service)是一種帶有附加功能的 Web 服務(wù),該功能支持在特定于用戶的函數(shù)調(diào)用中打包返回的 JSON 數(shù)據(jù)。這種方法依賴于接受回調(diào)函數(shù)名作為請求參數(shù)的遠(yuǎn)程服務(wù)。然后該服務(wù)生成對該函數(shù)的調(diào)用,將 JSON 數(shù)據(jù)作為參數(shù)傳遞,在到達(dá)客戶端時(shí)將其插入 Web 頁面并開始執(zhí)行。





回頁首


jQuery 的 JSONP 支持

從 1.2 版本開始,jQuery 擁有對 JSONP 回調(diào)的本地支持。如果指定了 JSONP 回調(diào),就可以加載位于另一個(gè)域的 JSON 數(shù)據(jù),回調(diào)的語法為:url?callback=?

jQuery 自動將 ? 替換為要調(diào)用的生成函數(shù)名。清單 4 顯示了該代碼。


清單 4. 使用 JSONP 回調(diào)
jQuery.getJSON(url+"&callback=?", function(data) {     alert("Symbol: " + data.symbol + ", Price: " + data.price); }); 

為此,jQuery 將一個(gè)全局函數(shù)附加到插入腳本時(shí)需要調(diào)用的窗口對象。另外,jQuery 也能優(yōu)化非跨域調(diào)用。如果向同一個(gè)域發(fā)出請求,jQuery 就將其轉(zhuǎn)化為普通 Ajax 請求。

使用 JSONP 支持的示例服務(wù)

在上一個(gè)例子中,使用了靜態(tài)文件(ticker.js)將 JavaScript 動態(tài)插入到 Web 頁面中。盡管返回了 JSONP 回復(fù),但它不允許您在 URL 中定義回調(diào)函數(shù)名。這不是 JSONP 服務(wù)。因此,如何才能將其轉(zhuǎn)換為真正的 JSONP 服務(wù)呢?可使用的方法很多。這里我們將分別使用 PHP 和 Java 展示兩個(gè)示例。

首先,假設(shè)您的服務(wù)在所請求的 URL 中接受了一個(gè)名為 callback 的參數(shù)。(參數(shù)名不重要,但是客戶和服務(wù)器必須都同意該名稱)。另外假設(shè)向服務(wù)發(fā)送的請求是這樣的:

http://www.yourdomain.com/jsonp/ticker?symbol=IBM&callback=showPrice

在這種情況下,symbol 是表示請求 ticker symbol 的請求參數(shù),而 callback 是 Web 應(yīng)用程序的回調(diào)函數(shù)的名稱。使用清單 5 所示的代碼可以通過 jQuery 的 JSONP 支持調(diào)用該服務(wù)。


清單 5. 調(diào)用回調(diào)服務(wù)
jQuery.getJSON("http://www.yourdomain.com/jsonp/ticker?symbol=IBM&callback=?",  function(data) {     alert("Symbol: " + data.symbol + ", Price: " + data.price); }); 

注意,我們使用 ? 作為回調(diào)函數(shù)名,而非真實(shí)的函數(shù)名。因?yàn)?jQuery 會用生成的函數(shù)名替換 ?。所以您不用定義類似于showPrice() 的函數(shù)。

清單 6 顯示了用 PHP 實(shí)現(xiàn)的 JSONP 服務(wù)的一段代碼。


清單 6. 用 PHP 實(shí)現(xiàn)的 JSONP 服務(wù)的代碼片段
$jsonData = getDataAsJson($_GET['symbol']); echo $_GET['callback'] . '(' . $jsonData . ');'; // prints: jsonp1232617941775({"symbol" : "IBM", "price" : "91.42"}); 

清單 7 顯示了具有同樣功能的 Java™ Servlet 方法。


清單 7. 用 Java servlet 實(shí)現(xiàn)的 JSONP 服務(wù)
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp)    throws ServletException, IOException { 	String jsonData = getDataAsJson(req.getParameter("symbol")); 	String output = req.getParameter("callback") + "(" + jsonData + ");";  	resp.setContentType("text/javascript");            	PrintWriter out = resp.getWriter(); 	out.println(output); 	// prints: jsonp1232617941775({"symbol" : "IBM", "price" : "91.42"}); } 

那么,如果要構(gòu)建 mashup 應(yīng)該怎么辦,是從第三方服務(wù)器收集內(nèi)容,并在單一的 Web 頁面中顯示它們嗎?答案很簡單:您必須使用第三方 JSONP 服務(wù)。這種服務(wù)并不少。

現(xiàn)成的 JSONP 服務(wù)

知道如何使用 JSONP 之后,可以開始使用一些現(xiàn)成的 JSONP Web 服務(wù)來構(gòu)建應(yīng)用程序和 mashup。下面為接下來的開發(fā)項(xiàng)目做準(zhǔn)備。(提示:您可以復(fù)制特定的 URL 并將其粘貼到瀏覽器的地址欄,以檢查生成的 JSONP 響應(yīng))。

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 API:來自 Flickr 的最新貓圖片:

http://api.flickr.com/services/feeds/photos_public.gne?tags=cat&tagmode=any &format=json&jsoncallback=?                 

Yahoo Local Search API:在郵編為 10504 的地區(qū)搜索比薩:

http://local.yahooapis.com/LocalSearchService/V3/localSearch?appid=YahooDemo&query=pizza &zip=10504&results=2&output=json&callback=? 





回頁首


重要提示

JSONP 是構(gòu)建 mashup 的強(qiáng)大技術(shù),但不幸的是,它并不是所有跨域通信需求的萬靈藥。它有一些缺陷,在提交開發(fā)資源之前必須認(rèn)真考慮它們。第一,也是最重要的一點(diǎn),沒有關(guān)于 JSONP 調(diào)用的錯(cuò)誤處理。如果動態(tài)腳本插入有效,就執(zhí)行調(diào)用;如果無效,就靜默失敗。失敗是沒有任何提示的。例如,不能從服務(wù)器捕捉到 404 錯(cuò)誤,也不能取消或重新開始請求。不過,等待一段時(shí)間還沒有響應(yīng)的話,就不用理它了。(未來的 jQuery 版本可能有終止 JSONP 請求的特性)。

JSONP 的另一個(gè)主要缺陷是被不信任的服務(wù)使用時(shí)會很危險(xiǎn)。因?yàn)?JSONP 服務(wù)返回打包在函數(shù)調(diào)用中的 JSON 響應(yīng),而函數(shù)調(diào)用是由瀏覽器執(zhí)行的,這使宿主 Web 應(yīng)用程序更容易受到各類攻擊。如果打算使用 JSONP 服務(wù),了解它能造成的威脅非常重要。(參見 參考資料 了解更多信息)。





回頁首


結(jié)束語

在該系列的第一篇文章中,我們講解了如何結(jié)合使用 JSONP 和 jQuery 快速構(gòu)建強(qiáng)大的 mashup。主要主題包括:

  • 瀏覽器同源策略的限制以及解決辦法
  • 作為一種有效的跨域通信技術(shù),JSONP 能夠繞過當(dāng)前瀏覽器的同源策略限制
  • JSONP 使 Web 應(yīng)用程序開發(fā)人員能夠快速構(gòu)建 mashup
  • 示例 JSONP 服務(wù)及其使用:Ticker 服務(wù)

本系列的下一篇文章將介紹 Yahoo! 查詢語言(YQL),這種單端點(diǎn) JSONP 服務(wù)允許您跨 Web 查詢、過濾和合并數(shù)據(jù)。最后還使用 YQL 和 jQuery 構(gòu)建 mashup 應(yīng)用程序。



參考資料