精通 Grails: 用 JSON 和 Ajax 实现异步 Grails

用 Grails 实现 Google Maps mashups

JavaScript Object Notation(JSON)和 Asynchronous JavaScript + XML(Ajax)是 Web 2.0 开发中的主要技术。在本期的 精通 Grails 系列中,作者 Scott Davis 演示了此 Web 框架中包括的本地 JSON 和 Ajax 功能。

Scott Davis, 主编, AboutGroovy.com

Scott DavisScott Davis 是国际知名作家、演讲家、软件开发人员。他出版的书籍有 Groovy Recipes: Greasing the Wheels of JavaGIS for Web Developers: Adding Where to Your ApplicationThe Google Maps APIJBoss At Work



2008 年 12 月 23 日

本文讨论 Grails 对于其互补技术 JSON 和 Ajax 的支持。在前几期的 精通 Grails 系列文章中,JSON 和 Ajax 都扮演支援者的角色,而这一次,它们担任主角。您将使用内置的 Prototype 库和 Grails <formRemote> 标记发出一个 Ajax 请求。您还将看到一些关于提供本地 JSON 和通过 Web 动态获得 JSON 的例子。

为了进行演示,您将组建一个旅行计划页面,在该页面中,用户可以输入出发地机场和目的地机场。当机场显示在一个 Google Map 上时,用户可通过一个链接搜索目的地机场附近的宾馆。图 1 显示了这个页面:

图 1. 旅行计划页面
旅行计划页面

您可以在 1 个 GSP 文件和 3 个控制器中,用大约 150 行代码实现所有这些功能。

Ajax 和 JSON 简史

关于本系列

Grails 是一种新型 Web 开发框架,它将常见的 Spring 和 Hibernate 等 Java™ 技术与当前流行的约定优于配置等实践相结合。Grails 是用 Groovy 编写的,它不仅可以与遗留的 Java 代码无缝集成,而且提供脚本语言的灵活性和动态性。学习完 Grails 之后,您将彻底改变看待 Web 开发的方式。

在 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 浏览器都提供这样的对象。

Ajax 资源中心

请访问 Ajax 资源中心,这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。

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/iatahttp://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 中测试通过
在 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
一个普通的 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. 实现 addAirportdrawLine
<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.iataairport.nameairport.latairport.lng 等字段。(要温习 JSON 对象,请参见 清单 5) 。

有了 airport 对象的一个句柄之后,创建一个新的 GMarker。这就是我们在 Google Maps 上用于查看的 “红图钉”。title 属性告诉 API,当用户的鼠标悬停在该标记上时,显示什么内容作为工具提示。bindInfoWindowHtml() 方法告诉 API,当用户在该标记上单击鼠标时,显示什么内容。将这个标记作为叠加层添加到地图上之后,调用 drawLine() 函数。顾名思义,它在两个机场标记之间画一条线(如果它们都存在的话)。

要了解关于 GMarkerGLatLngGPolyline 等的 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}")
  }
}

所有查询字符串参数都是硬编码的,但最后两个除外:latitudelongitude。倒数第二行实例化一个新的 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 sourcesame 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 超级链接
显示 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 带来的乐趣!

参考资料

学习

获得产品和技术

  • Grails:下载最新版本的 Grails。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development, Open source
ArticleID=360821
ArticleTitle=精通 Grails: 用 JSON 和 Ajax 实现异步 Grails
publish-date=12232008