На днях я зарелизил новую версию своего gem Gon – 4.0.0 и решил привести пару примеров его возможностей и использования. Данная библиотека служит для упрощения работы с данными в MVC архитектуре. Она позволяет работать с данными контроллера из JS пропуская шаги перекидывания этих данных через вью. На сегодняшний день существуют реализации гона для RoR приложений, sinatra-like приложений (sinatra, padrino, etc.) и для .Net MVC.
Карта в админке
Передо мной стояла задача — реализовать разделение предложений в Групоне по территориальным областям, которые бы мог редактировать администратор. Предложения из областей показываются людям которые живут в этих областях с большим приоритетом нежели предложения из других областей.
Решено было сделать в админке интерактивную карту на основе Яндекс.Карты, на которой должны отображаться точки предложений и области. Области можно добавлять и редактировать и их координаты соответственно будут храниться в базе данных. Яндекс.Карты предоставляют обширный API для рисования различных элементов на картах, мне подошли оттуда многоугольники для отображения областей и точки с тултипами для предложений. Скриншот ниже без точек предложений, тк секретная информация ;)
Я создал несколько моделей данных — CityArea для описывания сущности территориальной области в городе, CityAreaPoint для хранения координат точек из которых состоит CityArea, и AreaOffer — принадлежность предложения к определенной области. Подготовил страницу, JS для рисования областей, серверную логику для связывания объектов друг с другом и встал вопрос о выводе данных на страницу. Один из стандартных способов в MVC для меня – это выбрать нужные данные в экшене контроллера, отредактировать их до необходимого формата, записать в переменную конечный массив данных и далее во вью добавить элемент с data-атрибутом в который эти данные будут записаны и откуда будут считаны JS-ом. То есть я сделал тогда примерно вот так:
app/controllers/admin/map_controller.rb
@location = City.find(params[:city_id].to_i)
@offers = Offer.sorted.by_city(@location).by_day(params[:date])
@offers.map! do |offer|
points = offer.points.map do |point|
{
id: it.id,
longitude: it.longitude,
latitude: it.latitude
}
end
{
id: offer.id,
permalink: offer.permalink,
title: offer.short_title,
points: points
}
end
@city_areas = CityArea.find_by_city @location
@city_areas.map! do |area|
# то же что и с офферами почти
end
app/views/admin/map/map.html.haml
.data-container{ data: { areas: @city_areas.to_json, offers: @offers.to_json } }
app/assets/javascripts/admin/map.js
$('.data-container').data('offers')
...
Получился очень загруженный контроллер — слишком много кода, которого там не должно быть. Можно вынести в модель, но код останется все равно примерно таким. Я решил использовать свой gem gon который имеет поддержку темплейтов rabl и jbuilder. Rabl — прекрасный gem, который отлично работает с коллекциями объектов и с ассоциациями объектов бд, поэтому код внутри шаблонов получается более оптимальным. Ну а гон позволяет использовать мощь rabl легко и просто:
app/controllers/admin/map_controller.rb
@location = City.find(params[:city_id].to_i)
@offers = Offer.sorted.by_city(@location).by_day(params[:date])
@city_areas = CityArea.find_by_city @location
gon.rabl template: 'app/rabl/offers.json.rabl', as: 'offers'
gon.rabl template: 'app/rabl/areas.json.rabl', as: 'areas'
app/rabl/offers.json.rabl
collection @offers => 'offers'
attributes :id, :permalink, :short_title
child :points do
attributes :id, :latitude, :longitude
end
app/assets/javascripts/admin/map.js
gon.offers
…
Таким образом затратив минимум усилий на преобразование данных и прокидывание этих данных в JS я получил «чистый» контроллер и массивы нужных мне данных в JS.
Gon gem
Помимо работы с шаблонами rabl и jbuilder, гон отлично подходит для вывода каких-либо начальных данных или глобальных данных, которые задаются в любой точке проекта и доступны из любой точки проекта. То есть например если значение какой-то переменной должно быть на всех страницах проекта — достаточно задать эту переменную в инишалайзере. Для этого используется метод gon.global, который работает аналогично с gon за исключением того что область видимости переменных записанных в него глобальная. В JS переменные доступны через неймспейс gon.global.
Кроме того, вчера я зарелизил версию 4.0.0 в которой появилась функциональность обновления данных в переменной без перезагрузки страницы — gon.watch. Новая функциональность позволяет обновлять данные как в цикле с промежутком в n-секунд, так и разово, ответом на какое-либо событие. В качестве примера я покажу как несколькими строками кода можно вывести в реальном времени результаты команды top:
Допустим у нас есть новое rails приложение. Для отображения терминального топа нам достаточно сделать следующее:
1. Добавляем в Gemfile строку gem 'gon'
и запускаем bundle install
2. Добавляем контроллер с экшеном например home#foo, вью для него app/views/home/foo.html.erb
и JS app/assets/javascripts/home.js.coffee
rails g controller home foo
3. В экшене контроллера app/controllers/home_controller.rb
записываем в gon.watch переменную разовое выполнение команды top:
def foo
gon.watch.top = `top -l1`
end
4. В лэйауте app/views/layouts/application.html.erb
добавляем хелпер с опцией watch в head:
<head>
<%= include_gon(watch: true) %>
5. Во вью app/views/home/foo.html.erb
добавляем тег, в котором у нас будет выводиться top:
<pre style='font-family:monospace;'></pre>
6. В кофе файл добавляем код который у нас будет следить за переменной и обновлять содержимое тега pre:
$(document).ready ->
renewPre = (data) ->
$('pre').html(data.replace(/\n/, "n"))
gon.watch('top', interval: 1000, renewPre)
gon.watch
принимает два или три параметра — название переменной, опции (опциональные), callback. В данном случае мы передали опцию interval: 1000
, которая зациклит выполнение кода gon.watch с периодичностью в 1 секунду. Это означает, что каждую секунду gon.watch
посылает запрос в экшн контроллера в котором была назначена переменная и возвращает актуальное значение, при успешном выполнении запроса вызывается callback, который в свою очередь переписывает содержимое тега pre. Чтобы остановить цикл существует функция gon.unwatch, которая принимает параметрами имя переменной и callback функцию. Естественно, если не передавать опцию interval
, то цикла не будет, будет разовый запрос и в случае его успешности — разовый вызов callback-а.
Вот и все, запускаем rails s
, открываем 0.0.0.0:3000/foo
— там у нас живой top.
Автор: gazay