В этой статье будет продемонстрирована работа с 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
… и вы видете страничку на
Теперь нажмите Ctrl+C в консоли и продолжим.
Выкачайте и установите плагин cometd: grails install-plugin cometd
Выкачайте и установите плагин dojo: grails install-plugin cometd
Выкачайте и установите плагин Dojo: grails install-plugin dojo
Установите все дополнительные скрипты dojo
grails install-dojo
Проверьте, что наш самолет взлетит со всеми этими плагинами, и вы видете страничку на localhost:8080
Теперь нажмем Ctrl+C и начнем собственно писать код.
Команда "grails create-service Stock" создаст нам пустой класс в grails-app\services\demo\StockService.groovy
Отредактируем сервис, можно попробовать использовать copy/paste.
Дана максимально краткая инструкция, как их всех подружить и при помощи 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