Быстрая разработка
Вдохновленный постами на западных блогах вроде «Clone TinyURL with 40 lines of Ruby» или «Clone Pastie in 15 Minutes with Sinatra & DataMapper» я решил попробовать пройти и заодно описать весь процесс реализации легковесного веб-приложения на руби, от проектирования до деплоймента.
Инструменты
Для руби есть огромное количество различных инструментов для быстрой разработки. Я остановился на следующих:
Sinatra — DSL для веба. Легковесный фреймворк, работающий по принципу «convention over configuration». Позволяет быстро и легко разрабатывать веб-приложения, и легко дополняется всем, что только может вам понадобиться. Основа нашего приложения.
DataMapper — ORM, главный конкурент ActiveRecord. В чем-то уступает, в чем-то превосходит вышеназванный, прекрасно работает с разными базами данных, легко конфигурируется и встраивается.
HAML — HTML для программистов. Язык разметки, чуть более красивый чем традиционный erb, генерирует чистый и валидный xhtml. Содержит в себе эквивалент для CSS — SASS.
Heroku — Позволяет удобно и даже бесплатно (конечно, с ограничениями) разместить получившееся приложение. Опциональный инструмент, деплоить можно куда угодно.
Что будем писать?
Выбрав инструменты, я задумался, а что же, собственно, написать? И решил, что это будет инструмент для организации своей комикс-ленты. В таком приложении имеет место и клиентский функционал, и админ-панель, и генерация rss-фида для подписки, что позволит затронуть разные аспекты разработки, и приблизить ее к реальным задачам. Ну и еще я люблю веб-комиксы:)
Разбор кода
Вот мы и подошли к самому интересному. Практически весь получившийся код легко понятен и найти его можно на github. Рекомендую открыть его, чтобы представлять общую картину, а я хотел бы остановится на наиболее важных фрагментах кода, и тех фрагментах, которые вызвали у меня некоторые затруднения в реализации.
Для начала разберем структуру проекта, она очень проста:
comics.rb
config.ru
models.rb
public
views
Файл models.rb содержит в себе модели, конфигурацию базы данных и все что касается работы с ней. comics.rb содержит весь код для синатры. Так-же синатра по умолчанию подхватывает папки views, содержащую представления на haml и public с файлами доступными из веба (javascript, изображения).
Начнем с моделей.
models.rb
- DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3:///#{Dir.pwd}/comics.db")
Параметры БД на heroku содержатся в ENV['DATABASE_URL'], если такой переменной нет, то создаем sqlite-базу в каталоге с проектом. Править в исходниках ничего не придется.
models.rb
- class DateTime
- def rfc822
- self.strftime "%a, %d %b %Y %H:%M:%S %z"
- end
- end
Спецификация RSS 2.0 требует дату в формате RFC #822. Для этого добавим объектам класса DateTime метод rfc822, который отформатирует timestamp нужным образом, и именно его будем использовать в дальнейшем в представлениях.
comics.rb
- def protected!
- response['WWW-Authenticate'] = %(Basic) and \
- throw(:halt, [401, "Not authorized]) and \
- return unless authorized?
- end
- def authorized?
- comics = Comics.first
- @auth ||= Rack::Auth::Basic::Request.new(request.env)
- @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == [comics.login, comics.password]
- end
Простая реализация аутентификации для Sinatra. Почти целиком взята из FAQ, отличие в том, что логин и пароль берутся из базы данных, вместо вшитых в исходник. Использовать предельно просто: достаточно в нуждающемся в аутентификации экшене вписать protected!
comics.rb
- get '/rss.xml' do
- content_type 'application/rss+xml', :charset => 'utf-8'
- @comics = Comics.first
- @strips = Strip.all :limit => 10
- haml(:rss, :layout => false)
- end
Отдача rss-фида. Меняем Content-Type, и добавляем: layout => false, чтобы фид не отрендерился в layout.
Теперь несколько хинтов в представлениях.
layout.haml
- %title= "#{@comics.title} — #{@strip.title}" rescue @comics.title
Если не использовать здесь механизм исключений, то при пустой переменной @strip нас остановит ошибка NoMethodError, так как у класса nil нет метода title. В руби такие штуки надо всегда держать в голове.
models.rb
- class Strip
- # объявляем property
- def next
- Strip.first(:created_at.gt => self.created_at, :order => [:created_at.asc])
- end
- def previous
- Strip.first(:created_at.lt => self.created_at)
- end
- def get_id
- self.id
- end
- default_scope(:default).update(:order => [:created_at.desc])
- end
layout.haml
- - tonext = "/#{@strip.next.get_id}" rescue "#"
- - toprevious = "/#{@strip.previous.get_id}" rescue "#"
Механизм понятен — поиск следующего и предыдущего стрипа для того, который смотрим сейчас, и вывод ссылок на них в представлении. Почему надо было делать отдельный метод get_id, вместо того чтобы напрямую использовать существующий id? Дело в том, что если мы смотрим последний на текущий момент стрип, то метод next вернет nil. А у nil в свою очередь есть метод id, который не долго думаю вернет «4». Можете убедиться в этом сами, поэкспериментировав в irb.
На этом с разбором можно и закончить, с удовольствием отвечу на любые вопросы по коду, и отреагирую на критику в комментариях.
Деплоймент
Написанное приложение легко запустится как и любое другое приложение на синатре командой ruby comics.rb. Но мы ведь хотим показать его миру, и в этом нам поможет Heroku. Регистрируемся на heroku, и устанавливаем себе на локальную машину gem heroku. Теперь пишем конфиг для Rack:
config.ru
- require 'comics'
- run Sinatra::Application
Следующим шагом создаем приложение на heroku, и пушим туда код. Условимся, что приложение уже лежит у вас в git-репозитории:
heroku create comics
git push heroku master
Осталось только забить базу данных начальными данными, для этого в models.rb есть метод install:
heroku console
install
Вот и все, можно заходить по адресу, который выдал gem при создании приложения, и если все сделано правильно, то наслаждаться результатом.
Ссылки
Comics на GitHub
Демо на Heroku (Админка, пароль по запросу в хабрапочту).