Как стать автором
Обновить

AJAX: Как за 15 минут подружить Grails c Dojo Toolkit и CometD.

В этой статье будет продемонстрирована работа с Dojo Toolkit и CometD на Grails.

Дана максимально краткая инструкция, как их всех подружить и при помощи Dojo выполнить обычный AJAX запрос через стандартный тэг <g:remoteLink update="">, как использовать Dijit виджеты в *.gsp, и как обеспечить передачу данных с сервера прямо в броузер по протоколу Bayeux.


Итак, у вас установлен JDK и распакован grails (http://www.grails.org/Download), а так же установлены переменные JAVA_HOME, GRAILS_HOME.
Ну и $GRAILS_HOME/grails/bin находится в вашем PATH.

Выберете любую папку и наберите:
grails create-app demo

Перейдите в новосозданную папку 'demo' и проверьте что пустое приложение стартует…
grails run-app
....
Running Grails application..
Server running. Browse to http://localhost:8080/demo

… и вы видете страничку на
http://localhost:8080


Теперь нажмите Ctrl+C в консоли и продолжим.

Выкачайте и установите плагин cometd: grails install-plugin cometd
...
Resolving plugin JAR dependencies ...
Plugin cometd-0.2.2 installed


Выкачайте и установите плагин dojo: grails install-plugin cometd
...
Resolving plugin JAR dependencies ...
Plugin cometd-0.2.2 installed
...


Выкачайте и установите плагин Dojo: grails install-plugin dojo
...
Done.'/web-app/js/dojo/1.4.3/dojo/dojo.js' has been copied into the application.
(Optional) - You may install the full Dojo Toolkit by running 'grails install-dojo'.

Plugin dojo-1.4.3.2 installed
Plugin provides the following new scripts:
------------------------------------------
grails install-dojo

Установите все дополнительные скрипты dojo
grails install-dojo
...
[copy] Copying 2476 files to D:\Work\demo\web-app\js\dojo\1.4.3
Dojo 1.4.3 was downloaded and copied to the application.


Проверьте, что наш самолет взлетит со всеми этими плагинами, и вы видете страничку на localhost:8080
Теперь нажмем Ctrl+C и начнем собственно писать код.

Server Side


Команда "grails create-service Stock" создаст нам пустой класс в grails-app\services\demo\StockService.groovy

Отредактируем сервис, можно попробовать использовать copy/paste.

package demo

import org.springframework.beans.factory.InitializingBean
import grails.converters.*

static class Quote {
  float lastTrade
  float change
  int volume
}

class StockService implements InitializingBean {
  // we don't do anything transactional
  boolean transactional = false

  // let it be auto-wired from Cometd Plugin
  def bayeux

  // our publisher
  def client

  def quotes = []

  def goOn = true
  def int sleepSeconds = 15

  // just like @PostConstruct
  void afterPropertiesSet() {
    def session = bayeux.newLocalSession()
    //def sSession = bayeux.newServerSession(session, "server")
    session.handshake()

    bayeux.createIfAbsent('/quotes/AAPL');
    bayeux.createIfAbsent('/quotes/GOOG');
    bayeux.createIfAbsent('/quotes/YHOO');
        quotes = [session.getChannel('/quotes/AAPL'), session.getChannel('/quotes/GOOG'), session.getChannel('/quotes/YHOO')]
    start();
  }

  String stop(){
     goOn = false
     return "Publishing thread stopped"
  }

  String defineSleepSeconds(int seconds){
     sleepSeconds = seconds
     return "Publishing new data every ${seconds} seconds"
  }

  String start(){
       goOn = true
    Thread.startDaemon {
      def rnd = new Random()
      while (goOn) {
        quotes.each {
          // I'm lazy, just generate random numbers
          def q = new Quote(
            lastTrade:((float)rnd.nextInt(100) / 10f),
            change: (0.5f - (float)rnd.nextInt(100) / 100f),
            volume:rnd.nextInt(500)
          )
          // on behalf of client, publish JSON result to selected channel
          it.publish(q as JSON)
        }
        try {
          // delay
          println "Published, sleeping "+sleepSeconds  
          Thread.sleep(sleepSeconds*1000)
        } catch (InterruptedException ex) {
          // do nothing
        }
      }
    }
     return "Publishing thread started";
  }
}

* This source code was highlighted with Source Code Highlighter.

Сервис беcполезен без клиента, команда "grails create-controller Stock" создаст нам контроллер в grails-app\controllers\demo\StockController.groovy
Отредактируем его

package demo

class StockController {
  
  def stockService

  //any call to this controller will initialize StockService and start publishing
  def index = {
  render "Initialized ${stockService.quotes}"
  }

  def stop = {
  render stockService.stop()
  }

  def start = {
  render stockService.start()
  }

  def seconds = {
  render stockService.defineSleepSeconds(Integer.valueOf(params.seconds))
  }
}



Проверим, что всё компилится и работает.
grails run-app

Зайдем на
http://localhost:8080/demo


На первой странице появился наш контроллер, переход по ссылке выдает что-то типа
Initialized [/quotes/AAPL@L:11pekx09lbjs6m102p8g3j9sl3v, /quotes/GOOG@L:11pekx09lbjs6m102p8g3j9sl3v, /quotes/YHOO@L:11pekx09lbjs6m102p8g3j9sl3v]

В консоле виден вывод println.
Наш демон начал публиковать фальшивые сводки, вот только никто их пока не принимает.

Client Side


Будем издеваться над первой страницей
grails-app/view/index.gsp


Но сначала выберем Dijit theme, например tundra.
Откроем grails-app/views/layouts/main.gsp и изменим body на body class=«tundra» и добавим ссылку на CSS в HEAD
 <link rel="StyleSheet" type="text/css" href="${resource(dir:'js/dojo/1.4.3/dijit/themes/tundra', file:'tundra.css')}"/>



Теперь открываем grails-app/views/index.gsp и добавим в head инициализацию dojo
 <script type="text/javascript">
   var djConfig = {
    parseOnLoad : true,
  isDebug : true
   };
 </script>
 <g:javascript library="dojo" />



Эти параметры добавили нам на страничку консоль.
А теперь добавим еще инициализацию пары виджетов, ну и клиентского comet
 <script type="text/javascript">
   dojo.require("dijit.layout.ContentPane"); //загрузка скрипта сплиттера
   dojo.require("dijit.layout.BorderContainer");
     dojo.require('dojox.cometd'); //загрузка скрипта клиента cometd

    dojo.addOnLoad(init); //инициализируем клиент кометд по окончанию загрузки страницы
   dojo.addOnUnload(destroy); //отпишемся от кометд по выгрузке

   function init() { //подключимся к cometd
      dojox.cometd.init("${createLink(controller:'cometd')}");
      //подпишемся на все 3 канала
      dojox.cometd.subscribe('/quotes/AAPL', onMessage);
      dojox.cometd.subscribe('/quotes/GOOG', onMessage);
      dojox.cometd.subscribe('/quotes/YHOO', onMessage);
   }

   function destroy() { //отключимся
    dojox.cometd.unsubscribe('/quotes/AAPL');
    dojox.cometd.unsubscribe('/quotes/GOOG');
    dojox.cometd.unsubscribe('/quotes/YHOO');
    dojox.cometd.disconnect();
   }
     
    function onMessage(m) { //получим сообщение и проапдейтим таблицу
    var k = m.channel.substring(8, 12);
    var o = eval('('+m.data+')');
    dojo.byId(k + '_lastTrade').value = o.lastTrade;
    dojo.byId(k + '_change').value = (o.change + '').substring(0, 4);
    dojo.byId(k + '_volume').value = o.volume;
   }
 </script>



Что теперь? Надо написать сам html. Добавим в самый конец всё той же index.gsp

  <div dojoType="dijit.layout.BorderContainer" design="headline" style="height:300px;border:solid 3px;">
      <div dojoType="dijit.layout.ContentPane" region="top" style="height:20px;" splitter="true" minSize=10>
     <g:remoteLink controller="stock" action="start" update="stock">Start</g:remoteLink> 
       <g:remoteLink controller="stock" action="stop" update="stock">Stop</g:remoteLink> 
       <g:remoteLink controller="stock" action="seconds" params="[seconds:3]" update="stock">Every 3 sec</g:remoteLink> 
       <g:remoteLink controller="stock" action="seconds" params="[seconds:10]" update="stock">Every 10 sec</g:remoteLink> 
       <g:remoteLink controller="stock" action="seconds" params="[seconds:30]" update="stock">Every 30 sec</g:remoteLink>
      </div>

      <div dojoType="dijit.layout.ContentPane" region="center">
      <div id="stock"> </div>
     <form action="">
    <table border="1" cellpadding="2" cellspacing="0">
    <tr>
      <td>Symbol</td>
      <td>Last Trade</td>
      <td>Change</td>
      <td>Volume</td>
    </tr>
    <tr>
      <td>AAPL</td>
      <td><input type="text" id="AAPL_lastTrade"></td>
      <td><input type="text" id="AAPL_change"></td>
      <td><input type="text" id="AAPL_volume"></td>
    </tr>
    <tr>
      <td>GOOG</td>
      <td><input type="text" id="GOOG_lastTrade"></td>
      <td><input type="text" id="GOOG_change"></td>
      <td><input type="text" id="GOOG_volume"></td>
    </tr>
    <tr>
      <td>YHOO</td>
      <td><input type="text" id="YHOO_lastTrade"></td>
      <td><input type="text" id="YHOO_change"></td>
      <td><input type="text" id="YHOO_volume"></td>
    </tr>
    </table>
    </form>
    </div>
    </div>

* This source code was highlighted with Source Code Highlighter.

Что ж тут понаписано?
<div dojoType=«dijit.layout.BorderContainer»/> определяет BorderContainer с парой областей и сплиттером между ними
В этом примере он нафиг не сдался, но пусть будет для демонстрации dijit.

<g:remoteLink controller=«stock» action=«start» update=«stock»>Start</g:remoteLink>
Асинхронно вызывает метод start контроллера StockController и присваивает полученный ответ в innerHTML тега с id=«stock»
Простейший content-oriented AJAX в действии.
Конечно сгенеренный remoteLink'ом скрипт у Dojo выглядит хуже, чем у Prototype:
<a href="/demo/stock/start" onclick="dojo.xhr('Get',{preventCache:false, url:'/demo/stock/start', load:function(response){dojo.attr(dojo.byId('stock'),'innerHTML',response); if(dojo.parser){dojo.parser.parse(dojo.byId('stock'))} }, handle:function(response,ioargs){ }, error:function(error,ioargs){dojo.attr(dojo.byId('stock'),'innerHTML',ioargs.xhr.responseText); } });return false;">Start</a>


Но мы же его не видим в нашем коде ;)

<g:remoteLink controller=«stock» action=«stop» update=«stock»>Stop</g:remoteLink> 
останавливает тред паблишера.

А 3 разных тега
<g:remoteLink controller=«stock» action=«seconds» params="[seconds:3]" update=«stock»>Every 3 sec</g:remoteLink>
меняют delay между публикациями.

В таблице же находятся поля, значения в которых меняет функция onMessage(m)

Пример готов, запустим его. Нажмем ссылку start и получим что-то вроде screen.png.

В опере можно полюбоваться на длинный polling запросы, которые оканчиваются, только когда у сервера появляется сообщение для клиента.
В сервисе, конечно же есть пара логических ошибок, но сometd таки работает.

Ссылки:
www.grails.org/plugin/cometd
cometd.org/documentation
www.grails.org/plugin/dojo
dojotoolkit.org
Общий взгляд на Dojo Toolkit
Книга Comet and Reverse Ajax: The Next-Generation Ajax 2.0
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.