歡迎您光臨本站 註冊首頁

精通 Grails: 用 JSON 和 Ajax 實現非同步 Grails

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  
JavaScript Object Notation(JSON)和 Asynchronous JavaScript + XML(Ajax)是 Web 2.0 開發中的主要技術。在本期的 精通 Grails 系列中,作者 Scott Davis 演示了此 Web 框架中包括的本地 JSON 和 Ajax 功能。

本文討論 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 中使用的各種技術,從那以後這個名稱就一直沿用下來(參見 參考資料)。

近年來,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 的替代品(請參閱 參考資料)。(在本文的後面您將使用 Yahoo! 的 JSON Web 服務)。





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>  

清單 12 中的代碼做的第一件事是聲明兩個新的變數:一個變數用於存放線條,另一個數組用於存放兩個機場標記。對傳入的 JSON 調用 eval() 之後,就可以直接調用 airport.iata、airport.name、airport.lat 和 airport.lng 等欄位。(要溫習 JSON 對象,請參見 清單 5)。

有了 airport 對象的一個句柄之後,創建一個新的 GMarker。這就是我們在 Google Maps 上用於查看的 “紅圖釘”。title 屬性告訴 API,當用戶的滑鼠懸停在該標記上時,顯示什麼內容作為工具提示。bindInfoWindowHtml() 方法告訴 API,當用戶在該標記上單擊滑鼠時,顯示什麼內容。將這個標記作為疊加層添加到地圖上之後,調用 drawLine() 函數。顧名思義,它在兩個機場標記之間畫一條線(如果它們都存在的話)。

要了解關於 GMarker、GLatLng 和 GPolyline 等的 Google Maps API 對象的更多信息,請參閱在線文檔(見 參考資料)。

輸入兩個機場,應該會看到如圖 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 回調的信息,請查看 參考資料 中提供的文檔鏈接。

使用控制器可以讓遠程 JSON 請求帶來兩個好處:可以規避同源 Ajax 限制(參見 為什麼不能直接從瀏覽器遠程調用 Web 服務? 側邊欄),但是更重要的是,它提供某種封裝。控制器將變得與 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 帶來的樂趣! (責任編輯:A6)



[火星人 ] 精通 Grails: 用 JSON 和 Ajax 實現非同步 Grails已經有930次圍觀

http://coctec.com/docs/linux/show-post-69014.html