24 сентября 2009 в 18:31

Применение наследования в шаблонах Mason

Perl*
Mason — достаточно известный и распространенный фреймворк для разработки Perl-приложений. Несмотря на простой синтаксис, он обладает достаточно широкими возможностями и высокой производительностью; имеет встроенные средства для интеграции с mod_perl и memcached. Освоить работу с mason очень просто — система хорошо документирована. В то же время, есть ряд интересных тонкостей, до которых документацию дочитывают не все. Возможно, именно поэтому код, который мне доводилось видеть, до боли напоминал неудачную поделку на PHP, в которой для упрощения жизни разработчик не использовал ничего сложнее разбивки на инклюдники. Можно ли на масоне писать лучше? По-моему, да.

Disclaimer

В этой статье я не планирую открыть Америку и описать что-то, сильно выходящее за рамки известной документации. Скорее, это будет рассказ о несложных, но эффективных практических решениях, которые успешно работают на нескольких проектах. Я также не буду писать вводную вида «mason для чайников» — тем, кто уже видел масоньи компоненты, она вряд ли понадобится, а остальные смогут либо разобраться на месте, как настоящие крутые перцы, либо потратить четверть часа на раскуривание мануала, если ничего другое не помогает :) В плане терминологии я также постараюсь не отходить от оригинальной документации. Например, буду называть шаблоны mason компонентами.

Сферическое наколенное творчество в вакууме

Допустим, у нас есть набор несложных страничек с контентом. При обращении к странице проверяется, имеет ли текущий пользователь к ней доступ; если не имеет — отправляем его на опознание. Естественно, контент вписан в стандартный дизайн сайта, и автор позаботился о том, чтобы вынести общие части в инклюдники, а код — в функции. Итак, пример файла company.html:
<%INIT>
my $user = GetUserFromCookies();
$m->redirect('/login.html') unless $user && $user->haveAccess($r->uri);
</%INIT>

<& /Header.inc, title => "О сайте" &>
<h1>О сайте</h1>
<p>Наш сайт самый рулезный в рунете!</p>
<& /Footer.inc &>

Знакомый подход? В принципе, ничего плохого в приведенном примере нет. Разве что раздражает необходимость тащить во всех компонентах почти одинаковые инклюды для отображения дизайна, да и проверка авторизации требует, чтобы ее копипастили со страницы на страницу. Что можно сделать? Просто убрать лишнее :)

Теоретические основы наследования

Фича №1: компоненты-родители

Каждый компонент mason может быть унаследован от какого-либо другого компонента. Такой компонент может быть задан (или сброшен) принудительно при помощи секции <%FLAGS> и флага с названием inherit, либо вычислен автоматически. Во втором случае в папке, где находится компонент, ищется файл с именем autohandler, а если его нет — поиски продолжаются папкой выше, и т. д. — до папки, которую mason считает корневой. Если предок обнаружен, mason пытается найти родителя и для него.

Как только цепочка наследования выстроена, выполнение передается на «самый родительский» компонент, который может выполнить какие-то действия и передать управление своему потомку — при помощи магического вызова $m->call_next (магия разоблачена в документации). После отработки потомка выполнение компонента-родителя продолжается.

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

Фича №2: атрибуты и методы

С атрибутами все просто: в секции <%ATTR> компонента можно заводить произвольный набор именованных параметров. Если в компонентах-родителях имеются атрибуты, не объявленные в текущем компоненте непосредственно — они наследуются.

С методами дело обстоит почти так же просто: каждый метод заворачивается в секцию <%METHOD> и представляет собой «компонент в компоненте», который может быть вызван примерно так же, как подключаются инклюдники. Как и обычные компоненты, методы могут использоваться как шаблон (для вывода данных), и как функция (для получения результата вычислений).

Практические основы наследования

Итак, попробуем облагородить страницы при помощи этих двух фич. Шапку и хвост логично будет утащить в /autohandler. Единственное «но» — в шапке выводится название страницы (в теге title). Его можно смело положить в атрибуты страницы, если мы до них сможем докопаться из autohandler'а (забегая вперед скажу — да, сможем). Теперь у нас остается только размноженная секция <%INIT> с проверкой авторизации. Как быть, если авторизация нужна не на всех страницах? Прекрасный и вполне ООПшный способ решить эту проблему — доверить самой странице управлять проверкой. Заведем атрибут authorize, пусть авторизация проверяется на всех страницах, где он установлен в истинное значение (для определенности положим, что у нас должны проверяться все страницы, кроме /profile/login.html). Результаты наших размышлений:

/autohandler:
<%INIT>
my $component_requested = $m->request_comp; # company.html component
if ($component_requested->attr('authorize'))
{
    my $user = GetUserFromCookies();
    $m->redirect('/login.html') unless $user && $user->haveAccess($r->uri);
}
</%INIT>

<%ATTR>
authorize => 1
</%ATTR>

<& /Header.inc, title => $component_requested->attr('title') &>
<h1><% $component_requested->attr('title') %></h1>
% $m->call_next;
<& /Footer.inc &>

/company.html:
<%ATTR>
title => "О сайте"
</%ATTR>

<p>Наш сайт самый рулезный в рунете!</p>

/profile/login.html:
<%ATTR>
title => "Вход на сайт"
authorize => 0
</%ATTR>

<p>Тут могла бы быть форма для логина</p>

Некоторые простые выводы, неочевидные для простого примера:
  1. В атрибутах удобно хранить информацию о том, какой элемент меню являются текущим, и подсвечивать его
  2. В приведенном коде Header.inc принимает заголовок страницы в качестве аргумента, но в принципе никто не мешает добраться до него из самого хедера при помощи точно такого же кода
  3. Если грамотно разложить страницы по папкам, можно создать в них дополнительный уровень autohandler'ов. Это позволит гибче управляться с атрибутами (например, можно открыть доступ ко всему содержимому папки /free/, переместив атрибут authorize в файл /free/autohandler). Можно также модифицировать содержимое страницы (например, добавить заголовок верхнего уровня или блок с EULA под основным текстом страницы).

Усложняем задачу

Предположим, у нас есть некий блок, который может меняться от страницы к странице. Например, на всем сайте в правой колонке показываются новости раздела, но на страницах юзерского профайла отображается обязательство держать приватные данные в тайне. Очевидно, здесь также поможет наследование, но задачу на этот раз гораздо удобнее решить на методах. В /autohandler пропишем дефолтный метод для показа новостей (реализацию придумайте сами), а в /profile/autohandler — переопределяющий его текст:
<%METHOD rightmenu>
<p>Данные никому не дадим!</p>
</%METHOD>

Теперь предположим, что на некоторый (не на всех) страницах у нас есть блок подменю, который собирается динамически, причем в разных разделах сайта подменю формируется по-разному. Например, на страницах с контентом он может браться из базы данных CMS, а в разделе «мой профайл» — менюшка статичная, сформированная руками. Естественно, мы не хотим смешивать данные о меню с его представлением, поэтому собственно отрисовкой у нас будет заниматься компонент SubMenu.inc, а структура, описывающая само меню, будет передаваться ему в качестве параметра. Вопрос: куда положить структуру, чтобы использовать возможности наследования? Ответ: в метод компонента. Ниже приведены фрагменты, которые предлагается добавить в соответствующие файлы.
/autohandler:
% my $component_requested = $m->request_comp;
% if ($component_requested->method_exists('submenu')) {
    <& /SubMenu.inc, MenuItems => $component_requested->call_method('submenu') &>
% }

/profile/autohandler:
<%METHOD submenu>
    <%INIT>
    return [
        '/profile/password.html' => 'Изменить пароль',
        '/profile/update.html' => 'Обновить личные данные',
        '/profile/logout.html' => 'Выход'
    ];
    </%INIT>
</%METHOD>

Вместо послесловия

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

В статье показаны лишь идеи, и их можно без труда развить. Например, как бы вы решили задачу, если бы было нужно сделать компонент, способный отдавать CSV-файл вместо показа страницы? Отлично, а если страница в зависимости от желания пользователя выдает либо CSV, либо HTML в стандартном для сайта дизайне? А слабо сделать приложение со скинами и возможностью в разных страницах подключать разные скины? Кстати, не такая простая задача, как кажется на первый взгляд. Тем не менее, попробуйте :)
Bambr @Bambr
карма
65,2
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (9)

  • 0
    Не ожидал такого от «шаблонизатора», круто
    • +3
      Mason далеко не просто «шаблонизатор» :)
    • +1
      Mason — скорее фреймворк, чем просто шаблонизатор. Используя только его уже можно сделать динамический сайт.
  • +1
    Когда использовал mason, то поленился внимательно изучить доку(
    В итоге сделал так: есть один корневой компонент, в нем реализованы шапка и подвал. В середине, на основе переданных в <%args> данных, вычилялся и инклудился компонент с содержимым. Теперь понимаю, что стоило воспользоваться autohandler'ами.
  • 0
    Вопрос к автору: а приходилось вам реализовать локализацию (не мультиязычность) сайта средствами mason? Если да, не опишите ли общий подход?
    • +3
      Приходилось, но результат не понравился.
      Первый вариант локализации видел в Request Tracker. Детали не помню, кажется реализовано через фильтры. Лучше посмотрите сами.
      Второй вариант пришел в голову кому-то из нашей команды, и были созданы ресурсные файлы с хешами, где ключами были упрощенные до предела английские фразы, а значения — локализованный контент. Разработка сильно замедлилась (словарь рос на глазах, ключи никто не помнил), быстродействие тоже не блистало.
      Третий вариант — максимально разделить логику от локализованных шаблонов и попытаться выехать на переплетении файлов с «чисто локализованными текстами» и «логики без признаков текстов». Например, можно завести отдельные папки для разных языков и общую с логикой, и расплодить масоновские обработчики, чтобы каждый из них искал файлы сначала в локализованной папке (в «своем» языке), а потом в общей. Если будет алгоритм выбора нужного обработчика (скажем, по куке), схема будет работать. Но разделить логику и тексты красиво получается далеко не всегда. Если у Вас это получится — можно попробовать. У меня не получилось.
      А вообще я бы с удовольствием прочитал про более удачный опыт локализации.
      • 0
        Я бы и сам почитал. Приходилось на практике реализовывать третий вариант. Результат, мягко говоря, не совсем удобен) Очень парит держать все копии шаблонов в синхронизации (у меня их 3 было).
        • 0
          Если прикрутите предварительную генерацию локализованных шаблонов из одного источника, станет гораздо удобнее и пропадет необходимость крошить шаблоны на атомы. Вот этот подход — могу рекомендовать. Только вот масон тут не причем :)
          ЗЫ Доброе утро :)
  • 0
    Спасибо, сильно не сталкивался с Mason'ом, но все же приятно видеть добряка Perl'а…

    Хотя визуально сейчас, субъективно замечу, что всё это выглядит неуклюже в сравнении с шаблонами RoR'а :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.