本文討論 Grails 對於其互補技術 JSON 和 Ajax 的支持。在前幾期的 精通 Grails 系列文章中, JSON 和 Ajax 都扮演支援者的角色,而這一次,它們擔任主角。您將使用內置的 Prototype 庫和 Grails <formRemote> 標記發出一個 Ajax 請求。您還將看到一些關於提供本地 JSON 和通過 Web 動態獲得 JSON 的例子。
為了進行演示,您將組建一個旅行計劃頁面,在該頁面中,用戶可以輸入出發地機場和目的地機場。 當機場顯示在一個 Google Map 上時,用戶可通過一個鏈接搜索目的地機場附近的賓館。圖 1 顯示了這 個頁面:
圖 1. 旅行計劃頁面
您可以在 1 個 GSP 文件和 3 個控制器中,用大約 150 行代碼實現所有這些功能。
Ajax 和 JSON 簡史
在 20 世紀 90 年代中期 Web 首次流行起來的時候,浏覽器只允許粗粒度的 HTTP 請求。單擊一個超級 鏈接或一個表單提交按鈕,就會導致整個頁面被清除,並且被新的結果替代。這對於以頁面為中心的導航 來說本無大礙,但是頁面上單個的組件卻無法獨立地更新。
1999 年,Microsoft® 在 Internet Explorer 5.0 中引入了 XMLHTTP
對象。這個新 對象使開發人員可以發出 “微” HTTP 請求,保持周圍的 HTML 頁面不受影響。雖然這個特性不是基於 World Wide Web Consortium(W3C)標准,但 Mozilla 小組已經意識到它的潛力,並在 2002 年的 Mozilla 1.0 發行版中增加了一個 XMLHttpRequest
(XHR)對象。從那以後,它就成了一 個事實上的標准,每個主流 Web 浏覽器都提供這樣的對象。
2005 年,Google Maps 終於發布。對異步 HTTP 請求的廣泛使用使得它與當時的其他 Web 映射站點 形成鮮明的對比。在浏覽 Google Map 時,不再是單擊一下,然後等待整個頁面重新裝載,而是可以用鼠 標順暢地滾動地圖。Jesse James Garrett 在一個 blog 帖子中使用簡單易記的 Ajax 描述在 Google Maps 中使用的各種技術,從那以後這個名稱就一直沿用下來(參見 參考資料 )。參考資料)。(在本文的後面您將使用 Yahoo! 的 JSON Web 服務) 。
近年來,Ajax 已成為用於 “Web 2.0” 應用程序的一個涵蓋性術語,而不是一組特定的技術。請求 通常是異步的,並且以 JavaScript 發出,但是響應並非總是 XML。在基於浏覽器的應用程序的開發中, XML 缺乏本地的、易於使用的 JavaScript 解析器。當然,也可以使用 JavaScript DOM API 解析 XML, 但是對初學者而言這並不容易。因此,Ajax Web 服務常常返回純文本、HTML 片段或 JSON 格式的結果。
2006 年 7 月,Douglas Crockford 將描述 JSON 的 RFC 4627 提交到 Internet Engineering Task Force(IETF)。當年年末,Yahoo! 和 Google 等主要服務提供商將 JSON 輸出作為 XML 的替代品(請 參閱
JSON 的優點
在 Web 開發方面,JSON 與 XML 相比主要有兩個優點。首先,它更加簡潔。JSON 對象是一系列以逗 號分隔的 name:value 對,最外面有一對花括號。相反,XML 則使用重復的開始和結束標記包裝數據值。 因此,與相應的 JSON 相比,這樣便產生了兩倍的元數據開銷,所以 Crockford 將 JSON 趣稱為 “XML 的無脂替代品”(請參閱 參考資料)。當處理 Web 開發的 “細管道” 時,每次減少一些字節都可以帶 來實在的性能好處。
清單 1 顯示了 JSON 和 XML 如何組織相同的信息:
清單 1. 比較 JSON 和 XML
{"city":"Denver", "state":"CO", "country":"US"}
<result>
<city>Denver</city>
<state>CO</state>
<country>US</country>
</result>
對於 Groovy 程序員來說,JSON 對象看上去應該更熟悉:如果將花括號換成方括號的話,在 Groovy 中就是定義一個 HashMap。說起方括號,定義 JSON 對象數組的方式與定義 Groovy 對象的方式是完全一 樣的。一個 JSON 數組就是一個以逗號分隔的系列,外面以方括號包圍,如清單 2 所示:
清單 2. 一個 JSON 對象列表
[{"city":"Denver", "state":"CO", "country":"US"},
{"city":"Chicago", "state":"IL", "country":"US"}]
當解析和處理 JSON 時,就突出了 JSON 的第二個優點。將 JSON 裝載到內存只需一個 eval() 調用 。裝載後,就可以通過名稱直接訪問任何字段,如清單 3 所示:
清單 3. 裝載 JSON 和調用字段
var json = '{"city":"Denver", state:"CO", country:"US"}'
var result = eval( '(' + json + ')' )
alert(result.city)
Groovy 的 XmlSlurper 也允許直接訪問 XML 元素。(您已經在 “Grails 服務和 Google 地圖” 中 使用過 XmlSlurper)。如果現代 Web 浏覽器支持客戶端 Groovy,我就不會對 JSON 這麼感興趣。不幸 的是,Groovy 完全是一個服務器端解決方案。就客戶機-服務器開發而言,JavaScript 是唯一選項。所 以我選擇在服務器端使用 Groovy 處理 XML,而在客戶端則使用 JavaScript 處理 JSON。在這兩種情況 下,我都可以最輕松地得到數據。
至此,您已粗略地了解了 JSON,接下來可以通過 Grails 應用程序生成 JSON。
在 Grails 控制器中呈現 JSON
在 “使用 Ajax 實現多對多關系” 中,您首先從一個 Grails 控制器返回 JSON。清單 4 中的閉包 類似於您當時創建的閉包。不同之處在於,這個閉包是通過一個友好的 Uniform Resource Identifier( URI)訪問的,這已在 “RESTful Grails” 中討論。它還使用您在 “測試 Grails 應用程序” 中首次 見到的 Elvis 操作符。
將一個名為 iata 的閉包添加到您在 “Grails 與遺留數據庫” 中創建的 grails- app/controllers/AirportMappingController.groovy 類中,記得在文件頂部導入 grails.converters 包,如清單 4 所示:
清單 4. 將 Groovy 對象轉換成 JSON
import grails.converters.*
class AirportMappingController {
def iata = {
def iata = params.id?.toUpperCase() ?: "NO IATA"
def airport = AirportMapping.findByIata(iata)
if(!airport){
airport = new AirportMapping(iata:iata, name:"Not found")
}
render airport as JSON
}
}
在浏覽器中輸入 http://localhost:9090/trip/airportMapping/iata/den 進行測試。應該可以看到 清單 5 中所示的 JSON 結果:
清單 5. JSON 中的一個有效的 AirportMapping 對象
{"id":328,
"class":"AirportMapping",
"iata":"DEN",
"lat":"39.858409881591797",
"lng":"-104.666999816894531",
"name":"Denver International",
"state":"CO"}
也可以輸入 http://localhost:9090/trip/airportMapping/iata 和 http://localhost:9090/trip/airportMapping/iata/foo,以確認是否返回 “Not Found”。清單 6 顯 示了返回的無效的 JSON 對象:
清單 6. JSON 中的一個無效的 AirportMapping 對象
{"id":null,
"class":"AirportMapping",
"iata":"FOO",
"lat":null,
"lng":null,
"name":"Not found",
"state":null}
當然,這樣的 “考驗” 不能替代真正的測試。
測試控制器
在 test/integration 中創建 AirportMappingControllerTests.groovy。添加清單 7 中的 2 個測試 :
清單 7. 測試一個 Grails 控制器
class AirportMappingControllerTests extends GroovyTestCase{
void testWithBadIata (){
def controller = new AirportMappingController()
controller.metaClass.getParams = {->
return ["id":"foo"]
}
controller.iata()
def response = controller.response.contentAsString
assertTrue response.contains("\"name\":\"Not found\"")
println "Response for airport/iata/foo: ${response}"
}
void testWithGoodIata(){
def controller = new AirportMappingController()
controller.metaClass.getParams = {->
return ["id":"den"]
}
controller.iata()
def response = controller.response.contentAsString
assertTrue response.contains("Denver")
println "Response for airport/iata/den: ${response}"
}
}
輸入 $grails test-app 運行測試。在 JUnit HTML 報告中應該可以看到成功信息,如圖 2 所示。( 要回顧 Grails 應用程序的測試,請參閱 “測試 Grails 應用程序”)。
圖 2. 在 JUnit 中測試通過
看看 清單 7 中的 testWithBadIata() 中發生了什麼。第一行(顯然)是創建 AirportMappingController 的一個實例。這是為了後面可以調用 controller.iata() 並針對產生的 JSON 寫一個斷言。要使調用失敗(在此就是如此)或成功(在 testWithGoodIata() 中),需要用一個 id 項為 params hashmap 提供種子。通常,查詢字符串被解析並存儲到 params 中。但是,在這裡,沒 有 HTTP 請求被解析。相反,我使用 Groovy 元編程直接覆蓋 getParams 方法,使期望的值出現在返回 的 HashMap 中。(要了解關於 Groovy 元編程的更多信息,請參閱 參考資料)。
現在,JSON 產生器已經可以工作,並且經過了測試,接下來看看如何在一個 Web 頁面中使用 JSON。
設置初始的 Google Map
我希望可通過 http://localhost:9090/trip/trip/plan 訪問旅行計劃頁面。這意味著將一個 plan 閉包添加到 grails-app/controllers/TripController.groovy 中,如清單 8 所示:
清單 8. 設置控制器
class TripController {
def scaffold = Trip
def plan = {}
}
由於 plan() 不是以 render() 或 redirect() 結束,根據約定優於配置原則,顯示的將是 grails- app/views/trip/plan.gsp。用清單 9 中的 HTML 代碼創建文件。(要回顧這個 Google Map 的基礎原理 ,請參閱 “Grails 服務和 Google 地圖”)。
清單 9. 設置初始 Google Map
<html>
<head>
<title>Plan</title>
<script src="http://maps.google.com/maps?file=api&v=2&key=YourKeyHere"
type="text/javascript"></script>
<script type="text/javascript">
var map
var usCenterPoint = new GLatLng (39.833333, -98.583333)
var usZoom = 4
function load() {
if (GBrowserIsCompatible()) {
map = new GMap2(document.getElementById ("map"))
map.setCenter(usCenterPoint, usZoom)
map.addControl(new GLargeMapControl());
map.addControl(new GMapTypeControl ());
}
}
</script>
</head>
<body onload="load()" onunload="GUnload()">
<div class="body">
<div id="search" style="width:25%; float:left">
<h1>Where to?</h1>
</div>
<div id="map" style="width:75%; height:100%; float:right"></div>
</div>
</body>
</html>
如果一切正常,在浏覽器中訪問 http://localhost:9090/trip/trip/plan 將看到如圖 3 所示的界面 :
圖 3. 一個普通的 Google Map
有了基本的地圖之後,現在應該添加兩個字段,分別用於出發地機場和目的地機場。
添加表單字段
在 “使用 Ajax 實現多對多關系” 中,您使用了 Prototype 的 Ajax.Request 對象。在本文的後面 ,當從一個遠程源獲取 JSON 時,您將再次使用它。同時,您將使用 <g:formRemote> 標記。將清 單 10 中的 HTML 添加到 grails-app/views/trip/plan.gsp 中:
清單 10. 使用 <g:formRemote>
<div id="search" style="width:25%; float:left">
<h1>Where to?</h1>
<g:formRemote name="from_form"
url="[controller:'airportMapping', action:'iata']"
onSuccess="addAirport(e, 0)">
From:<br/>
<input type="text" name="id" size="3"/>
<input type="submit" value="Search" />
</g:formRemote>
<div id="airport_0"></div>
<g:formRemote name="to_form"
url="[controller:'airportMapping', action:'iata']"
onSuccess="addAirport(e, 1)">
To: <br/>
<input type="text" name="id" size="3"/>
<input type="submit" value="Search" />
</g:formRemote>
<div id="airport_1"></div>
</div>
在浏覽器中單擊 Refresh 按鈕,看看新的變化,如圖 4 所示:
圖 4. 添加表單字段
如果使用常規的 <g:form>,那麼,當用戶提交表單時,將刷新整個頁面。如果選擇 <g:formRemote>,則由一個 Ajax.Request 在幕後異步地執行表單提交。輸入文本字段被命名為 id,確保在控制器中填充 params.id。<g:formRemote> 上的 url 屬性清楚地表明,當用戶單擊提 交按鈕時,將調用 AirportMappingController.iata()。
這裡不能使用 “使用 Ajax 實現多對多關系” 中的 <g:formRemote>,因為不能將一個 HTML 表單嵌入到另一個 HTML 表單中。但是,這裡可以創建兩個不同的表單,而且不必自己編寫 Prototype 代碼。異步 JSON 請求的結果將被傳遞給 addAirport() JavaScript 函數。
接下來的任務是創建 addAirport()。
添加處理 JSON 的 JavaScript
您將創建的 addAirport() 函數負責兩項簡單的任務:將 JSON 對象裝載到內存中,然後為各種目的 使用字段。在這裡,您使用緯度和經度值創建一個 GMarker,並將它添加到地圖中。
要使 <g:formRemote> 工作,必須在 head 部分包含 Prototype 庫,如清單 11 所示:
清單 11. 在 GSP 中包含 Prototype
<g:javascript library="prototype" />
接著,將清單 12 中的 JavaScript 添加到 init() 函數後面:
清單 12. 實現 addAirport 和 drawLine
<script type="text/javascript">
var airportMarkers = []
var line
function addAirport(response, position) {
var airport = eval('(' + response.responseText + ')')
var label = airport.iata + " -- " + airport.name
var marker = new GMarker(new GLatLng(airport.lat, airport.lng), {title:label})
marker.bindInfoWindowHtml(label)
if(airportMarkers[position] != null){
map.removeOverlay(airportMarkers[position])
}
if(airport.name != "Not found"){
airportMarkers[position] = marker
map.addOverlay(marker)
}
document.getElementById("airport_" + position).innerHTML = airport.name
drawLine()
}
function drawLine(){
if(line != null){
map.removeOverlay(line)
}
if(airportMarkers.length == 2){
line = new GPolyline([airportMarkers[0].getLatLng(), airportMarkers[1].getLatLng ()])
map.addOverlay(line)
}
}
</script>
有了 airport 對象的一個句柄之後,創建一個新的 GMarker。這就是我們在 Google Maps 上用於查 看的 “紅圖釘”。title 屬性告訴 API,當用戶的鼠標懸停在該標記上時,顯示什麼內容作為工具提示 。bindInfoWindowHtml() 方法告訴 API,當用戶在該標記上單擊鼠標時,顯示什麼內容。將這個標記作 為疊加層添加到地圖上之後,調用 drawLine() 函數。顧名思義,它在兩個機場標記之間畫一條線(如果 它們都存在的話)。
輸入兩個機場,應該會看到如圖 5 所示的頁面:
圖 5. 顯示兩個機場和它們之間的連線
更改 GSP 文件時,別忘了刷新 Web 浏覽器。
您已經獲得從本地 Grails 應用程序返回的 JSON,在下一節,您將動態地從一個遠程 Web 服務得到 JSON。當然,得到 JSON 之後,就可以像在這個例子中一樣使用它:將它裝載到內存中,然後直接訪問不 同的屬性。
遠程 JSON 還是本地 JSON?
接下來的任務是顯示目的地機場附近的 10 家賓館。這需要遠程獲取數據。
應該本地存放數據,還是在處理每個請求時都遠程地獲取數據?對於這個問題,沒有標准的答案。對 於機場數據集,我覺得完全可以本地存放。這樣的數據很容易得到,而且體積不大,容易存放。(美國只 有 901 個機場,很多主要的機場基本上是保持不變的,這份列表不會那麼快就過時)。
如果機場數據集不穩定,並且太大不便本地存儲,或者不能單獨下載,那麼我會更傾向於遠程地請求 它。您在 “Grails 服務和 Google 地圖” 中用過的 geonames.org geocoding 服務提供 JSON 輸出和 XML。在 Web 浏覽器中輸入 http://ws.geonames.org/search? name_equals=den&fcode=airp&style=full&type=json。應該可以看到清單 13 所示的 JSON 結果:
清單 13. 從 GeoNames 返回的 JSON 結果
{"totalResultsCount":1,
"geonames":[
{"alternateNames":[
{"name":"DEN","lang":"iata"},
{"name":"KDEN","lang":"icao"}],
"adminCode2":"031",
"countryName":"United States",
"adminCode1":"CO",
"fclName":"spot, building, farm",
"elevation":1655,
"countryCode":"US",
"lng":-104.6674674,
"adminName2":"Denver County",
"adminName3":"",
"fcodeName":"airport",
"adminName4":"",
"timezone":{
"dstOffset":-6,
"gmtOffset":-7,
"timeZoneId":"America/Denver"},
"fcl":"S",
"name":"Denver International Airport",
"fcode":"AIRP",
"geonameId":5419401,
"lat":39.8583188,
"population":0,
"adminName1":"Colorado"}]
}
可以看到,GeoNames 服務比您在 “Grails 與遺留數據庫” 中導入的 USGS 提供更多關於機場的信 息。如果出現新的用戶需求,例如需要知道機場的時區或海拔高度,GeoNames 還可以提供另一種令人感 興趣的結果。它還包括像 London Heathrow(LHR)和 Frankfort(FRA)這樣的國際機場。您可以將 AirportMapping.iata() 轉換為使用 GeoNames,這是一個課外練習。
同時,為了顯示目的地機場附近的賓館,惟一有效的選項是利用一個遠程 Web 服務。由於有數千家賓 館,而且??館列表是不斷變化的,所以必須讓其他人負責管理這份列表。
Yahoo! 提供了一個本地搜索服務,通過該服務可以搜索一個街道地址、郵政編碼,甚至是一個經度/ 緯度點附近的企業。如果您在 “RESTful Grails” 中已經注冊並得到一個 developer 密匙,那麼可以 在這裡重用它。毫不奇怪,您在那時使用的一般搜索 URI 的格式與現在要使用的本地搜索非常類似。上 一次,您允許 Web 服務默認地返回 XML。但是,通過添加一個 name=value 對(output=json),就可以 得到 JSON。
在浏覽器中輸入以下內容(不要換行),看看 Denver International Airport 附近的賓館的 JSON 列表:
http://local.yahooapis.com/LocalSearchService/V3/localSearch?appid=
YahooDemo&query=hotel&latitude=39.858409881591797&longitude=
-104.666999816894531&sort=distance
清單 14 顯示了 JSON 結果(刪節):
清單 14. Yahoo! 返回的 JSON 結果
{"ResultSet":
{"totalResultsAvailable":"803",
"totalResultsReturned":"10",
"firstResultPosition":"1",
"ResultSetMapUrl":"http:\/\/maps.yahoo.com\/broadband\/?tt=hotel&tp=1",
"Result":[
{"id":"42712564",
"Title":"Springhill Suites-Denver Arprt",
"Address":"18350 E 68th Ave",
"City":"Denver",
"State":"CO",
"Phone":"(303) 371-9400",
"Latitude":"39.82076",
"Longitude":"-104.673719",
"Distance":"2.63",
[SNIP]
現在,您有了一個可用的賓館列表,接下來需要為其創建一個控制器方法,就像為 AirportMapping.iata() 創建該方法一樣。
創建用於發出遠程 JSON 請求的控制器方法
在本文的前面,您已經創建了一個 HotelController。將清單 15 中的 near 閉包添加到其中。(您 在 “Grails 服務和 Google 地圖” 中已經看到了類似的代碼)。
清單 15. HotelController
class HotelController {
def scaffold = Hotel
def near = {
def addr = "http://local.yahooapis.com/LocalSearchService/V3/localSearch?"
def qs = []
qs << "appid=YahooDemo"
qs << "query=hotel"
qs << "sort=distance"
qs << "output=json"
qs << "latitude=${params.lat}"
qs << "longitude=${params.lng}"
def url = new URL(addr + qs.join("&"))
render(contentType:"application/json", text:"${url.text}")
}
}
所有查詢字符串參數都是硬編碼的,但最後兩個除外:latitude 和 longitude。倒數第二行實例化一 個新的 java.net.URL。最後一行調用服務(url.text),並呈現結果。由於沒有使用 JSON 轉換器,因 此必須顯式地將 MIME-type 設置為 application/json。除非特意設置,否則 render 會返回 text/plain。
在浏覽器中輸入下面的內容(不要換行):
http://localhost:9090/trip/hotel/near?lat=
39.858409881591797&lng=-104.666999816894531
將結果與前面直接調用 http://local.yahooapis.com 的結果相比,兩者應該是相同的。
為什麼不能直接從浏覽器遠程調用 Web 服務?
如果將 local.yahooapis.com URL 插入到一個 Ajax.Request 中,它將靜默失敗。如果將它輸入到浏 覽器的地址欄,它將會成功,但是編程式地從 JavaScript 中調用它時,就會再次失敗。這是一個特有的 現象,而不是存在 bug。
具體而言,Ajax 請求要遵循同源(same source 或 same origin)規則。這意味著 Ajax 請求只能回 到源 HTML 頁面所在的同一個字段。在您的例子中,可以任意調用 http://localhost,但是 http://local.yahooapis.com 或其他地方是不能調用的。
這樣做是出於安全考慮。當您在 http://amazon.com 中輸入信用卡號時,一定希望確保那些數字不會 同時被悄悄地發送到 http://hackers.r.us。(更正式的說法是 XSS 或跨站點腳本)。
同源規則僅適用於客戶端 JavaScript,而不適用於服務器端 Groovy。因此我讓您通過一個控制器代 理對 http://local.yahooapis.com 調用,並透明地將它傳回浏覽器。
如果確實想從浏覽器調用 Yahoo! 或 Google Web 服務,兩者都會通過提供回調選項以巧妙的方法規 避了同源規則。
使用控制器可以讓遠程 JSON 請求帶來兩個好處:可以規避同源 Ajax 限制,但是更重要的是,它提 供某種封裝。控制器將變得與 Data Access Object(DAO)類似。
就像您不希望將 URL 硬編碼到遠程 Web 服務中一樣,您也不希望在視圖中出現原始的 SQL。現在, 通過調用一個本地控制器,可以保證下游的客戶機不受實現更改的影響。表名或字段名的更改會破壞嵌入 式的 SQL 語句,URL 的更改則會破壞嵌入式的 Ajax 調用。而通過調用 AirportMapping.iata(),則就 可以隨意更改本地表和遠程 GeoNames 服務中的數據源,並保證客戶端界面不受影響。長遠來看,為了提 升性能,甚至可以將對遠程服務的調用緩存到一個本地數據庫,為每個請求構建本地緩存。
現在,這個服務已經可以工作,您可以從 Web 頁面調用它。
添加 ShowHotels 鏈接
只有當用戶提供目的地機場時,才應該顯示 Show Nearby Hotels 超級鏈接。同樣,只有確認用戶真 正想看到一個賓館列表時,才應該發出遠程請求。因此,首先將 showHotelsLink() 函數添加到 plan.gsp 中的腳本塊中。另外,將一個對 showHotelsLink() 的調用添加到 addAirport() 的最後一行 ,如清單 16 所示:
清單 16. 實現 showHotelsLink()
function addAirport(response, position) {
...
drawLine()
showHotelsLink()
}
function showHotelsLink(){
if(airportMarkers[1] != null){
var hotels_link = document.getElementById("hotels_link")
hotels_link.innerHTML = "<a href='#' onClick='loadHotels()'>Show Nearby Hotels...</a>"
}
}
Grails 提供了一個 <g:remoteLink> 標記,它可以創建異步超級鏈接(類似於 <g:formRemote> 提供異步的表單提交),但是因為生命周期的問題,它們在這裡不能用。g: 標記 是在服務器上呈現的。由於這個鏈接要動態地添加到客戶端上,因此需要依賴一個純 JavaScript 解決方 案。
您可能注意到對 document.getElementById("hotels_link") 的調用。將一個新的 <div> 添加 到 search <div> 的底端,如清單 17 所示:
清單 17. 添加 hotels_link <div>
<div id="search" style="width:25%; float:left">
<h1>Where to?</h1>
<g:formRemote name="from_form" ... >
<g:formRemote name="to_form" ...>
<div id="hotels_link"></div>
</div>
刷新浏覽器,確認在提供一個目的地機場之後會顯示超級鏈接,如圖 6 所示:
圖 6. 顯示 Show Nearby Hotels 超級鏈接
現在,需要創建 loadHotels() 函數。
進行 Ajax.Remote 調用
在 plan.gsp 中的腳本塊中添加一個新函數,如清單 18 所示:
清單 18. 實現 loadHotels()
function loadHotels(){
var url = "${createLink(controller:'hotel', action:'near')}"
url += "?lat=" + airportMarkers[1].getLatLng().lat()
url += "&lng=" + airportMarkers[1].getLatLng().lng()
new Ajax.Request(url,{
onSuccess: function(req) { showHotels(req) },
onFailure: function(req) { displayError(req) }
})
}
在這裡使用 Grails createLink 方法是安全的,因為當在服務器端呈現頁面時,Hotel.near() 的 URL 的基本部分是不變的。可以使用客戶端 JavaScript 將 URL 的動態部分附加上去,然後使用熟悉的 Prototype 調用發出 Ajax 請求。
處理錯誤
為了簡單起見,我在 <g:formRemote> 調用中省略了錯誤處理。因為正在調用一個遠程服務( 盡管是通過一個本地控制器代理),所以提供某種反饋總比靜默失敗更好。將 displayError() 函數添加 到 plan.gsp 中的腳本塊中,如清單 19 所示:
清單 19. 實現 displayError()
function displayError(response){
var html = "response.status=" + response.status + "<br />"
html += "response.responseText=" + response.responseText + "<br />"
var hotels = document.getElementById("hotels")
hotels.innerHTML = html
}
顯然,這只是在 Show Nearby Hotels 鏈接下面的 hotels <div> 中應該正常顯示結果的地方 顯示錯誤。您正在將遠程調用封裝在一個服務器端控制器中,因此可以在這裡加強錯誤處理。
將一個 hotels <div> 添加到前面添加的 hotels_link <div> 的下面,如清單 20 所示 :
清單 20. 添加 hotels <div>
<div id="search" style="width:25%; float:left">
<h1>Where to?</h1>
<g:formRemote name="from_form" ... >
<g:formRemote name="to_form" ...>
<div id="hotels_link"></div>
<div id="hotels"></div>
</div>
您只需做一件事:添加一個函數,以便裝載成功的 JSON 請求,並填充 hotels <div>。
處理成功
如清單 21 所示,最後一個函數以 Yahoo! 服務返回的 JSON 響應為參數,構建一個 HTML 列表,並 將它寫到 hotels <div>:
清單 21. 實現 showHotels()
function showHotels(response){
var results = eval( '(' + response.responseText + ')')
var resultCount = 1 * results.ResultSet.totalResultsReturned
var html = "<ul>"
for(var i=0; i < resultCount; i++){
html += "<li>" + results.ResultSet.Result[i].Title + "<br />"
html += "Distance: " + results.ResultSet.Result[i].Distance + "<br />"
html += "<hr />"
html += "</li>"
}
html += "</ul>"
var hotels = document.getElementById("hotels")
hotels.innerHTML = html
}
最後一次刷新浏覽器,並輸入兩個機場。屏幕看上去應該如 圖 1 所示。
這個例子到此結束,希望您自己繼續完善它。您可以使用另一個 GMarker 數組在地圖中標出賓館。您 也可以添加 Yahoo! 結果中的其他字段,例如電話號碼和街道地址。此外,您還可以進行其他實踐。
結束語
只有大約 150 行代碼,還不錯吧?在本文中,您看到了在發出 Ajax 請求時,JSON 如何有效替代 XML。您看到了從本地 Grails 應用???序返回 JSON 是多麼容易,並且從遠程 Web 服務返回 JSON 也不 是很難。當在服務器端呈現 HTML 時,可以使用 Grails 標記,比如 <g:formRemote> 和 <g:linkRemote>。但是,知道如何使用 Prototype 提供的底層 Ajax.Request 調用對於真正動態 的 Web 2.0 應用程序是很關鍵的。
下一次,您將看到 Grails 的本地 Java Management Extensions(JMX)功能的應用。到那時,就可 以盡情享受精通 Grails 帶來的樂趣!