使用 JSONP 實現(xiàn)跨域通信
簡介
Asynchronous JavaScript and XML (Ajax) 是驅(qū)動新一代 Web 站點(流行術(shù)語為 Web 2.0 站點)的關(guān)鍵技術(shù)。Ajax 允許在不干擾 Web 應(yīng)用程序的顯示和行為的情況下在后臺進行數(shù)據(jù)檢索。使用 XMLHttpRequest 函數(shù)獲取數(shù)據(jù),它是一種 API,允許客戶端 JavaScript 通過 HTTP 連接到遠程服務(wù)器。Ajax 也是許多 mashup 的驅(qū)動力,它可將來自多個地方的內(nèi)容集成為單一 Web 應(yīng)用程序。
不過,由于受到瀏覽器的限制,該方法不允許跨域通信。如果嘗試從不同的域請求數(shù)據(jù),會出現(xiàn)安全錯誤。如果能控制數(shù) 據(jù)駐留的遠程服務(wù)器并且每個請求都前往同一域,就可以避免這些安全錯誤。但是,如果僅停留在自己的服務(wù)器上,Web 應(yīng)用程序還有什么用處呢?如果需要從多個第三方服務(wù)器收集數(shù)據(jù)時,又該怎么辦?
同源策略阻止從一個域上加載的腳本獲取或操作另一個域上的文檔屬性。也就是說,受到請求的 URL 的域必須與當(dāng)前 Web 頁面的域相同。這意味著瀏覽器隔離來自不同源的內(nèi)容,以防止它們之間的操作。這個瀏覽器策略很舊,從 Netscape Navigator 2.0 版本開始就存在。
克服該限制的一個相對簡單的方法是讓 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ù)。腳本加載時它開始執(zhí)行。該方法是可行的,因為同源策略不阻止動態(tài)腳本插入,并且將腳本看作是從提供 Web 頁面的域上加載的。但如果該腳本嘗試從另一個域上加載文檔,就不會成功。幸運的是,通過添加 JavaScript Object Notation (JSON) 可以改進該技術(shù)。
JSON 和 JSONP
JSON 是用于在瀏覽器和服務(wù)器之間交換信息的輕量級數(shù)據(jù)格式(與 XML 相比)。JOSON 依賴于 JavaScript 開發(fā)人員,因為它是 JavaScript 對象的字符串表示。例如,假設(shè)有一個含兩個屬性的 ticker 對象:symbol 和 price。這是在 JavaScript 中定義 ticker 對象的方式:
var ticker = {symbol: 'IBM', price: 91.42};
并且這是它的 JSON 表示方式:
{symbol: 'IBM', price: 91.42}
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)備將這兩個步驟包含到 Web 頁面
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>
至此,本文已展示了如何將靜態(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 的獨立 JavaScript 文件中。
showPrice({symbol: 'IBM', price: 91.42});
現(xiàn)在改變 Web 頁面中的腳本
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ù)。這其實是真正的 JSONP(JSON with Padding):打包在函數(shù)調(diào)用中的 JSON 數(shù)據(jù)。注意,為了完成該操作,Web 頁面必須在插入時具有已經(jīng)定義好的回調(diào)函數(shù),也就是我們例子中的 showPrice()。
不過,所謂的 JSONP 服務(wù)(或 Remote JSON Service)是一種帶有附加功能的 Web 服務(wù),該功能支持在特定于用戶的函數(shù)調(diào)用中打包返回的 JSON 數(shù)據(jù)。這種方法依賴于接受回調(diào)函數(shù)名作為請求參數(shù)的遠程服務(wù)。然后該服務(wù)生成對該函數(shù)的調(diào)用,將 JSON 數(shù)據(jù)作為參數(shù)傳遞,在到達客戶端時將其插入 Web 頁面并開始執(zhí)行。
jQuery 的 JSONP 支持
從 1.2 版本開始,jQuery 擁有對 JSONP 回調(diào)的本地支持。如果指定了 JSONP 回調(diào),就可以加載位于另一個域的 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 將一個全局函數(shù)附加到插入腳本時需要調(diào)用的窗口對象。另外,jQuery 也能優(yōu)化非跨域調(diào)用。如果向同一個域發(fā)出請求,jQuery 就將其轉(zhuǎn)化為普通 Ajax 請求。
使用 JSONP 支持的示例服務(wù)
在上一個例子中,使用了靜態(tài)文件(ticker.js)將 JavaScript 動態(tài)插入到 Web 頁面中。盡管返回了 JSONP 回復(fù),但它不允許您在 URL 中定義回調(diào)函數(shù)名。這不是 JSONP 服務(wù)。因此,如何才能將其轉(zhuǎn)換為真正的 JSONP 服務(wù)呢?可使用的方法很多。這里我們將分別使用 PHP 和 Java 展示兩個示例。
首先,假設(shè)您的服務(wù)在所請求的 URL 中接受了一個名為 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ù)名。因為 jQuery 會用生成的函數(shù)名替換 ?。所以您不用定義類似于 showPrice() 的函數(shù)。
6 顯示了用 PHP 實現(xiàn)的 JSONP 服務(wù)的一段代碼。
6. 用 PHP 實現(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 實現(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ā)項目做準(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 的強大技術(shù),但不幸的是,它并不是所有跨域通信需求的萬靈藥。它有一些缺陷,在提交開發(fā)資源之前必須認真考慮它們。第一,也是最重要的一點,沒有關(guān)于 JSONP 調(diào)用的錯誤處理。如果動態(tài)腳本插入有效,就執(zhí)行調(diào)用;如果無效,就靜默失敗。失敗是沒有任何提示的。例如,不能從服務(wù)器捕捉到 404 錯誤,也不能取消或重新開始請求。不過,等待一段時間還沒有響應(yīng)的話,就不用理它了。(未來的 jQuery 版本可能有終止 JSONP 請求的特性)。
JSONP 的另一個主要缺陷是被不信任的服務(wù)使用時會很危險。因為 JSONP 服務(wù)返回打包在函數(shù)調(diào)用中的 JSON 響應(yīng),而函數(shù)調(diào)用是由瀏覽器執(zhí)行的,這使宿主 Web 應(yīng)用程序更容易受到各類攻擊。如果打算使用 JSONP 服務(wù),了解它能造成的威脅非常重要。
浙公網(wǎng)安備 33010602011771號