Изучение Grails: Асинхронная обработка данных в Grails при помощи JSON и Ajax

Работа с mashup-компонентами Google Maps в Grails

Технологии JSON (JavaScript Object Notation) и Ajax (Asynchronous JavaScript + XML) лежат в основе приложений Web 2.0. В этой статье из серии Изучение Grails Скотт Дэвис расскажет о поддержке JSON и Ajax в этой инфраструктуре создания Web-приложений.

Главной темой этой статьи является поддержка Grails таких технологий, как JSON и Ajax. Им уделялось внимание и в предыдущих статьях серии Изучение Grails, но теперь они выйдут на ведущие роли. Мы расскажем об отправке Ajax-запросов при помощи библиотеки Prototype и тега <formRemote> в Grails. Кроме того, будут продемонстрированы примеры работы с данными в формате JSON, причем как локальными, так и получаемыми через Web.

Все эти технологии будут показаны на примере страницы для планирования путешествий, на которой пользователь может указать аэропорты начальной и конечной точек поездки. После того как аэропорты будут показаны на карте Google Map, пользователь сможет нажать на специальную ссылку для поиска отелей в окрестностях точки назначения. Пример страницы показан на рисунке 1.

Рисунок 1. Страница планирования путешествий
Страница планирования путешествий

Эту функциональность можно полностью реализовать, уложившись в примерно 150 строк кода на одной странице CSP и в трех контроллерах.

Краткий экскурс в историю Ajax и JSON

Об этой серии

Grails – это инфраструктура разработки Web-приложений, сочетающая использование знакомых Java™-технологий, таких как Spring и Hibernate, с такими современными подходами как, например, "соглашения по конфигурации" (convention over configuration). Grails написан на Groovy, обеспечивая бесшовную интеграцию с Java-кодом, добавляя в то же время гибкость и динамизм скриптового языка. Освоив Grails, вы навсегда измените свою точку зрения на разработку Web-приложений.

Когда в середине 90-х годов прошлого века Web только набирал популярность, браузеры могли отправлять только самые примитивные запросы. Нажатие на ссылку или кнопку формы всегда приводило к перезагрузке страницы с целью замещения результатов. Этот подход неплохо работал для навигации по страницам, но не позволял обновлять отдельные компоненты страниц независимо друг от друга.

В 1999 году Microsoft® представила поддержку объекта XMLHTTP в пятой версии браузера Internet Explorer. Благодаря этому объекту разработчики получили возможность делать "микрозапросы" через HTTP, которые затрагивали только часть страницы. Эта возможность в то время не была отражена ни в одном стандарте Web-консорциума (W3C), однако команда разработчиков Mozilla осознала ее потенциал и реализовала поддержку объекта XMLHttpRequest (XHR) в первом релизе браузера Mozilla в 2002 году. С тех пор он является стандартом де-факто, поддерживаемым всеми основными Web-браузерами.

В 2005 году Google выпустил свой сервис Google Maps, который резко контрастировал с другими Web-сайтами того времени из-за активного использования асинхронных HTTP-запросов. Работая с Google Maps, вы можете свободно прокручивать карту мышью, не делая кликов и не ожидая полной перезагрузки страницы. Описывая технологии, использованные Google, Джесси Джеймс Гаррет (Jesse James Garrett) впервые употребил термин "Ajax", который с тех пор прочно вошел в обиход (см. раздел Ресурсы).

В последние годы термин Ajax приобрел более широкий смысл, означающий скорее приложения Web 2.0 вообще, чем конкретный набор технологий. Эти приложения, как правило, используют асинхронные запросы, работая с ними в JavaScript и получая ответы в виде XML. Одной из трудностей работы с XML в браузерных приложениях является нехватка родного и легкого в использовании JavaScript-парсера. Разумеется, можно разбирать документы XML при помощи API DOM в JavaScript, но для новичков это не так просто. Вследствие этого Web-сервисы Ajax часто возвращают результаты в виде обычного текста, фрагментов HTML или JSON.

В июле 2006 года Дуглас Крокфорд (Douglas Crockford) предложил специальной комиссии Интернет-разработок (Internet Engineering Task Force – IETF) стандарт JSON, описанный в документе RFC 4627. Уже к концу 2006 года крупнейшие сервис-провайдеры, такие как Yahoo! и Google начали использовать JSON наряду с XML (см. раздел Ресурсы). Ниже мы будем использовать Web-сервисы Yahoo!, возвращающие результаты в формате JSON.


Преимущества JSON

Когда дело касается создания Web-приложений, у JSON есть два основных преимущества перед XML. Во-первых, он компактнее. Объект в JSON представляет собой набор пар типа "имя:значение", разделенных запятыми и заключенных в фигурные скобки, в отличие от XML, в котором используются открывающий и закрывающий теги для каждого элемента данных. Из-за этого объем служебной информации при описании метаданных в JSON сокращается вдвое, из-за чего Крокфорд назвал 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>

Объекты JSON должны выглядеть знакомо для программистов на Groovy, поскольку, если заменить фигурные скобки квадратными, то получится не что иное, как определение HashMap в Groovy. Продолжая тему квадратных скобок, скажем также, что массивы в JSPN выглядят в точности как в Groovy, т.е. как разделенные запятыми последовательности элементов, заключенных в скобки. Пример приведен в листинге 2.

Листинг 2. Список объектов в JSON
[{"city":"Denver", "state":"CO", "country":"US"},
 {"city":"Chicago", "state":"IL", "country":"US"}]

Второе преимущество JSON становится очевидным при разборе и начале работы с данными. Для загрузки фрагмента JSON в память достаточно одного вызова eval(). После этого можно сразу же обращаться к любому полю данных, как показано в листинге 3.

Листинг 3. Загрузка JSON в память и обращение к полям
var json = '{"city":"Denver", state:"CO", country:"US"}'
var result = eval( '(' + json + ')' )
alert(result.city)

Аналогичный прямой доступ к элементам XML в Groovy предоставляется классом XmlSlurper (о нем было рассказано в статье Сервисы Grails и Google Maps). JSON представлял бы значительно меньший интерес, если бы браузеры позволяли выполнять клиентский код на Groovy - но тот, однако, является исключительно серверным языком. Таким образом, единственным решением, которое можно применять на клиентской стороне, является JavaScript. Вследствие этого с XML лучше работать на сервере средствами Groovy, а на клиентской стороне – с JSON средствами JavaScript. В обоих случаях требуется минимум усилий для доступа к данным.

Теперь, когда вы получили базовое представление о JSON, можно переходить к созданию Grails-приложения, одной из функций которого будет работа с JSON.


Формирование фрагментов JSON контроллером Grails

Нам уже приходилось возвращать результаты в формате JSON из контроллера Grails в статье Отношения типа "многие-ко-многим" при помощи Ajax. Замыкание, приведенное в листинге 4, напоминает те, что использовались в той статье. Разница заключается в том, что доступ к этому замыканию осуществляется через удобочитаемый URI (универсальный идентификатор ресурсов), в соответствии с тем, как было описано в статье Grails и REST. В замыкании также используется оператор Elvis, который впервые был представлен в статье Тестирование приложения Grails.

Далее добавьте замыкание iata в класс grails-app/controllers/AirportMappingController.groovy из статьи Grails и старые базы данных. Не забудьте при этом включить импорт пакета 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 в браузере. Вы должны увидеть результат в формате JSON, подобно показанному в листинге 5.

Листинг 5. Представление существующего объекта AirportMapping в JSON
{"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" (аэропорт не найден). Соответствующий этому результату объект JSON показан в листинге 6.

Листинг 6. Представление несуществующего объекта AirportMapping в JSON
{"id":null,
"class":"AirportMapping",
"iata":"FOO",
"lat":null,
"lng":null,
"name":"Not found",
"state":null}

Разумеется, эти примитивные проверки никак не могут заменить реальные тесты приложения.


Тестирование контроллера

Создайте класс AirportMappingControllerTests.groovy в каталоге test/integration и добавьте в него два теста из листинга 7.

Листинг 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 для запуска тестов. Просмотрев HTML-отчеты JUnit (рисунок 2), вы должны увидеть, что оба теста выполнены успешно. Если вы хотите освежить свои знания о тестировании приложений Groovy, то обратитесь к статье Тестирование приложения Grails.

Рисунок 2. Отчет об успешном выполнении тестов JUnit
Отчет об успешном выполнении тестов JUnit

В тесте testWithBadIata(), приведенном в листинге 7, происходит следующее. В первой строке создается экземпляр контроллера (класс AirportMappingController). Это необходимо для того, чтобы ниже можно было вызвать метод controller.iata() и выполнить проверку возвращенного контроллером фрагмента JSON. Для того чтобы протестировать как успешный (тест testWithGoodIata()), так и неуспешный (данный тест) вызов контроллера, используется ассоциативный массив params с элементом id. Обычно в params сохраняются параметры HTTP-запроса, однако в данном тесте никаких внешних запросов нет, а метод getParams перегружается при помощи механизма метапрограммирования, чтобы все нужные значения присутствовали в массиве, который будет возвращен контроллером. Ссылки на источники более подробной информации о метапрограммировании в Groovy приведены в разделе Ресурсы.

Теперь, когда код, отвечающий за формирование JSON, протестирован, можно переходить к вопросам получения информации в формате JSON с Web-страницы.


Инициализация карты 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. Далее создайте страницу, HTML-код которой показан в листинге 9 (основы Google Maps описаны в ранней статье под названием Сервисы Grails и Google Maps).

Листинг 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 использовался объект Ajax.Request из библиотеки Prototype. Мы продолжим работать с ним и в этой статье для получения фрагментов JSON от удаленных источников. Однако это будет ниже, а пока мы будем использовать тег <g:formRemote>. Добавьте фрагмент HTML, показанный в листинге 10, на страницу 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>

Нажмите на кнопку Обновить в браузере, чтобы увидеть изменения на странице (рисунок 4).

Рисунок 4. Добавление полей форм
Добавление полей форм

Если бы мы использовали обычный тег <g:form>, то отправка формы на сервер приводила бы к перезагрузке всей страницы. Вместо этого используется <g:formRemote>, при помощи которого вызывается объект Ajax.Request, выполняющий асинхронную отправку данных незаметно для пользователя. Текстовое поле ввода имеет идентификатор id, поэтому контроллер получит на вход значение, хранящееся в params.id. Значение атрибута url тега <g:formRemote> недвусмысленно говорит о том, что при нажатии пользователем на кнопку submit будет вызван AirportMappingController.iata().

В статье Отношения типа "многие-ко-многим" при помощи Ajax мы не могли использовать тег <g:formRemote>, поскольку нельзя включать одну HTML-форму внутрь другой. В этом же примере можно создать две независимые формы и не заботиться о ручном использовании библиотеки Prototype. Результаты асинхронного запроса будут переданы в функцию addAirport() на JavaScript.

Нашей следующей задачей будет создание функции addAirport().


Создание функции JavaScript для обработки JSON

Функция addAirport(), о которой далее пойдет речь, делает две простые вещи: во-первых, она загружает объект JSON в память, а во-вторых – обрабатывает значения его полей. В этом примере значения широты и долготы будут использоваться при инициализации объекта GMarker и нанесении его на карту.

Для того чтобы работал тег <g:formRemote>, необходимо подключить библиотеку Prototype в заголовочной секции файла (листинг 11).

Листинг 11. Импорт Prototype в файле GSP
<g:javascript library="prototype" />

Далее добавьте фрагмент JavaScript, приведенный в листинге 12, сразу за функцией 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 указывается текст подсказки, выдаваемый при наведении курсора мыши на маркер. При помощи метода bindInfoWindowHtml() задается поведение маркера при нажатии на него мышью. Функция drawLine() вызывает сразу после того, как маркер нанесен на карту в виде оверлея. Как и следует из ее названия, она рисует линию, соединяющую маркеры аэропортов (если они оба существуют).

Дополнительная информация об API Google Maps, в частности о таких объектах, как GMarker, GLatLng и GPolyline, приведена в online-документации (см. раздел Ресурсы).

После добавления аэропортов страница примет вид, показанный на рисунке 5.

Рисунок 5. Отображение двух аэропортов и соединяющей их линии
Отображение двух аэропортов и соединяющей их линии

Не забывайте обновлять страницу в браузере после каждого изменения файла GSP.

Теперь, когда у нас готов пример локального использования JSON, полученного от приложения Grails, пришло время сделать следующий шаг. Далее мы будем динамически получать JSON от удаленного Web-сервиса. Разумеется, получив фрагмент JSON, вы сможете работать с ним совершенно аналогично тому, как было показано в предыдущем примере: загружать его в память и обращаться к полям объекта.


Удаленный или локальный JSON?

Нашей следующей задачей будет отображение на карте 10 отелей, ближайших к аэропорту назначения. Такие задачи практически всегда требуют взаимодействия с удаленным источником данных.

Не существует однозначного ответа на вопрос, стоит ли хранить данные локально или получать их от удаленного источника при каждом запросе пользователя. В случае с аэропортами имеет смысл хранить локально, поскольку эти данные легко доступны и не представляют никаких сложностей при хранении. В США находится всего 901 аэропорт, более того, этот список изменяется очень редко и, скорее всего, будет актуальным в обозримом будущем.

Если бы список аэропортов менялся, был бы слишком велик для локального хранения либо его нельзя было бы загрузить за один раз, то имело бы смысл запрашивать данные при каждом запросе. Геосервис geonames.org (см. раздел Ресурсы, который мы использовали в статье Сервисы Grails и Google Maps, предоставляет результаты не только в XML, но и в JSON. Например, обратитесь по следующему адресу в браузере: http://ws.geonames.org/search?name_equals=den&fcode=airp&style=full&type=json. Вы должны увидеть фрагмент JSON, приведенный в листинге 13.

Листинг 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 предоставляет более подробную информацию об аэропортах, чем содержится в данных USGS, которые мы импортировали в статье Grails и старые базы данных. Таким образом, он может быть полезен, если пользовательские запросы возрастут, например, если понадобится информация о часовых поясах либо высоте над уровнем моря. Кроме того, GeoNames предоставляет информацию об аэропортах за пределами США, например Лондонском Heathrow (LHR) или аэропорте Франкфурта (FRA). В качестве упражнения вы можете попробовать модифицировать AirportMapping.iata() таким образом, чтобы он использовал данные GeoNames.

Единственной реальной возможностью отображать список отелей, находящихся поблизости от аэропорта назначения, является использование удаленных Web-сервисов. Список отелей насчитывает тысячи единиц, и он постоянно меняется, поэтому лучше, если он будет поддерживаться вне нашего приложения.

Yahoo! предоставляет сервис для поиска организаций, расположенных поблизости от определенного адреса, в области, соответствующей определенному почтовому индексу и даже поиску по географическим координатам (широте и долготе, см. раздел Ресурсы). Если вы зарегистрировались и получили ключ разработчика, как предлагалось в статье Grails и REST, то вы сможете его использовать и в примерах к данной статье. Неудивительно, что форматы URI поисковых запросов, использовавшихся в той статье и для поиска по адресам, весьма схожи. В прошлой статье Web-сервис возвращал результаты в формате по умолчанию, т.е. в XML. Для указания того, что данные требуются в формате JSON, служит параметр output=json, который следует добавить к строке запроса.

Выполните показанный ниже запрос в браузере (убрав лишние переводы строк), и вы увидите список отелей, расположенных поблизости от международного аэропорта Денвера, в формате JSON.

http://local.yahooapis.com/LocalSearchService/V3/localSearch?appid=
   YahooDemo&query=hotel&latitude=39.858409881591797&longitude=
   -104.666999816894531&sort=distance

Сокращенная версия результатов в формате JSON приведена в листинге 14.

Листинг 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. Добавьте в него замыкание near, показанное в листинге 15 (нечто похожее вы видели в статье Сервисы Grails и Google Maps).

Листинг 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-тип application/json, поскольку иначе render будет использовать тип по умолчанию, которым является text/plain.

Выполните в браузере показанный ниже запрос.

http://localhost:9090/trip/hotel/near?lat=
   39.858409881591797&lng=-104.666999816894531

Сравните результаты с теми, которые были получены прямым запросом к сервису http://local.yahooapis.com. Они должны быть идентичны.

Почему нельзя вызывать удаленные Web-сервисы непосредственно из браузера?

Если передать URL local.yahooapis.com объекту Ajax.Request, то запрос завершится неудачно. Подобный запрос выполняется успешно в адресной строке браузера, однако приводит к ошибке при программном выполнении из JavaScript. Поверьте мне, так и было задумано.

Запросы Ajax должны следовать правилу "единого источника" (same source). Оно означает, что через Ajax можно запрашивать только ресурсы, находящиеся в том же домене, что и HTML-страница, с которой был произведен запрос. В нашем случае это значит, что можно выполнять запросы к http://localhost, но не к http://local.yahooapis.com или другим доменам.

Это было сделано из соображений безопасности. Например, вводя номер своей кредитной карты на сайте http://amazon.com, важно быть уверенным, что они не будут незаметно от вас посланы какому-нибудь http://hackers.r.us. Подобные действия известны под аббревиатурой XSS (межсайтовое скриптование).

Однако это правило относится только к клиентскому коду на JavaScript, и не относится к серверному коду на Groovy. Вследствие этого можно использовать контроллер в виде прокси при вызове сервиса http://local.yahooapis.com. Затем контроллер возвращает результат браузеру.

Если вы все же хотите обращаться к Web-сервисам Yahoo! или Google непосредственно из браузера, то можно использовать полулегальные варианты на основе обратных вызовов. Более подробную информацию о функциях обратного вызова в JSON можно получить по ссылкам, приведенным в разделе Ресурсы.

Использование контроллера для выполнения JSON-запросов к удаленным сервисам имеет два преимущества. Во-первых, оно позволяет обойти ограничение на Ajax-запросы (см. заметку Почему нельзя вызывать удаленные Web-сервисы непосредственно из браузера?). Второе и более важное преимущество заключается в инкапсуляции. В этом случае контроллер выполняет функции, чем-то похожие на функции объекта доступа к данным (DAO).

Жесткое задание URL удаленного Web-сервиса – это такой же отрицательный момент, как, например, использование SQL-операторов непосредственно в слое представления. Вызов локального контроллера защищает клиентов от возможных проблем, связанных с будущими изменениями в реализации. Подобно тому, как изменение имени таблицы или колонки может нарушить работу SQL-операторов, изменение URL приведет к потере работоспособности запросов Ajax. Если же использовать AirportMapping.iata(), то можно свободно менять источники данных – от локальных таблиц до удаленных сервисов, подобных GeoNames, не трогая при этом интерфейс клиентов. Более того, в будущем можно даже начать кэшировать данные, полученные от удаленных сервисов, в локальном хранилище в целях повышения быстродействия, обновляя состояние кэша при каждом запросе.

Теперь, после того как сервис заработал сам по себе, можно начать его вызывать с нашей Web-страницы.

Добавление ссылки для отображения отелей

Ссылку для показа ближайших отелей нет смысла отображать на карте, пока пользователь не отметит аэропорт назначения. По тем же причинам не стоит выполнять запрос к сервису до тех пор, пока пользователь не захочет увидеть отели. Мы начнем с того, что добавим функцию showHotelsLink() в скрипт на странице plan.gsp. Кроме того, добавим вызов этой функции в конец функции 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> в нижнюю часть <div> с идентификатором search, как показано в листинге 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. Отображение ссылки для показа ближайших отелей
Отображение ссылки для показа ближайших отелей

Далее необходимо реализовать функцию 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) }
  })
}

Здесь можно спокойно использовать метод createLink, поскольку базовая часть URL, указывающего на Hotel.near() не изменится при формировании страницы на стороне сервера. Динамическая часть URL будет формировать на клиенте при помощи JavaScript, а сам запрос будет выполняться знакомым методом, через Prototype.


Обработка ошибок

В целях упрощения примера мы не делали обработку ошибок при вызове через <g:formRemote>. Однако теперь происходит вызов удаленного сервиса (хотя и через локальный контроллер), поэтому было бы мудро выдавать осмысленные сообщения о возможных ошибках. Добавьте функцию displayError(), показанную в листинге 19, в скрипт на странице plan.gsp.

Листинг 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
}

Следует признать, что это решение недалеко ушло от простого вывода сообщений об ошибках в теле элемента hotels <div> сразу под ссылкой "Show Nearby Hotels" (т.е. там, где должен быть выведен список отелей). Однако вызов удаленного сервиса инкапсулируется внутри серверного контроллера, поэтому вы можете там же реализовать более тщательную обработку ошибок.

Добавьте элемент 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, получает данные в формате JSON от сервиса Yahoo! и формирует 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 строк кода, не правда ли? В этой статье было продемонстрировано, что JSON вполне реально представляет собой альтернативу XML при выполнении запросов Ajax. Вы увидели, насколько легко получать локальные данные в формате JSON от приложения Grails. Запрашивать JSON-данные от удаленных Web-сервисов также не представляет особых трудностей. Теги <g:formRemote> и <g:linkRemote>, входящие в состав Grails, можно использовать при серверном формировании HTML. Не менее важно знать, как использовать вызовы Ajax.Request, предоставляемые библиотекой Prototype, поскольку они играют критически важную роль в динамических приложениях Web 2.0.

В следующей статье мы рассмотрим в действии возможности Grails, связанные с технологией JMX (Java Management Extensions). До тех пор просто получайте удовольствие от экспериментов с Grails.

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java, Open source
ArticleID=497443
ArticleTitle=Изучение Grails: Асинхронная обработка данных в Grails при помощи JSON и Ajax
publish-date=06212010