Программирование на языке Ruby.

19.4. Разработка Web-приложений с помощью Nitro.

Nitro — еще один комплект инструментов для разработки Web-приложений. Хотя в сочетании с библиотекой Og для объектно-реляционного отображения Nitro хорошо приспособлен к созданию традиционных приложений в духе MVC, задуман он для поддержки различных архитектур.

Установить Nitro проще всего с помощью системы RubyGems. Gem-пакет зависит от нескольких внешних библиотек, которые тоже придется установить ( og, redcloth и ряд других).

Gem install nitro --include-dependencies.

Во время работы над книгой последней была версия Nitro 0.31.0. Но, конечно, API и библиотеки постоянно изменяются. Кроме того, имейте в виду, что приведенный ниже обзор Nitro далеко не полон.

19.4.1. Создание простого приложения Nitro.

Nitro часто используется в сочетании с Og — библиотекой ORM, обеспечивающей устойчивость объектов Ruby. Но наличие Og необязательно; Nitro, в частности, хорош тем, что не нужно заранее решать, понадобится ли приложению база данных или иная форма обеспечения устойчивости. Если со временем проект изменится и такая нужда возникнет, то для превращения объектов Ruby в устойчивые достаточно будет добавить в модели несколько строк кода. Есть планы модифицировать семейство методов attr, так что в будущем задача может стать еще проще.

Библиотека Og рассматривалась в разделе 10.4.7. В примерах работы с Nitro мы почти не будем пользоваться ей.

Хотя у Nitro очень много возможностей, для создания простого приложения нужно немногим больше, чем для создания обычного статического сайта.

Сначала создадим каталог приложения:

/home/jbritt/demo.

Затем добавим в него папку public, содержащую файл index.html:

/home/jbritt/demo/public/index.html.

Д ля начала сделаем этот файл совсем простым:

<html>

 <head>

  <title>Nitro!</title>

 </head>

 <body>

  <h1>The Ruby Way</h1>

  <h2>Hal Fulton</h2>

 </body>

</html>

Теперь в корневом каталоге приложения создадим файл run.rb:

Require 'nitro'.

Nitro.run.

Чтобы увидеть новое приложение в действии, запустите файл run.rb (из каталога demo/). Потом откройте браузер и введите URL http://127.0.0.1:9999 (здесь 9999 — порт Nitro по умолчанию).

Если все пройдет удачно, то появится приведенная выше простая страница. Принимайте поздравления — вы создали первое приложения для Nitro! Разумеется, Nitro позволяет делать гораздо больше, поэтому посмотрим, как это приложение можно расширить.

Прежде всего, интерес представляет файл run.rb. В зависимости от того, как вы развернули свою программу, его запуском может заниматься диспетчерский сценарий в каталоге public. Но для демонстрации и тестирования вы можете запустить его вручную и воспользоваться встроенным диспетчером WEBrick. Впрочем, для промышленной эксплуатации имеет смысл изучить предоставляемую Nitro поддержку Mongrel, SCGI или FastCGI.

Nitro поддерживает много разных архитектур и паттернов, а приложение обычно строится на базе паттерна Модель-Вид-Контроллер (model-view-controller, MVC). Окончательный выбор всегда остается за вами, a Nitro облегчает переход от простых сайтов, предназначенных только для просмотра, к полноценным приложениям с поддержкой базы данных.

По умолчанию при обработке запроса Nitro сначала ищет файл в папке public. Предполагается, что если имя страницы явно не указано, речь идет о файле index.html. В этом отношении Nitro ведет себя как любая статическая система. При желании мы можем поместить в папку public дополнительные статические HTML-страницы, а в ее подпапках хранить изображения и CSS-файлы.

Но интереснее то, что происходит, когда Nitro не может найти прямого соответствия запрошенной странице. Давайте изменим расширение файла index.html на .xhtml:

Public/index.xhtml.

Перезапустите сценарий run.rb. Снова введите URL http://127.0.0.1:9999, вы должны увидеть ту же самую страницу. Не найдя файла index.html, Nitro ищет файл index.xhtml и загружает его. По умолчанию xhtml — расширение, применяемое в Nitro для динамического контента. В общем случае, получив запрос, Nitro сначала ищет файл с расширением html, а потом — xhtml.

В XHTML-файлах могут быть подставляемые переменные и программная логика. Измените файл index.xhtml следующим образом:

<html>

 <head>

  <title>Nitro!</title>

 </head>

 <body>

  <h1>The Ruby Way</h1>

  <h2>Hal Fulton</h2>

  <p>Page last updated: #{Time.now}</p>

 </body>

</html>

После перезагрузки страницы вы увидите текущие дату и время. Nitro также поддерживает синтаксис команд обработки, принятый в XML:

<?r curr_date = Time.new.strftime( "%a, %b %d, %Y") ?>

<html>

 <head>

  <title>Nitro!</title>

 </head>

 <body>

  <h1>The Ruby Way</h1>

  <h2>Hal Fulton</h2>

  <p>Page last updated: #{curr_date}</p>

 </body>

</html>

Отметим, что из наличия синтаксиса <?r ... ?> не следует, что весь шаблон должен подчиняться правилам XML. Nitro располагает средствами для обработки шаблонов как XML-документов, а такой синтаксис позволяет иметь шаблоны, которые одновременно являются корректными XML-документами.

19.4.2. Nitro и паттерн MVC.

Вставка кода непосредственно в шаблон удобна для экспериментов и перехода со статических страниц на динамические. Но со временем обнаруживается, что тестировать и сопровождать такое приложение становится все сложнее. Паттерн Модель-Вид-Контроллер позволяет упростить жизнь за счет переноса кода в классы Ruby.

Начнем с создания класса контроллера main.rb. Если в каталоге приложения есть папка src, Nitro добавит ее в список путей, по которым ищутся страницы. Следуя принятым в Nitro соглашениям, создадим файл <app_root>/src/controller/book.rb:

Class BookController.

 def index.

  @author = "Hal Fulton".

  @title = "The Ruby Way".

  @last_update = Time.new.

 end.

End.

Изменим файл index.xhtml, включив в него ссылки на эти переменные:

<html>

 <head>

  <title>Nitro!</title>

 </head>

 <body>

  <h1>#{@title}</h1>

  <h2>#{@author}</h2>

  <p>Page last updated: #{@last_update}</p>

 </body>

</html>

Придется также немного подправить файл run.rb:

Require 'nitro'.

Require 'controller/book'.

Nitro.run(BookController).

Перезапустите сервер WEBrick и перезагрузите страницу, чтобы посмотреть на результат.

Отметим несколько вещей. Файлы шаблонов могут остаться в папке public; класс контроллера не обязан расширять какой-то специальный базовый класс; класс, переданный методу Nitro.run, автоматически сопоставляется с корневым URL приложения. Все эти аспекты конфигурируемы.

По умолчанию Nitro ищет шаблоны в каталогах template и public. Если вы не хотите помещать шаблоны в каталог public (скажем, потому, что предпочитаете зарезервировать его только для статических HTML-файлов), то можете создать каталог template и хранить их там. Предполагается, что пути к шаблонам повторяют пути, заданные в URL, относительно корневой папки шаблонов. Наш файл index.xhtml мог бы храниться как public/index.xhtml или template/index.xhtml. Использование каталога public упрощает переход от статического сайта к динамическому, но лучше организовать раздельное хранение статических файлов и шаблонов.

Классы контроллеров могут отображаться на пути URL с помощью файла Server.map. Например, наше демонстрационное приложение может содержать статическую главную страницу со списком литературы (в котором на данный момент присутствует всего одна книга), находящуюся совсем по другому пути. Для этого нужно следующим образом изменить файл run.rb:

Require 'nitro'.

Require 'controller/book'.

Nitro::Server.map = { '/books' => BookController }.

Nitro.run().

Соответствующий шаблон нужно будет переместить в другое место, которое соответствовало бы новому пути ( template/books/index.xhtml)

Перезапустите сервер и укажите новый URL:

Http://127.0.0.1:9999/books.

Сейчас самое время создать настоящую страницу сайта, но ради экономии места подавим это желание. Интереснее добавить механизм для запроса данных о нескольких книгах (сколь бы замечательна ни была та, что вы держите в руках). Добавим файл src/model/book.rb, содержащий описание модели Book:

Class Book.

 @@items = {}.

 attr_accessor :title.

 attr_accessor :author.

 attr_accessor :update_time.

 def initialize( values = {} ).

  @title, @author = values[:title], values[:author].

  @update_time = Time.now.

 end.

 def save.

  @@items[author] = self.

 end.

 def self.find(author).

  @@items[author].

 end.

End.

Теперь контроллер может запрашивать данные у класса Book. Изменим метод index:

Def index.

 book = Book.find_by_author("Mark Twain").

 @author = book.author.

 @title = book.title.

 @last_update = book.update_time.

End.

А в файл run.rb вставим ссылку на модель Book и загрузим какие-нибудь данные:

Require 'model/book'.

Book.new(:title => "Life on the Mississippi",

         :author => "Mark Twain").save.

После перезапуска приложения и перезагрузки страницы мы увидим сведения о книге. А если книг больше? Вместо того чтобы «зашивать» в код данные о них, мы можем попросить контроллер выполнить поиск по имени автора. Добавим в него новый метод find:

Def find(author).

 book = Book.find_by_author(author).

 @author = book.author.

 @title = book.title.

 @last_update = book.update_time.

End.

Это тот же код, что в методе index (и для создания соответствующего ему шаблона достаточно было бы переименовать index.xhtml в find.xhtml), но он принимает один аргумент — имя автора. Хотя мы разрабатываем Web-приложение, класс контроллера мало чем отличается от класса в любом другом приложении, написанном на Ruby. Самое заметное отличие заключается в том, что методы не возвращают значений, но никаких зависимостей от специальных переменных окружения или каких-то особых объектов нет. (Впрочем, имейте в виду, что Nitro все же добавляет разные «приятности» для программирования Web-приложений; для доступа к ним достаточно унаследовать свой класс от Nitro::Controller.) Например, Nitro обеспечивает «обстраивание», быструю передачу сообщений от одного запроса другому и хитроумный конвейер конструирования страницы, но все это только если вы сами захотите.

Сделаем еще одно замечание по поводу методов контроллера. Методы, предназначенные для обработки запросов на получение страницы, обычно идут в паре с шаблонами, имеющими схожее имя. Для получения конечного результата Nitro объединяет метод и шаблон. Мы видели, что Nitro может работать без контроллеров, ограничиваясь только видами. Но верно и обратное. Метод контроллера может генерировать всю страницу вообще без шаблона. Запросы на получение страниц обрабатываются в Nitro как действия (actions). Действие — это комбинация вида и методов контроллера. Внутри себя Nitro динамически создает методы действия, объединяющие то и другое. Но если какой-то компоненты недостает, ничего страшного не случится. Если у действия нет шаблона, результатом становится значение, возвращаемое методом контроллера.

Например, некоторый URL может отвечать на запрос, возвращая лишь название книги. Для этого достаточно такого метода контроллера BookController:

Def sparse(author).

 @context.content_type = 'text/plain'.

 book = Book.find_by_author(author).

 book.title.

End.

Если метод контроллера возвращает не HTML-документ, то следует изменить заголовок content-type, отравляемый вместе с ответом в результате присваивания @context.content_type=. (Кстати, даже если парный шаблон существует, его можно не использовать, переопределив метод render_text.)

Но откуда метод find или sparse получает аргумент? По умолчанию Nitro следует привычному для Web-приложений паттерну, согласно которому сегменты пути URL отображаются на контроллеры, методы и аргументы. Перезапустите приложение и введите такой URL: http://127.0.0.1:9999/books/find/Hal%20Fulton.

Обратите внимание, что %20 — это кодированное представление пробела в имени автора. Nitro производит декодирование еще до вызова метода find.

В общем случае отображение URL выполняется так: /controller/method/arg1. Дополнительные аргументы можно передать в виде последующих сегментов пути. Впрочем, в Nitro есть механизм настройки маршрутизации, так что вы не обязаны отражать в URL детали реализации.

19.4.3. Nitro и Og.

Хотя библиотека объектно-реляционного отображения Og, используемая в паре с Nitro, рассматривалась в другом месте, наше изложение было бы неполным без демонстрации того, как просто добавить к приложению интерфейс с базой данных. Сначала настроим Og в файле run.rb:

# Прямо перед вызовом Book.new :

Require 'og'.

Og.setup(:store => 'mysql',

         :name => 'demo',

         :user => 'root',

         :destroy => true,

         :port => 3316).

Затем изменим модель Book:

Require 'glue/timestamped'.

Class Book.

 is Timestamped.

 property :title, String.

 property :author, String.

 def initialize{ values = {} ).

  @title, @author = values[:title], values[:author].

 end.

End.

От хранения экземпляров Book в переменных класса мы отказались. Вызовы attr_accessor заменены обращением к методу property, который служит нескольким целям. Он тоже создает методы доступа к переменным, но, кроме того, сообщает Og, что этот класс должен обладать устойчивостью. Метод initialize почти не изменяется, но затребовав файл timestamped и поместив в класс маркер is Timestamped, мы автоматически получаем атрибут update_time.

Остальные методы можно удалить, теперь они реализуются библиотекой Og. При перезапуске приложения Nitro создаст для него базу данных MySQL, а в ней — таблицу для хранения объектов Book. Так разрабатываются приложения «на чистом Ruby без SQL».

19.4.4. Решение типичных для Web-приложений задач в Nitro.

Nitro обладает поразительно богатой системой составления страниц. Ее полное описание выходит за рамки данной книги. Но любому Web-разработчику приходится постоянно решать некоторые типичные задачи, поэтому посмотрим, как это можно сделать в Nitro.

Если вы создаете сайт с одной-двумя страницами, не так уж важно, повторяются ли многократно одна и та же разметка и текст. Но когда страниц больше, обновлять все одинаковые фрагменты вручную становится утомительно и чревато ошибками. Nitro помогает следовать принципу DRY (Don't Repeat Yourself — «Не повторяйся»), предлагая целый ряд способов повторного использования.

Простейший из них — включение файла. Пусть, например, все страницы должны иметь общий хвостовик. Можно было бы поместить его в отдельный файл шаблона и включить во все страницы, как показано ниже:

<?include href='/footer' ?>

Сам файл footer.xinc мог бы выглядеть, к примеру, так:

<div id='footer'>Read More Ruby Books</div>

Если в качестве значения атрибута href указан относительный путь, Nitro будет просматривать папки template, определенные для текущего контроллера. Если же путь абсолютный, то просматривается только папка template в корневом каталоге приложения.

Показанный выше способ лучше всего подходит для статического контента. Существует и иной синтаксис включения, позволяющий вставить указанный файл в вызывающий шаблон непосредственно перед компиляцией шаблона:

<include href='/footer' />

Результат получается таким, как если бы включаемый текст был частью вызывающего шаблона.

Более сложная форма включения контента связана с элементом render:

Render href='/controller/action' />

Где href — некий путь в каталоге приложения.

Процедура компиляции при включении частичных видов с помощью <render /> практически не отличается от случая полного вида. В контроллере могут быть методы, соответствующие включаемому файлу; их можно использовать для установки значений переменных экземпляра во включаемом шаблоне.

В Nitro граница между кодом шаблона и кодом на Ruby размыта. Один из примеров мы уже приводили: действие контроллера может быть «размазано» между методом и файлом шаблона, а может целиком входить туда или сюда. Другой пример — элементы Nitro (Nitro Elements), способ инкапсуляции кода и разметки в нестандартном теге, который можно использовать в видах.

Вместо того чтобы определять в каждом виде HTML-разметку всей страницы, мы можем определить общий HTML-код и повторно использовать его в различных действиях. Создадим файл element/layout.xhtml:

<html>

 <head>

  <title>#{@title}</title>

  <style>

   body {

    background-соlоr: white; font-family: sans-serif;

   }

  </style>

 </head>

 #{content}.

</html>

Теперь воспользуемся новым элементом в файле template/books/find.xhtml:

<Layout title='Details for #{@title}'>

 <h1>#{@title}</h1>

 <h2>#{@author}</h2>

 <p>Page last updated: #{@last_update}</p>

</Layout>

Все содержимое элемента Layout вставляется в переменную content в файле layout.xhtml. Элементы могут принимать параметры; атрибут title в открывающем теге Layout становится значением переменной экземпляра @title в файле layout.xhtml.

Вам это напоминает вызов метода с передачей ему аргументов? Так оно и есть. Мы можем определить разметку в виде класса Ruby ( src/element/layout2.rb):

Require 'nitro/element'.

Class Layout2 < Nitro::Element.

 def render.

  %^<html>

  <head>

   <title>#{@title}</title>

   <style>

    body {

     background-color: white; font-family: sans-serif;

    }

   </style>

  </head>

  #{content}.

 </html>^

 end.

End.

А затем изменим файл find.xhtml, воспользовавшись элементом Layout2 (нужно еще будет затребовать класс нового элемента в файле run.rb). Элементы могут содержать другие элементы, так что виды можно собирать из повторно используемых компонентов.

Часто встречаются большие фрагменты кода, содержащие логику, общую для нескольких приложений. Например, во многих Web-приложениях есть понятие учетной записи пользователя и авторизации. Чем заново писать код для каждой такой программы, можно включить уже готовый: это сэкономит время и упростит сопровождение.

Такой вид повторного использования называется частью. Часть (part) — это, по существу, мини-сайт, реализующий одну конкретную функцию. (В дистрибутив Nitro входит одна такая часть под названием Admin.) Код в таком подсайте не нуждается в отдельном файле run.rb, хотя включать его полезно, если вы хотите, чтобы часть могла выполняться автономно в демонстрационных целях.

Естественно, части хранятся в папке part. Пусть имеется некий код аутентификации пользователя, который можно использовать повторно. Тогда дерево частей приложения могло бы выглядеть так:

<app_root>/part/users.

<app_root>/part/users.rb.

<app_root>/part/users/public/

<app_root>/part/users/controller.rb.

<app_root>/part/users/model/user.rb.

<app_root>/part/users/model/acl.rb.

<app_root>/part/users/template/login.xhtml.

<app_root>/part/users/template/form.xinc.

<app_root>/part/users/run.rb.

Главный файл run.rb мог бы включить такую часть с помощью одной директивы require:

Require 'part/users'.

Теперь Nitro будет рассматривать весь код в каталоге part/users, как если бы он находился в дереве исходных текстов главного приложения. Поиск шаблонов начинается в папке template приложения и продолжается в подкаталогах каталога part. Если вы хотите переопределить шаблон, поставляемый вместе с частью, то нужно просто поместить замещающий вариант в соответствующий подкаталог папки template.

Часто приложение должно отображать повторяющиеся данные; обычно они оформлены в виде HTML-таблицы в шаблоне. Если число строк заранее не известно, то придется обойти какой-то набор.

Можно встроить код Ruby непосредственно в шаблон, но Nitro предлагает специальный механизм конвейерной компиляции, чтобы упростить программирование типичных логических конструкций.

Конвейерная компиляция — это последовательность преобразований, которым подвергаются шаблоны по мере объединения в действия. Существуют классы преобразования для различных задач, в частности статического включения файлов, XSLT-преобразований и локализации. Класс Morphing исследует разметку в шаблоне и ищет специальные атрибуты, которые обозначают различные преобразования.

В нашем демонстрационном приложении данных мало, но если представить себе много авторов, каждый из которых написал несколько книг, то можно было бы показать список книг данного автора. Метод контроллера поместил бы в переменную @books список книг, а часть шаблона для обхода этого цикла могла бы выглядеть так:

<h4>Books by #{@author}</h4>

<ul>

 <li each="book in @books" > #{book.title}</li>

</ul>

Класс Morphing находит атрибут each элемента li и преобразует его в следующий код:

<?r for book in @books ?>

<li>#{book.title} </li>

<?r end ?>

Порожденная таким образом разметка передается следующему этапу конвейера.

Точно так же для повтора элемента можно использовать атрибут times. Например, фрагмент.

<img src='/img/ruby.png' alt='*' times='@book.rating' />

Будет преобразован в следующий код:

<?r 3.times do ?>

<img src='/img/ruby.png' alt='*' />

<?r end ?>

19.4.5. Прочие детали.

В Nitro слишком много различных механизмов, чтобы все их можно было рассмотреть здесь. Но в этом разделе мы упомянем некоторые особенно приятные «вкусности».

В состав Nitro входит вспомогательный код, позволяющий использовать многочисленные библиотеки JavaScript, которые поддерживают различные формы DHTML и Ajax. Для облегчения интеграции в Nitro применяется высокоуровневый синтаксис. Например, в дистрибутиве Nitro есть пример поиска по сайту Flickr и вывода уменьшенных изображений. Текстовое поле для ввода тегов поиска поддерживает технологию Ajax, что достигается следующей разметкой:

<input type="text" id="tags" name="tags" auto_complete="true" />

Контроллер реализует метод tags_auto_complete, который возвращает строку в формате XML, зависящую от содержимого поля.

Nitro позволяет кэшировать действия (то есть отрисованные страницы целиком), результаты вызова методов и сгенерированные фрагменты текста. Например, чтобы поместить в кэш результат отрисовки страницы index, контроллер должен вызвать следующий метод:

Cache_output :index.

Кэширование можно добавить и в сегменты кода:

<?r cache(:book_list_cache_key) do ?>

<ul>

 <li each="book in Books.all">#{book.title}</li>

</ul>

<?r end ?>

В Nitro встроен класс преобразования для локализации, позволяющий автоматически подставлять в шаблон то или иное содержимое. Он не является стандартным этапом конвейерной компиляции, вы сами должны добавить его в файл run.rb:

Require 'nitro/compiler/localization'.

Include Nitro.

Compiler.transformation_pipeline = [

 StaticInclude,

 Elements,

 Morphing,

 Markup,

 Localization,

 Cleanup.

]

Отметим, что конвейер можно переконфигурировать, выбросив стандартные преобразования и добавив собственные. Далее определяются локали:

Localization.locales = {

 :en => 'conf/locales/en.yml',

 :de => 'conf/locales/de.yml'.

}

Локаль — это просто YAML-файл, сопоставляющий одним строкам другие:

---

:author: Autor.

:language: Sprache.

:book_rank: Buchrank.

В шаблонах применяются специальные конструкции для обозначения подставляемого текста:

<div class='detail'>[[:author]]: #{@book.author}</div>

<div class='detail'>[[:language]]: #{@book.language}</div>

<div class='detail'>[[:book_rank]]: #{@book.rank}</div>

Выбор файла локали определяется значением session[:LOCALE]. В методах контроллера можно получить текущую локаль с помощью специальной переменной @lc.

@language = @lc[:language].

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

Дополнительную информацию о Nitro можно найти на следующих ресурсах:

• http://www.nitroproject.org/ (домашняя страница Nitro).

• http://rubyforge.org/forum/forum.php?forum_id=5921 (страница проекта Nitro на сайте Ruby Forge);

• http://oxyliquit.de/ (справочное и учебное руководство по Nitro).