Search
Write a publication
Pull to refresh

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
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.