Приглашаем на борт: AJAX, HTML-холст, и Суперпоезд
HTML-элемент canvas («холст»), впервые появившийся в браузере Safari фирмы Apple, позволяет веб-разработчикам создавать двумерные рисунки при помощи JavaScript. Теперь, когда его поддержка появилась и в недавно вышедшем Firefox 1.5, HTML-холст стал использоваться ещё шире (этот элемент даже планируется включить в стандарт HTML 5). Дело осталось только за Microsoft: неизвестно, сколько времени пройдёт, прежде чем поддержка элемента canvas появится и в Internet Explorer. Однако я верю, что при продолжающемся сейчас движении в сторону Web 2.0 HTML-холст оказывается неожиданно полезным ресурсом для всех тех приложений, которые могут пожертвовать поддержкой IE, — особенно в свете появления сейчас JavaScript-библиотек, позволяющих на полную мощь использовать класс XMLHttpRequest. Я попробовал использовать canvas и XHR совместно, и это произвело на меня сильное впечатление (интересно отметить забавное совпадение: их разработчики — фирмы Apple и Microsoft соответственно) :-)
Моим первым экспериментом по сочетанию canvas и AJAX была попытка воспроизвести наглядный словарь синонимов от Thinkmap, используя только HTML, JavaScript, и (на серверной стороне) базу данных WordNet. У HTML-холста обнаружились некоторые ограничения, с которыми я был вынужден столкнуться — в первую очередь, его неспособность отображать текст. С другой стороны, обнаружились и его сильные стороны — такие, как простота и удобство интеграции с AJAX. То, что у меня получилось, вы можете увидеть по адресу awordlike.com.
Суперпоезд
В этой статье я собираюсь продемонстрировать вам менее сложный эксперимент, в котором HTML-холст будет использоваться для отображения в реальном времени состояния воображаемой железной дороги (загрузить файлы с примерами). Я не буду углубляться в детали использования JavaScript и Ruby; если вам требуется помощь в освоении этих языков, то материалы, с которыми вам стоит ознакомиться, приведены в разделе «Ссылки». Теперь перейдём к делу.
Штат Вашингтон завершил, наконец, создание Суперпоезда — системы пригородных поездов, призванной избавить окрестности Сиэтла от их знаменитых транспортных пробок. Частью этого проекта являлась разработка программного комплекса, передающего в центр управления Суперпоездом координаты каждого поезда. Команда разработчиков этого комплекса потрудилась на славу: создана система рассылки сообщений, позволяющая любому пользователю в сети Суперпоезда получать оповещения о состоянии поездов. К несчастью, они уделили меньше внимания пользовательскому интерфейсу этого комплекса, и диспетчеры Суперпоезда вынуждены довольствоваться текстовой веб-страничкой, которую всякий раз, когда требуется получить свежую информацию, нужно обновлять в браузере вручную. Моё задание состояло в создании нового пользовательского интерфейса, который бы графически отображал состояние всех поездов Суперпоезда в реальном времени. Сначала, в качестве эксперимента, требовалось создать новый интерфейс только для одной линии Суперпоезда. Но прежде чем я смог бы задействовать с клиентской стороны AJAX и canvas, нужен был способ опрашивать состояние поездов через веб-сервер. Я начал с малого: первым делом запустил Ruby WEBrick, и подключил замыкание docroot:
server.rb
require 'webrick'
include WEBrick
server = HTTPServer.new( :Port => 8053 )
server.mount("/", HTTPServlet::FileHandler, "./docroot")
server.mount_proc("/train/line") do |request, response|
response['Content-Type'] = "text/plain"
response.body = "toot, toot"
end
trap("INT") { server.shutdown }
server.start
Если вы запустите этот скрипт ( ruby server.rb) и откроете в браузере адрес http://localhost:8053/train/line, то вы должны увидеть это:
Рисунок 1.
Я создал папку docroot, в которой буду размещать HTML-файлы. Пока что пусть там лежит заглушка:
docroot/redwood.html
<html>
<body>
hello woodinville!
</body>
</html>
Теперь я создам замыкание /train/line, которое будет выводить чуть более полезную информацию. В качестве протокола между клиентом и сервером я использую JSON, потому что это самое простое, что только можно использовать из JavaScript.
server.rb
...
require 'trainspotter'
...
train_spotter = TrainSpotter.new
server.mount_proc("/train/line") do |request, response|
response['Content-Type'] = "text/plain"
json = train_spotter.status_report.
map { |train| '{"track": "' + train.track.to_s + '",
"location": ' + train.location.to_s + '}' }.
join ','
response.body = "[ #{json} ]"
end
...
trainspotter.rb
class TrainSpotter
def status_report
[ Status.new("south", 20) ]
end
end
class Status
attr_reader :track, :location
def initialize(track, location)
@track = track
@location = location
end
end
Открыв в браузере страницу по адресу http://localhost:8053/train/line, теперь можно увидеть нечто лишь чуть более полезное но всё ещё впереди.
Рисунок 2.
То, что мне на самом деле нужно — чтобы объект TrainSpotter вёл себя так, как будто он содержит постоянно обновляющийся отчёт о состоянии поезда. Пока что я задам ему простую систему поведения, которая давала бы мне достаточно правдоподобные данные:
trainspotter.rb
TRACKS = [:north, :south]
TRAINS_PROGRESS = {:north => 5, :south => 420}
MAX_SPEED = 5
class TrainSpotter
def status_report
report = []
TRAINS_PROGRESS[:north] += rand(MAX_SPEED)
report << Status.new("north", TRAINS_PROGRESS[:north])
TRAINS_PROGRESS[:south] -= rand(MAX_SPEED)
report << Status.new("south", TRAINS_PROGRESS[:south])
end
end
...
Теперь, когда я открываю в браузере адрес http://localhost:8053/train/line и обновляю страницу несколько раз подряд, я вижу, как данные изменяются! Прекрасно видно, как один поезд едет на юг, из Вудинвилля в Редмонд, а другой — на север, из Редмонда в Вудинвилль.
Рисунок 3.
Следующий этап — задействование AJAX в redwood.html, чтобы не приходилось постоянно обновлять страницу вручную. Идеально подходит для этого невероятно простая библиотека Prototype:
docroot/redwood.html
<html>
<head>
<script type="text/javascript" src="prototype-1.4.0.js"></script>
</head>
<body>
<div id="status"></div>
<script type="text/javascript">
new Ajax.PeriodicalUpdater($("status"), "/train/line")
</script>
</body>
</html>
Открыв в браузере страницу http://localhost:8053/redwood.html, я теперь вижу, что состояние поездов обновляется каждые две секунды (таков интервал опроса по умолчанию у объекта Ajax.PeriodicalUpdater библиотеки Prototype). Здорово! Но думаю, что всего этого ещё недостаточно, чтобы поразить заказчика. Настало время заменить отчёт о состоянии динамически обновляющимся рисунком на клиентской стороне. Я по-прежнему начинаю с малого — то есть, как и следует в любом железнодорожном проекте, с рельсов:
docroot/redwood.html
...
<body>
<canvas
id="redwood"
width="500"
height="120"
style="border: 1px solid black">
</canvas>
<script type="text/javascript">
var tracks = {
north: new Track(30),
south: new Track(85)
}
var canvas = undefined
// IE will return false here
if ($("redwood").getContext) {
canvas = $("redwood").getContext("2d")
drawTracks()
}
function drawTracks() {
$H(tracks).values().each(function(track) {
track.draw()
})
}
function Track(y) {
this.y = y
this.startX = 10
this.endX = 490
this.tieSize = 3
this.tieGap = 5
this.draw = drawTrack
}
function drawTrack() {
canvas.moveTo(this.startX, this.y)
canvas.beginPath()
var x = this.startX
while (x < this.endX) {
canvas.lineTo(x, this.y)
canvas.lineTo(x, this.y + this.tieSize)
canvas.moveTo(x, this.y)
canvas.lineTo(x, this.y - this.tieSize)
canvas.moveTo(x, this.y)
x = x + this.tieGap
}
canvas.closePath()
canvas.stroke()
}
</script>
<div id="status"></div>
...
Рисунок 4.
Обратите внимание, что я не пользуюсь для организации циклов обычным синтаксисом JavaScript. Раз уж я использую для AJAX JavaScript-библиотеку Prototype (1.4.0), то логично воспользоваться встроенными в неё итераторами коллекций в стиле Ruby и другими синтаксическими приятностями, такими как $(), $H().values() и each(). Сейчас, когда рельсы уложены, нужно пустить по ним сами поезда. Первым делом я откажусь от использования объекта Ajax.PeriodicalUpdater и заменю его вызовом window.setInterval ( setIntervalиграет важную роль в организации динамического обновления HTML-холста). Ещё я оформлю drawTracks в виде функции более высокого уровня — updateCanvas.
docroot/redwood.html
...
if ($("redwood").getContext) {
canvas = $("redwood").getContext("2d")
window.setInterval(updateCanvas, 1000 * 2)
updateCanvas()
}
function updateCanvas() {
clearScreen()
drawTracks()
}
function clearScreen() {
canvas.clearRect(0, 0, $("redwood").width, $("redwood").height)
}
function drawTracks() {
...
Холст уже автоматически обновляется каждые 2000 миллисекунд (т.е. 2 секунды), но поездов ещё нет. Займёмся теперь ими. Картинки, изображающие поезда, можно наносить на HTML-холст методом drawImage элемента canvas. Сейчас, когда регулярное обновление производится методом window.setInterval, я могу считывать состояние поездов при помощи самого низкоуровневого объекта библиотеки Prototype — Ajax.Request. Как только я получу от сервера данные, я обновляю расположения картинок, изображающих поезда. Приведённый здесь код динамически обновляет положение поездов, отображая их движение в реальном времени:
docroot/redwood.html
var trains = {
north: new Train("train-lr.png", 5),
south: new Train("train-rl.png", 60)
}
...
function updateCanvas() {
clearScreen()
drawTracks()
new Ajax.Request("/train/line",
{ onComplete: function(request) {
var jsonData = eval(request.responseText)
if (jsonData == undefined) { return }
jsonData.each(function(data) {
trains[data.track].update(data.location)
})
}
})
}
...
function Train(image, y) {
this.image = new Image()
this.image.src = image
this.y = y
this.update = updateTrain
}
function updateTrain(location) {
canvas.drawImage(this.image, location, this.y)
}
...
Рисунок 5.
На этом этапе мы соединили вместе несколько механизмов. Используя Prototype, мы выполняем асинхронный вызов, получаем в ответ JSON-строку, и вызовом функции eval() преобразуем эту строку в массив JavaScript-объектов. Объекты Train описывают состояние каждого поезда и действия, необходимые для его отображения. У приведённого кода есть недостаток — неприятное мерцание изображений поездов. Оно происходит из-за того, что между очисткой экрана и вызовом события onComplete , обработчик которого рисует поезда, проходит некоторое время. Так что избавиться от надоедливого мерцания просто — достаточно вызывать clearScreen в самый последний момент перед отрисовкой поездов.
docroot/redwood.html
...
function updateCanvas() {
new Ajax.Request("/train/line",
{ onComplete: function(request) {
var jsonData = eval(request.responseText)
if (jsonData == undefined) { return }
clearScreen()
jsonData.each(function(data) {
trains[data.track].update(data.location)
})
drawTracks()
drawHotspots()
}
})
}
...
Осталось добавить последнюю деталь. Расположения поездов — это лишь один из типов информации о линии Суперпоезда, которую мы можем отображать в нашей системе. Кроме этого, программный комплекс Суперпоезда отслеживает «горячие точки» — те участки железнодорожного пути, на которых происходили столкновения или незапланированные остановки. Я начну добавление в нашу систему этих горячих точек с того, что жёстко задам их расположения в классе TrainSpotter и подключу к WEBrick новое замыкание, которое бы передавало эти расположения веб-клиенту.
server.rb
...
server.mount_proc("/train/line") do |request, response|
response['Content-Type'] = "text/plain"
json = train_spotter.status_report.
map { |train| '{"track": "' + train.track.to_s + '",
"location": ' + train.location.to_s + '}' }.
join ','
response.body = "[ #{json} ]"
end
server.mount_proc("/train/hotspots") do |request, response|
response['Content-Type'] = "text/plain"
json = train_spotter.hot_spots
map { |train| '{"track": "' + train.track.to_s + '",
"location": ' + train.location.to_s + '}' }.
join ','
response.body = "[ #{json} ]"
end
...
trainspotter.rb
class TrainSpotter ... def hot_spots [ Status.new(:north, 125), Status.new(:south, 250), Status.new(:south, 150) ] end end
Надеюсь, вы не удовлетворены приведённым кодом замыкания /train/hotspots — действительно, он практически совпадает с кодом /train/line. Можно избавиться от возникшего дублирования, выделив в отдельную функцию преобразование объектов Status в JSON-строки.
server.rb
...
def status_list_to_json(list)
json = list.
map { |train| '{"track": "' + train.track.to_s + '",
"location": ' + train.location.to_s + '}' }.
join ','
"[ #{json} ]"
end
server.mount_proc("/train/line") do |request, response|
response['Content-Type'] = "text/plain"
response.body = status_list_to_json(train_spotter.status_report)
end
server.mount_proc("/train/hotspots") do |request, response|
response['Content-Type'] = "text/plain"
response.body = status_list_to_json(train_spotter.hot_spots)
end
...
Если вы сейчас откроете в браузере адрес http://localhost:8053/train/hotspots, то вы увидите это:
Рисунок 6.
Теперь надо добавить отображение горячих точек на клиентской стороне. Поскольку их расположения меняются не так часто, как расположения поездов, то я буду использовать отдельный вызов window.setInterval, чтобы запрашивать у сервера данные о горячих точках раз в час.
docroot/redwood.html
...
<script type="text/javascript">
...
var hotspots = []
var canvas = undefined
if ($("redwood").getContext) {
canvas = $("redwood").getContext("2d")
window.setInterval(updateCanvas, 1000 * 2)
window.setInterval(updateHotspots, 1000 * 60 * 60)
updateCanvas()
updateHotspots()
}
function updateHotspots() {
new Ajax.Request("/train/hotspots",
{ onComplete: function(request) {
hotspots = eval(request.responseText)
}
})
}
function updateCanvas() {
new Ajax.Request("/train/line",
{ onComplete: function(request) {
var jsonData = eval(request.responseText)
if (jsonData == undefined) { return }
clearScreen()
jsonData.each(function(data) {
trains[data.track].update(data.location)
})
drawTracks()
drawHotspots()
}
})
}
...
function drawHotspots() {
hotspots.each(function(hotspot) {
tracks[hotspot.track].drawHotspot(hotspot.location)
})
}
function Track(y) {
...
this.hotspotRadius = 6
this.hotspotColor = "red"
this.drawHotspot = drawHotspot
}
...
function drawHotspot(location) {
canvas.moveTo(location, this.y)
canvas.beginPath()
canvas.fillStyle = this.hotspotColor
canvas.arc(location, this.y, this.hotspotRadius, 0, Math.PI,
false)
canvas.closePath()
canvas.fill()
}
...
</script>
...
Рисунок 7.
Эксперимент успешно завершён! Заказчик, представляющий штат Вашингтон, полностью удовлетворён созданной системой. Теперь моё задание состоит в интеграции класса TrainSpotter с используемой в сети Суперпоезда системой рассылки сообщений. Если моя система покажет себя на настоящей линии Вудинвилль—Редмонд достаточно хорошо, то мне предстоит ещё немало работы над этим комплексом…
Ссылки
- Файлы с примерами к статье
- Работа с HTML-холстом
- JavaScript-библиотека Prototype
- Javascript
- Ruby
- WEBrick
- Наглядный словарь синонимов
- Изображения поездов
- WordNet
Перевод: А.Скробов
Опубликовано на XML.com (англ.): http://www.xml.com/pub/a/2006/01/18/ajax-html-canvas-ruby.html
Опубликовано на xmlhack.ru (рус.): http://xmlhack.ru/texts/06/ajax-html-canvas-ruby/ajax-html-canvas-ruby.html