Дружимся с Rack #2: Rack::Builder

октября 25, 2010  |  Published in Rack  |  9 Comments

rack logoВ первой части мы использовали rackup для того, чтобы сделать возможным выбор порта и сервера для запуска rack-приложения. Rackup’у мы предоставляли файл конфигурации следующего содержимого:

# config.ru
run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["Hello Rack!"]]}

Глубоко в своих недрах rackup преобразует ваш скрипт конфигурирования в экземпляр Rack::Builder. А теперь о том, что же такое Rack::Builder?

Что же такое Rack::Builder?

Rack::Builder реализует маленький DSL (в переводе язык предметной области или что-то типа того) для многократного построения Rack приложений.

- Документация по Rack API

coktail b52Rack::Builder это такая штука, которая склеивает различные Rack Middleware и приложения вместе и преобразует это месиво в то, что носит гордое название Rack Application. Rack::Build можно представить как коктейль изготовленный методом build — наслоением одного составляющего над другим.

Давайте сообщим Rack о том, что приложение infinity следует запускать на базе сервера WEBrick, по 9292 порту:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [env.inspect]]}
Rack::Handler::WEBrick.run infinity, :Port => 9292

Все экземпляры infinity посылают хэш env преобразованный в человекопонятную строку назад в браузер.

Ниже представлены три основных метода из Rack::Builder о которых вы должны обязательно знать:

1. Rack::Builder#run

Rack::Builder#run определяет актуальное приложение Rack, с которым будет работать Rack::Builder.

Преобразовываем infinity для использования с Rack::Builder:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [env.inspect]]}
builder = Rack::Builder.new
builder.run infinity
Rack::Handler::WEBrick.run builder, :Port => 9292

Согласно соглашению принятому в Ruby-сообществе следует использовать блоковую форму (это не правило, а рекомендация) Rack::Builder:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [env.inspect]]}
builder = Rack::Builder.new do
  run infinity
end
Rack::Handler::Mongrel.run builder, :Port => 9292

2.Rack::Builder#use

Rack::Builder#use добавляет middleware к стеку rack приложения созданному спомощью Rack::Builder (помните коктейль?). Если термин middleware вводит вас в замешательство или вызывает острые боли внизу спины, не беспокойтсь, в следующем посте я расскажу что это такое и чем middleware отличается от rack приложения. Придерживайтесь пока аналогии с before/after/around фильтрами в Ruby on Rails.

Rack имеет множество полезных middleware и одно из них это Rack::CommonLogger, что занимается тем, что записывает одну строку в log-файл в стандартном формате веб-сервера Apache.

Добавляем middleware Rack::CommonLogger к нашему infinity:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [env.inspect]]}
builder = Rack::Builder.new do
  use Rack::CommonLogger
  run infinity
end
Rack::Handler::WEBrick.run builder, :Port => 9292

Что нас здесь интересует, так это конечно же строка use Rack::CommonLogger. Так как мы не указываем Rack::CommonLogger’у, куда конкретно необходимо записывать логи, то он, по умолчанию, будет вести запись в env["rack.errors"], то есть выводить все ошибки в консоль.

3. Rack::Builder#map

Rack::Builder#map устанавливает стек для rack приложений/middleware указанный путь или URI и все дочерние пути.

Давайте представим что вы хотите показать текст «infinity 0.1″ на всех страницах в /version (/version, /version/…, но не /version…) вы можете хотеть сделать что-то похожее на это:

require 'rubygems'
require 'rack'

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [env.inspect]]}
builder = Rack::Builder.new do
  use Rack::CommonLogger
  run infinity

  map '/version' do
    run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["infinity 0.1"]] }
  end
end
Rack::Handler::WEBrick.run builder, :Port => 9292

К сожалению этот код нерабочий. Точнее он работает, но возвращает ошибку, а не то, что нам нужно. Дело в том, что метод Rack::Builder#map инкапсулирует одно пространство (область действия), и на каждое такое пространство положен только один метод Rack::Builder#run. В примере выше, мы запустили infinity в высшем, глобальном пространстве и map ‘/version’ является вложенным пространством, которое также имеет метод run, таким образом мы получаем два метода run в глобальном пространстве, из-за чего и возникает конфликт.

Рецепт лечения:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [env.inspect]]}
builder = Rack::Builder.new do
  use Rack::CommonLogger

  map '/' do
    run infinity
  end

  map '/version' do
    run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["infinity 0.1"]] }
  end
end
Rack::Handler::WEBrick.run builder, :Port => 9292

Теперь если вы перейдете по адресу http://localhsot:9292/version или http://localhost:9292/version/1 или даже http://localhost:9292/version/whatewer/doesnt/matter, вы увидите «infinity 0.1″ и для всех URI не начинающихся с /version, например в корне структуры приложени (http://localhost:9292) — вы увидите хэш env представленный в виде строки.

Важно запомнить, что:

  1. /versionsomething не будет показывать версию, а будет показывать хэш env.
  2. Когда вы работаете с большим количеством map, вы должны следовать от адресов высшего порядка к адресам низшего. Если это правило не будет соблюдаться, то адреса высшего порядка подчинят себе адреса низшего (можно сказать, что перезапишут, или аннулируют).

Вложенные блоки map

Давайте представим что вам необходимо добавить информацию о последней версии приложения «infinity beta 0.2″ по адресу /version/last, и напишем для реализации сей чудесной функции следующий код:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [env.inspect]]}
builder = Rack::Builder.new do
  use Rack::CommonLogger

  map '/' do
    run infinity
  end

  map '/version' do
    run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["infinity 0.1"]] }
  end

  map '/version/last' do
    run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["infinity beta 0.2"]] }
  end
end
Rack::Handler::WEBrick.run builder, :Port => 9292

Вышеприведенный код будет работать отлично. Как и ожидалось вы увидите «infinity beta 0.2″ по адресу http://localhost:9292/version/last и «infinity 0.1″ по адресу http://localhost:9292/version.

Но, по моему скромному мнению, лучшим способом будет написать этот код с использованием вложенных блоков map:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [env.inspect]]}
builder = Rack::Builder.new do
  use Rack::CommonLogger

  map '/' do
    run infinity
  end

    map '/version' do
      map '/' do
      run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["infinity 0.1"]] }
    end

    map '/last' do
      run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["infinity beta 0.2"]] }
    end
  end
end
Rack::Handler::WEBrick.run builder, :Port => 9292

Этот код настолько прекрасен, что хочется заняться с ним любовью! Стоит заметить, что для вложенных блоков map следует использовать вложенный, а не абсолютный URI (здесь автор оригинала статьи демонстрирует прекрасное владение навыками КО)

Rack::Builder -> rackup

Как я упоминал выше, rackup преобразует предоставляемый ему файл конфигурации в экземпляр Rack::Builder. Сейчас я покажу, что находится «под капотом» у rackup:

config_file = File.read(config)
rack_application = eval("Rack::Builder.new { #{config_file} }")

После этого преобразования rackup запускает rack — приложение на соответствующем сервере:

server.run rack_application, options

Все очень просто! Если принять во внимание все, что вы только что узнали о работе rackup, то вы легко можете преобразовать наш учебный пример в файл конфигурации для rackup:

# infinity.ru

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [env.inspect]]}

use Rack::CommonLogger

  map '/' do
    run infinity
  end

  map '/version' do
    map '/' do
      run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["infinity 0.1"]] }
    end

    map '/last' do
      run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["infinity beta 0.0"]] }
    end
end

и теперь запустим его:

$ rackup infinity.ru

Оригинал статьи:

Tags: ,

Responses

  1. ggenikus says:

    октября 25, 2010 at 13:59 (#)

    Большое спасибо вам за статьи !

    Мне не совсем понятно какую роль Rack выполняет в контексте работы приложения. Может есть пример его использования как части приложения (какая то история из жизни)? Спасибо.

  2. admin says:

    октября 25, 2010 at 15:42 (#)

    ggenikus, Rack выполняет роль связующего приложения с веб сервером. Собственно ничего не мешает вам самому реализовать это взаимодействие, но тогда в приложение придется вносить изменения различные при переходе на другой сервер и т.д. rack всю эту суету берет на себя. Rails и Sinatra — два самых популярных фреймворка на Ruby (а вместе с ними еще вагон и тележка) работают на базе rack.

    Изучение работы с rack полезно чисто в общеобразовательных целях, либо в случае, когда вы хотите написать собственный фреймворк или приложение (без использования фреймворков, якобы для лучшей производительности).

  3. ggenikus says:

    октября 25, 2010 at 15:49 (#)

    Спасибо, теперь стало понятнее. Понял глупость своего прошлого вопроса )

  4. says:

    февраля 27, 2011 at 05:34 (#)

    А нет ли где-нибудь описания работы Rack с сессиями?

  5. admin says:

    февраля 27, 2011 at 13:58 (#)

    Иван, к сожалению не находил, по Rack вообще статей очень мало, а в код лезть пока нет желания, да и потребности тоже. Если что-то найду, то обязательно переведу и опубликую.

  6. Кирилл says:

    марта 1, 2011 at 00:41 (#)

    Подскажите пожалуйста, может вы еще знаете где можно найти информацию про Rack?
    Я просто хочу сделать небольшой сайт с базовым CRUD использую sqlite или MySQL, ну и конечно стандартные web формы. Хотел бы использовать именно Rack, а не Rails. Ваш сайт выдается чуть ли не на первом месте в поисковике по запросу Rack. Я конечно буду пробовать искать информацию на офиц. сайте Rack, но может вы еще что-то знаете?

  7. admin says:

    марта 1, 2011 at 15:49 (#)

    Кирилл, Rack сам по себе достаточно мало кто использует и статей по нему соответственно совсем мало, да и изучать там особо не много. Я бы рекоментовал вам фреймворк Sinatra или еще более простой — Camping.

    А чем Rails не устраивает вас?

  8. says:

    марта 3, 2011 at 11:45 (#)

    admin, я пытаюсь устроится на работу RoR программиста и попутно изучаю ruby и его фрэймворки, от базового к более сложному. Вы правы, я попробовал Sinatra + Datamapper и у меня получилось сделать то, что хотел. Изучение Rack пока отложу, то там надо все самому писать =)
    Большое спасибо вам за ваш сайт, в ближайшее время постараюсь изучить как можно больше статей здесь. Статья про Git тоже очень понравилась ваше.
    Продолжайте развивать ваш сайт даль, ведь ruby набирает популярность =)

  9. admin says:

    марта 3, 2011 at 12:12 (#)

    И вам спасибо за спасибо!

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.