Как я использовал gem gon в Групоне

в 8:47, , рубрики: .net, mvc, ruby, ruby on rails, rubygems, sinatra, Блог компании «Evil Martians», метки: , , , , ,

На днях я зарелизил новую версию своего gem Gon – 4.0.0 и решил привести пару примеров его возможностей и использования. Данная библиотека служит для упрощения работы с данными в MVC архитектуре. Она позволяет работать с данными контроллера из JS пропуская шаги перекидывания этих данных через вью. На сегодняшний день существуют реализации гона для RoR приложений, sinatra-like приложений (sinatra, padrino, etc.) и для .Net MVC.

Карта в админке

Передо мной стояла задача — реализовать разделение предложений в Групоне по территориальным областям, которые бы мог редактировать администратор. Предложения из областей показываются людям которые живут в этих областях с большим приоритетом нежели предложения из других областей.


Решено было сделать в админке интерактивную карту на основе Яндекс.Карты, на которой должны отображаться точки предложений и области. Области можно добавлять и редактировать и их координаты соответственно будут храниться в базе данных. Яндекс.Карты предоставляют обширный API для рисования различных элементов на картах, мне подошли оттуда многоугольники для отображения областей и точки с тултипами для предложений. Скриншот ниже без точек предложений, тк секретная информация ;)

image

Я создал несколько моделей данных — 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:

image

Допустим у нас есть новое 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

* - обязательные к заполнению поля