Я люблю Ruby, и это мой переходный язык для создания веб-приложений. К сожалению, при работе с браузером Javascript является неизбежным злом. Как видите, я не большой поклонник.
Поэтому, когда кто-то приходит с предложением использовать Ruby в браузере, подпишите меня!
В первой части этой статьи я познакомлю вас с Opal и покажу, как настроить Opal. После этого мы перейдем к первой части нашего примера приложения. (Я буду держать вас в ожидании, пока вы не прокрутите вниз, что испортит удовольствие.)
Привет, Опал!
Тот, кто пришел, оказался Адамом Бейноном , создателем Опала . Opal — это компилятор исходного кода в Ruby to Javascript. Другими словами, Opal переводит Ruby, который вы пишете, в Javascript.
Получать опал
Запустите терминал:
% gem install opal opal-jquery Successfully installed opal-0.6.0 Successfully installed opal-jquery-0.2.0 2 gems installed
Обратите внимание, что мы также устанавливаем opal-jquery
. Этот гем оборачивает jQuery и предоставляет синтаксис Ruby для взаимодействия с DOM. Подробнее об этом позже.
Давайте опал в спине в irb
:
% irb > require 'opal' => true > Opal.compile("3.times { puts 'Ohai, Opal!' }") => "/* Generated by Opal 0.6.0 */\n(function($opal) {\n var $a, $b, TMP_1, self = $opal.top, $scope = $opal, nil = $opal.nil, $breaker = $opal.breaker, $slice = $opal.slice;\n\n $opal.add_stubs(['$times', '$puts']);\n return ($a = ($b = (3)).$times, $a._p = (TMP_1 = function(){var self = TMP_1._s || this;\n\n return self.$puts(\"Ohai, Opal!\")}, TMP_1._s = self, TMP_1), $a).call($b)\n})(Opal);\n"
Игра жизни Конвея в опале
Пришло время испачкать руки и намочить ноги опалом. Я всегда хотел повод построить игру жизни Конвея , так что это наша цель.
Если вы не знакомы с игрой Жизни Конвея (или лень читать статью в Википедии):
Это начинается с пустой сетки квадратных ячеек. Каждая клетка жива или мертва . Каждая клетка взаимодействует со своими восемью соседями.
Здесь 5 живых клеток. Остальные мертвы. Ячейка, отмеченная синей точкой, показана вместе с ее 8 соседями, отмеченными красными точками.
На каждом тике ячейка может проходить переход на основе четырех правил:
Правило 1
Любая живая клетка с менее чем двумя живыми соседями умирает, как если бы она была вызвана недостаточным населением
Правило 2
Любая живая клетка с двумя или тремя живыми соседями живет для следующего поколения.
Правило 3
Любая живая клетка с более чем тремя живыми соседями умирает, как если бы она была переполнена .
Правило 4
Любая мертвая клетка с ровно тремя живыми соседями становится живой клеткой, как будто путем размножения .
Удивительно, но только с этими 4 простыми правилами мы можем наблюдать очень интересные закономерности. Вот пример, названный легким космическим кораблем , взятым из ConwayLife.com :
Вот космический корабль в действии:
Настройка
Создайте пустой каталог, назовите его conway_gol
и создайте следующую структуру каталогов:
├── Gemfile ├── Rakefile ├── app │ ├── conway.rb ├── index.html └── styles.css
1. Gemfile
Заполните ваш Gemfile, чтобы он выглядел так:
source 'https://rubygems.org' gem 'opal' gem 'opal-jquery' gem 'guard' gem 'guard-rake'
После этого установите драгоценные камни:
% bundle install
2. Настройка охраны
Обратите внимание, что мы включили драгоценный камень.
Guard чрезвычайно удобен для разработки с Opal. Поскольку Opal является компилятором , он должен компилировать код Ruby, который вы написали в Javascript. Поэтому каждый раз, когда вы вносите изменения, вы перекомпилируете исходный код. Guard немного упрощает этот процесс.
Guard следит за определенными файлами или каталогами на основе правил, установленных в Guardfile
, который мы вскоре создадим. Он также поставляется с кучей удобных плагинов. Например, guard-rake
запускает задачу Rake при изменении файлов.
Затем в каталоге conway_gol
создайте Guardfile
используя следующую команду:
% bundle exec guard init 00:30:21 - INFO - rake guard added to Guardfile, feel free to edit it
Включите это правило в ваш Guardfile
.
guard 'rake', :task => 'build' do watch %r{^app/.+\.rb$} end
Это отслеживает любые изменения в любом файле Ruby в каталоге app
. Такое изменение вызовет задачу rake build
, о которой мы напишем далее.
3. Настройка Rakefile
В Rakefile
:
require 'opal' require 'opal-jquery' desc "Build our app to conway.js" task :build do env = Opal::Environment.new env.append_path "app" File.open("conway.js", "w+") do |out| out << env["conway"].to_s end end
Вот что делает задача build
Rake:
- Устанавливает каталог, в котором хранятся файлы Ruby
- Создает
conway.js
, результат компиляции Ruby -> Javascript.
4. Статические файлы
В index.html
:
<!DOCTYPE html> <html> <head> <script src="http://code.jquery.com/jquery-1.11.0.min.js"></script> <script src="http://code.jquery.com/jquery-migrate-1.2.1.min.js"></script> <link rel="stylesheet" href="styles.css"> </head> <body> <canvas id="conwayCanvas"></canvas> <script src="conway.js"></script> </body> </html>
Нам нужен JQuery. Мы также используем элемент canvas
HTML5, поэтому применяется обычное заявление об отказе «это не будет работать в более старых версиях Internet Explorer».
Наконец, мы помещаем созданный Opal conway.js
ниже элемента canvas. Это так, элемент canvas доступен для conway.js
.
В styles.css
:
* { margin: 0; padding: 0; }
Этот стиль избавляет от любых пробелов на границе, когда наша сетка рисуется.
5. Работа с опалом
Прежде чем мы используем Guard для автоматизации процесса, вот пример того, как вы будете взаимодействовать с Opal:
Перейдите в app/conway.rb
и введите это:
require 'opal' x = (0..3).map do |n| n * n * n end.reduce(:+) puts x
В вашем терминале, в conway_gol
, запустите задачу Rake:
% rake build
Откройте index.html
. Результат появится на консоли разработчика вашего браузера. Вот как это выглядит в Chrome:
Просто для conway.js
посмотрите, как выглядит сгенерированный conway.js
. Пока вы восхищаетесь созданным Javascript, уделите время размышлениям о великолепии команды Opal.
Так как у нас все Guard настроены, в другом окне терминала выполните эту команду:
% bundle exec guard 01:11:39 - INFO - Guard is using TerminalTitle to send notifications. 01:11:39 - INFO - Starting guard-rake build 01:11:39 - INFO - running build 01:11:41 - INFO - Guard is now watching at '/Users/Ben/conway_gol' [1] guard(main)>
Вот совет: всякий раз, когда вы хотите перезапустить rake build
, просто нажмите «Enter» в окне терминала Guard:
[1] guard(main)> (Hit Enter) 01:13:42 - INFO - Run all 01:13:42 - INFO - running build
Попробуйте внести изменения в conway.rb
:
require 'opal' x = (0..3).map do |n| n * n * n end.reduce(:*) # <- change to this puts x
Заметьте, что когда вы сохраняете conway.rb
(или любой другой файл Ruby), окно терминала с запущенным Guard выводит следующее сообщение:
09:25:11 - INFO - running build
При обновлении браузера отобразится обновленное значение.
Теперь, когда мы убедились, что все работает, продолжайте и удалите все в conway.rb
— conway.rb
интересное начинается сейчас!
Да начнется игра!
Наше приложение будет состоять из 2 основных компонентов. Первая часть — игровая логика. Вторая часть — это рисование холста и обработка событий холста.
Давайте сначала разберемся со второй частью.
1. Рисование пустой сетки
Вот для чего мы снимаем:
Линии сетки охватывают весь видовой экран браузера. Это означает, что нам нужно получить доступ к высоте и ширине окна просмотра. Что еще более важно, нам нужно получить доступ к элементу canvas в DOM, прежде чем мы сможем начать рисовать что-либо.
Класс Grid
Откройте conway.rb
в app
и заполните его следующим образом:
require 'opal' require 'opal-jquery' class Grid attr_reader :height, :width, :canvas, :context, :max_x, :max_y CELL_HEIGHT = 15; CELL_WIDTH = 15; def initialize @height = `$(window).height()` @width = `$(window).width()` @canvas = `document.getElementById(#{canvas_id})` @context = `#{canvas}.getContext('2d')` @max_x = (height / CELL_HEIGHT).floor @max_y = (width / CELL_WIDTH).floor end def draw_canvas `#{canvas}.width = #{width}` `#{canvas}.height = #{height}` x = 0.5 until x >= width do `#{context}.moveTo(#{x}, 0)` `#{context}.lineTo(#{x}, #{height})` x += CELL_WIDTH end y = 0.5 until y >= height do `#{context}.moveTo(0, #{y})` `#{context}.lineTo(#{width}, #{y})` y += CELL_HEIGHT end `#{context}.strokeStyle = "#eee"` `#{context}.stroke()` end def canvas_id 'conwayCanvas' end end grid = Grid.new grid.draw_canvas
Нам нужно явно требовать opal
и opal-jquery
.
Класс Grid
выглядит в основном как Ruby. На первый взгляд, у нас весь обычный синтаксис Ruby. Давайте рассмотрим каждую часть этого класса чуть более подробно, начиная с initialize
.
initialize
class Grid attr_reader :height, :width, :canvas, :context, :max_x, :max_y CELL_HEIGHT = 15; CELL_WIDTH = 15; def initialize @height = `$(window).height()` @width = `$(window).width()` @canvas = `document.getElementById(#{canvas_id})` @context = `#{canvas}.getContext('2d')` @max_x = (height / CELL_HEIGHT).floor @max_y = (width / CELL_WIDTH).floor end def canvas_id 'conwayCanvas' end ### snip snip ### end
В Opal мы можем оценить Javascript непосредственно в бэк-тиках. Например, чтобы получить высоту окна просмотра браузера:
@height = `$(window).height()`
Опал хранит значение @height
как класс Numeric
Ruby. Мы также используем $
, который вызывает экземпляр jQuery.
Работа с холстом
Не беспокойтесь, если вы никогда раньше не работали с элементом canvas
, так как в любом случае это не главное. Все, что я знаю о canvas
пришло из Dive Into HTML5 .
@canvas = `document.getElementById(#{canvas_id})`
Чтобы работать с холстом, вам нужна ссылка на него в DOM. Посмотрите, как мы можем использовать интерполяцию строк для заполнения идентификатора холста с помощью canvas_id
метода canvas_id
.
@context = `#{canvas}.getContext('2d')`
Что еще более важно, каждый холст имеет контекст рисования. Все рисование сделано через этот контекст. Обратите внимание, как мы снова используем интерполяцию строк для передачи в canvas
, извлечения контекста и сохранения его в @context
.
@max_x = (height / CELL_HEIGHT).floor @max_y = (width / CELL_WIDTH).floor
@max_x
и @max_y
хранят пределы сетки в терминах координат, что объясняет, почему мы должны делить на CELL_HEIGHT
и CELL_WIDTH
.
draw_canvas
Вот как мы рисуем линии сетки на холсте. canvas
вызывается только для установки ширины и высоты. Весь рисунок обрабатывается вызовами функций в context
.
draw_canvas
— хороший пример того, как Opal позволяет вам использовать код Ruby и Javascript вместе в идеальной гармонии.
def draw_canvas `#{canvas}.width = #{width}` `#{canvas}.height = #{height}` x = 0.5 until x >= width do `#{context}.moveTo(#{x}, 0)` `#{context}.lineTo(#{x}, #{height})` x += CELL_WIDTH end y = 0.5 until y >= height do `#{context}.moveTo(0, #{y})` `#{context}.lineTo(#{width}, #{y})` y += CELL_HEIGHT end `#{context}.strokeStyle = "#eee"` `#{context}.stroke()` end
Давайте посмотрим на холст
Наконец, получение сетки для рисования — это простой вызов метода:
grid = Grid.new grid.draw_canvas
Если вы используете Guard, нажмите «Enter», или вы можете запустить rake build
. В любом случае, когда вы откроете index.html
, вы увидите великолепную сетку.
2. Добавление некоторой интерактивности
Возможность рисовать линии сетки на canvas
— в Ruby, не меньше! — все хорошо, но совершенно бесполезно, если мы ничего не можем с этим поделать.
Давайте немного оживим.
Одна из вещей, которые мы можем сделать, это заполнить ячейку . Чтобы сделать это, нам нужно знать, где мы щелкнули, а затем вычислить положение ячейки относительно нашей сетки. То есть нам нужно выяснить координаты по щелчку на основе нарисованной нами сетки.
Еще до того, как узнать, где мы нажали, нам нужно знать, когда мы нажали. В этом разделе мы также рассмотрим, как opal-jquery
позволяет нам использовать Ruby для взаимодействия со слушателями событий jQuery.
Заполнить и заполнить ячейку
Следующие методы рисуют черный квадрат и очищают квадрат тех же размеров:
def fill_cell(x, y) x *= CELL_WIDTH; y *= CELL_HEIGHT; `#{context}.fillStyle = "#000"` `#{context}.fillRect(#{x.floor+1}, #{y.floor+1}, #{CELL_WIDTH-1}, #{CELL_HEIGHT-1})` end def unfill_cell(x, y) x *= CELL_WIDTH; y *= CELL_HEIGHT; `#{context}.clearRect(#{x.floor+1}, #{y.floor+1}, #{CELL_WIDTH-1}, #{CELL_HEIGHT-1})` end
Получение положения курсора
Вот пример перевода Javascript на Ruby. Я был слишком ленив и нетерпелив, чтобы понять, как реализовать эту функцию. Как оказалось, в Dive Into HTML 5 уже есть пример в Javascript:
function getCursorPosition(event) { var x; var y; if (event.pageX != undefined && event.pageY != undefined) { x = event.pageX; y = event.pageY; } else { x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; } }
Теперь давайте посмотрим, как выглядит Ruby со вкусом опала:
def get_cursor_position(event) if (event.page_x && event.page_y) x = event.page_x; y = event.page_y; else doc = Opal.Document[0] x = event[:clientX] + doc.scrollLeft + doc.documentElement.scrollLeft; y = event[:clientY] + doc.body.scrollTop + doc.documentElement.scrollTop; end end
Как видите, конверсия почти один в один.
Вы можете быть удивлены, почему существуют две ветви, чтобы найти позицию курсора. Короткий ответ: разные браузеры имеют разные способы реализации этой функциональности.
Открывая методы
Как я узнал, например, что метод page_x
существует для event
или что clientX
должен быть доступен с использованием хеш-записи?
def get_cursor_position(event) `console.log(#{event})` # <- I cheated. # code omitted end
Я не использовал puts event
или даже p event
. Я выбрал console.log
вместо этого.
Вот почему:
Использование console.log
дает нам гораздо больше подробностей, так как event
— это, прежде всего, объект Javascript. Использование puts
, p
или даже inspect
не так уж много.
В ветви if
мы обращаемся к event
с помощью точечной нотации, а в ветви else
мы воспринимаем event
как хеш
В зеленом поле event.page_x
— это вызов метода, потому что действительно есть функция page_x
определенная как $page_x: function { ... }
.
В фиолетовом поле значение clientX
является значением . Поэтому доступ к нему осуществляется с использованием хэш-записи.
Вот некоторый дополнительный код для вычисления координат относительно сетки.
def get_cursor_position(event) ## Previous code omitted ... x -= `#{canvas}.offsetLeft` y -= `#{canvas}.offsetTop` x = (x / CELL_WIDTH).floor y = (y / CELL_HEIGHT).floor Coordinates.new(x: x, y: y) end
Coordinates
и OpenStruct
Я пробрался в класс Coordinates
. Интересно, что в Opal есть и OpenStruct .
Вы можете определить Coordinates
следующим образом:
require 'opal' require 'opal-jquery' require 'ostruct' # <- remember to do this! class Grid # ... end class Coordinates < OpenStruct; end
Как и в случае с Ruby-версией, для ее использования нам require ostruct
.
Прослушивание событий
Наконец, у нас есть все строительные блоки для прослушивания событий. Оба слушателя будут слушать события на canvas
.
Первый прослушиватель событий запускается одним щелчком мыши. Как только это происходит, позиция курсора вычисляется и соответствующая ячейка заполняется.
Слушатель второго события срабатывает при двойном щелчке мыши. Опять же, позиция курсора вычисляется и соответствующая ячейка не заполняется.
def add_mouse_event_listener Element.find("##{canvas_id}").on :click do |event| coords = get_cursor_position(event) x, y = coords.x, coords.y fill_cell(x, y) end Element.find("##{canvas_id}").on :dblclick do |event| coords = get_cursor_position(event) x, y = coords.x, coords.y unfill_cell(x, y) end end
После рисования холста зарегистрируйте слушателя мыши:
class Grid # ... end grid = Grid.new grid.draw_canvas grid.add_mouse_event_listener # <- Add this!
Как только наши изменения index.html
откройте index.html
. Попробуйте нажать на любую сетку, чтобы отметить ячейку, и дважды щелкните, чтобы снять отметку с ячейки.
Вот мой шедевр:
Полный источник
Для справки вот полный исходный код:
require 'opal' require 'opal-jquery' require 'ostruct' class Grid attr_reader :height, :width, :canvas, :context, :max_x, :max_y CELL_HEIGHT = 15; CELL_WIDTH = 15; def initialize @height = `$(window).height()` # Numeric! @width = `$(window).width()` # A Numeric too! @canvas = `document.getElementById(#{canvas_id})` @context = `#{canvas}.getContext('2d')` @max_x = (height / CELL_HEIGHT).floor # Defines the max limits @max_y = (width / CELL_WIDTH).floor # of the grid end def draw_canvas `#{canvas}.width = #{width}` `#{canvas}.height = #{height}` x = 0.5 until x >= width do `#{context}.moveTo(#{x}, 0)` `#{context}.lineTo(#{x}, #{height})` x += CELL_WIDTH end y = 0.5 until y >= height do `#{context}.moveTo(0, #{y})` `#{context}.lineTo(#{width}, #{y})` y += CELL_HEIGHT end `#{context}.strokeStyle = "#eee"` `#{context}.stroke()` end def get_cursor_position(event) puts event p event `console.log(#{event})` if (event.page_x && event.page_y) x = event.page_x; y = event.page_y; else doc = Opal.Document[0] x = e[:clientX] + doc.scrollLeft + doc.documentElement.scrollLeft; y = e[:clientY] + doc.body.scrollTop + doc.documentElement.scrollTop; end x -= `#{canvas}.offsetLeft` y -= `#{canvas}.offsetTop` x = (x / CELL_WIDTH).floor y = (y / CELL_HEIGHT).floor Coordinates.new(x: x, y: y) end def fill_cell(x, y) x *= CELL_WIDTH; y *= CELL_HEIGHT; `#{context}.fillStyle = "#000"` `#{context}.fillRect(#{x.floor+1}, #{y.floor+1}, #{CELL_WIDTH-1}, #{CELL_HEIGHT-1})` end def unfill_cell(x, y) x *= CELL_WIDTH; y *= CELL_HEIGHT; `#{context}.clearRect(#{x.floor+1}, #{y.floor+1}, #{CELL_WIDTH-1}, #{CELL_HEIGHT-1})` end def add_mouse_event_listener Element.find("##{canvas_id}").on :click do |event| coords = get_cursor_position(event) x, y = coords.x, coords.y fill_cell(x, y) end Element.find("##{canvas_id}").on :dblclick do |event| coords = get_cursor_position(event) x, y = coords.x, coords.y unfill_cell(x, y) end end def canvas_id 'conwayCanvas' end end class Coordinates < OpenStruct; end grid = Grid.new grid.draw_canvas grid.add_mouse_event_listener
Следующий …
В следующем посте мы завершим наше приложение Conway’s Game of Life, реализовав игровую логику и подключив ее к нашей сетке. При этом мы также увидим еще несколько примеров того, как Opal изящно смешивает Ruby и Javascript,
Спасибо за прочтение!