Шаблоны с обратным вызовом. Reverse Callback Templating

| Комментариев: 5 | Нет трекбэков

Программисты всегда признавали, что отделение логики от представления - хорошая вещь. Сообщество Perl создало множество прекрасных систем для того чтобы делать только это. Тем не менее, не смотря на большое количество систем, все они укладываются в рамки двух моделей: модель pipeline и модель callback. Самые известные из представителей первой модели - HTML::Template и Template Toolkit. Шаблоны этих систем содержат простую логику представления в виде циклов, операторов условия и переменных (хотя TT гораздо более продвинутый и имеет очень обширные возможности - прим. Пер.). Программа на Perl в данной модели просто выполняет свою работу, затем загружает и обрабатывает соответствующий шаблон, так как будто бы данные текли через трубопровод (где на вход поступил шаблон и данные, а на выходе все это было смешанно и обработано в единое представление данных). Mason и Embperl относятся ко второй категории (callback). Они смешивают код и разметку шаблонов, и в данном случае шаблон «вызывает» Perl, когда он сталкивается с логикой программы.

Существует также и третья модель: модель обратного вызова (reverse callback model). Шаблон и код в этой модели разделены, так же как и в модели pipeline. Но вместо использования мини-языка для работы с логикой представления шаблон разбит на именованные секции. Perl работает с определенной секцией шаблона в определенное время, и обрабатывает ее. Это эффективней модели callback, которая обертывает логику программы вокруг секций шаблона в одном файле. Обратный вызов использует операторы Perl для загрузки или вызова определенных секций шаблона. У этого подхода есть несколько явных преимуществ.

Пример обратного вызова.

Предположим, у Вас есть простая структура данных, которую вы пытаетесь представить в HTML.

my @goods = (
    "oxfords,Brown leather,\$85,0",
    "hiking,All sizes,\$55,7",
    "tennis shoes,Women's sizes,\$35,15",
    "flip flops,Colors of the rainbow,\$7,90"
    );

 

Для начала Вам необходим HTML-шаблон с соответствующими секциями. Секции жизненно необходимы, так как они позволяют Template::Recall хранить логику представления в коде. Template::Recall по умолчанию использует паттерн /[\s*=+\s*\w+\s*=+\s*]/ (что совпадает, к примеру с [==== section_name ====]) для определения секций в одиночном файле. Начало одной секции - означает конец другой. Это связано с тем, что Template::Recall использует функцию split() основанную на вышеупомянутом паттерне, сохраняя \w+ в качестве ключа секции для внутренней структуры данных.

 

[ =================== header ===================]

 

<html>

<head>

    <title>my site - [' title ']</title>

</head>

<body>

 

<h4>The date is [' date ']</h4>

 

 

 

<table border="1">

 

    <tr>

        <th>Shoe</th>

        <th>Details</th>

        <th>Price</th>

        <th>Quantity</th>

    </tr>

 

[ =================== product_row =================== ]

    <tr>

        <td>[' shoe ']</td>

        <td>[' details ']</td>

        <td>[' price ']</td>

        <td>[' quantity ']</td>

    </tr>

 

 

[= footer =]

</table>

 

</body>

</html>

 

Этот шаблон весьма прост. В нем содержатся три секции: заголовок (header), строки данных (product_row) и «подвал» (footer). Секции, по существу, определяют логику программы. Программа вызовет секции заголовка и «подвала» только один раз, во время выполнения (в начале и в конце, соответственно). Строки данных (product_row) будут обработаны в цикле по массиву данных.

 

Названия, определенные в скобках [' and '] - представляют собой переменные шаблона, которые будут заменены во время его обработки. К примеру, [' date '] будет заменен текущей датой, когда будет запущена программа на исполнение.

 

Для начала работы мы должны инициализировать объект Template::Recall и передать ему в качестве аргумента путь к файлу шаблона.

 

use Template::Recall;

 

my $tr = Template::Recall->new( template_path => 'template1.html');

 

С созданием $tr, секции шаблона загружены и готовы к использованию. Очевидно, что первый шаг должен обработать и вывести секцию заголовка. Для этого используем метод render(). Данный метод получает в качестве обязательного аргумента имя секции для обработки, и опционально хэш, с названиями и значениями переменных шаблона которые надо заменить и которые находятся в данной секции. В нашем примере две переменных в секции заголовка: [' title '] и [' date '].

 

print $tr->render( 'header', { title => 'MyStore', date => scalar(localtime) } );

 

 

 

 

 

Названия ключей в хэше, должны совпадать с названиями переменных в шаблоне, в секции которую Вы собираетесь обработать. К примеру, date => scalar(localtime) означает [' date '] в секции заголовка, и будет динамически заменен на значение scalar(localtime).

 

Вы вероятно заметили, что заголовок шаблона создал начало таблицы HTML. И теперь подходящий момент для заполнения этой таблицы данными из массива @goods.

 

for my $good (@goods)

{

    my @attr     = split(/,/, $good);

    my $quantity = $attr[3] eq '0' ? 'Out of stock' : $attr[3];

 

    my %row      = (

        shoe     => $attr[0],

        details  => $attr[1],

        price    => $attr[2],

        quantity => $quantity,

    );

 

    print $tr->render('product_row', \%row);

}

 

В реальном проекте, данный массив вероятней всего пришел бы к нам из базы данных. Для каждой строки программа производит необходимые логические решения (к примеру, отображает «Out of stock" если количество равно «0»), затем вызывается метод $tr->render() для замены меток в секции шаблона на значения из хэша %row.

 

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

 

print $tr->render('footer');

 

Результат - миленький отчет по инвентаризации склада обуви.

The date is Fri Aug 10 14:22:30 2007

Shoe

Details

Price

Quantity

oxfords

Brown leather

$85

Out of stock

hiking

All sizes

$55

7

tennis shoes

Women's sizes

$35

15

flip flops

Colors of the rainbow

$7

90

 

 

Логика находится внутри кода.

Что произойдет, если вы немного расширите свои данные по обуви, добавив категории? Например, что если @goods выглядит так:

 

my @goods = (

    "dress,oxfords,Brown leather,\$85,0",

    "sports,hiking,All sizes,\$55,7",

    "sports,tennis shoes,Women's sizes,\$35,15",

    "recreation,flip flops,Colors of the rainbow,\$7,90"

    );

 

Вывод теперь нуждается в группировке, которая подразумевает использование вложенных циклов. Один цикл должен выводить заголовок категории - sports, dress,... - а другой цикл будет выводит подробную информацию о каждой обуви в данной категории.

 

Чтобы создать это, используя HTML::Template, Вам необходимо построить вложенную структуру данных, в виде анонимных массивов и хэшей, и затем обработать их директивой шаблона  <TMPL_LOOP>.

Template::Recall хранит логику в программе, и вам надо будет только создать вложенный цикл в Perl который вызывает соответствующие секции. Вы можете также использовать хэш для хранения секций категорий как ключей и детальную информацию - как значения ключей, а затем объединить их используя join:

 

Шаблон нужно немного модифицировать:

 

[====== table_start ====]

<table border="1">

[====== category =======]

<tr><td colspan="4"><b>['category']</b></td></tr>

[====== detail ======]

<tr><td>['shoe']</td><td>['detail']</td><td>['price']</td><td>['quantity']</td></tr>

[======= table_end ====]

</table>

Этот шаблон теперь содержит секцию category, единственная строка таблицы, которая охватывает все категории. Секция detail - представляет собой почти тоже самое что и в первом нашем примере.

my %inventory;

 

for my $good (@goods) {

    my @attr = split(/,/, $good);

    my $q    = $attr[4] == 0 ? 'Out of stock' : $attr[4];

 

    $inventory{ $tr->render('category', { category => $attr[0] } ) } .=

        $tr->render('detail',

            {

                shoe     => $attr[1],

                detail   => $attr[2],

                price    => $attr[3],

                quantity => $q,

            } );

}

 

print $tr->render('table_start') .

    join('', %inventory) .

    $tr->render('table_end');

 

Этот цикл выглядит удивительно похожим на первый пример, не так ли? Это потому, что вместо того чтобы печатать каждый ряд, код обрабатывает первую колонку массива @goods, и сохраняет выходные значения как ключи хэша %inventory. В этой же итерации обрабатывая секцию details, выходные данные добавляются к значению соответствующего ключа/категории.

После сохранения и обработки секций выводим все на печать, используя join для вывода всех значений хэша %inventory, включая хэши. Вывод будет примерно таким:

recreation

flip flops

Colors of the rainbow

$7

90

sports

hiking

All sizes

$55

7

tennis shoes

Women's sizes

$35

15

dress

oxfords

Brown leather

$85

Out of stock

 

Возможна также работа с условными операторами при выводе шаблона. Предположим, вы хотите выделить группу покупателей:

if ( $customer->is_elite ) {

    print $tr->render('special_deals', get_deals('elite') );

}

else {

    print $tr->render('standard_deals', get_deals() );

}

 

Как насчет вывода XML? Обычно для этого требуется отдельный шаблон. Вы можете по условию загружать .xml или .html шаблон:

my $tr;

if ( $q->param('fmt') eq 'xml' ) {

    $tr = Template::Recall->new( template_path => 'inventory.xml' );

}

else {

    $tr = Template::Recall->new( template_path => 'inventory.html' );

}

 

Perl обеспечивает вас всем, что вам необходимо для работы с моделью, представлением и контроллером (MVC). Template::Recall использует это для своей выгоды и помогает делать проекты легко поддерживаемыми.

 

Сравнение моделей шаблонов.

 

Важно отметить несколько моментов, которые наблюдаются в приведенных примерах - или скорее не наблюдаются. Во-первых, нет никакой смеси из кода и языка разметки шаблона. Доступ к шаблону происходит через вызов метода $tr->render(). Это строгое разделение проблемы (SOC - separation of concerns), точно такое же, как и в модели pipeline, и отличное от модели callback, которая смешивает код и разметку шаблона в одном файле. Мало того, SOC помогает лучше организовывать проектировку приложения, избавляя дизайнеров от ковыряния в коде, чтобы изменить разметку. Предположим, используя Mason для вывода строк из @goods, нужно выполнить следующее:

% for my $good (@goods) {

%  my @attr     = split(/,/, $good);

%  my $quantity = $attr[3] eq '0' ? 'Out of stock' : $attr[3];

<tr>

<td><% $attr[0] %></td>

<td><% $attr[1] %></td>

<td><% $attr[2] %></td>

<td><% $quantity %></td>

</tr>

% }

 

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

 

С другой стороны, если вы используете HTML::Template, происходит утечка логики программы в шаблон. То есть логика просачивается в представление. Чтобы обработать строки для массива @goods, потребуется использование оператора <TMPL_LOOP>.

 

    <TMPL_LOOP NAME="PRODUCT">

    <tr>

    <td><TMPL_VAR NAME=SHOE></td>

    <td><TMPL_VAR NAME=DETAILS></td>

    <td><TMPL_VAR NAME=PRICE></td>

    <td><TMPL_VAR NAME=QUANTITY></td>

    </tr>

    </TMPL_LOOP>

 

Это конечно не очень сложно, но тем не менее шаблон теперь отвечает за часть логики. Несомненно, это только логика представления, но тем не менее ЛОГИКА. HTML::Template также поддерживает <TMPL_IF> для условий и <TMPL_INCLUDE> для включения других шаблонов. И снова - это логика в файлах шаблонов.

 

Template::Recall держит так много логике в коде, насколько это возможно. Если Вам надо использовать условие, используйте оператор Perl  - if. Если Вам надо подключить другие шаблоны - загрузите их используя объект Template::Recall. Принимая во внимание, что модель pipelines, вероятно работет лучше для проектов с довольно искушенным коллективом дизайнеров, Template::Recall пытается быть другом программиста, и представляет ему или ей наиболее комфортное место - код.

 

Есть также некоторая тонкость в использовании модели pipeline в нашем простом примере с циклами.

Представим его в исполнении под HTML::Template

my $template = HTML::Template->new(filename => template1.tmpl');

 

my @output;

 

for my $good (@goods)

{

    my @attr = split(/,/, $_);

    my %row  = (

        SHOE     => $attr[0],

        DETAILS  => $attr[1],

        PRICE    => $attr[2],

        QUANTITY => $attr[3],

    );

    push( @output, \%row );

}

 

$template->param(PRODUCT => \@output);

 

print $template->output();

 

Код обходит массив @goods для построения второго массива - @output с элементами в виде ссылок на хэш. Затем шаблон обходит массив @output используя директиву <TMPL_LOOP>. То есть мы получаем проход по одним и тем же данным - дважды. Секции Template::Recall позволяют убрать эту избыточность, и Вы можете немедленно произвести вывод данных:

print $tr->render('product_row', \%row);

 

Это по существу то, что происходит при использовании Mason (или JSP/PHP/ASP). Главное различие в том, что Template::Recall отдает секцию через вызов метода, вместо того чтобы смешивать код и шаблон.

Template::Recall, используя секционированные шаблоны, сочетает эффективность модели callback с моделью отвечающую SOC - pipeline, и возможно это лучшее из двух миров!

 

Оригинал: Reverse Callback Templating by James Robson

Перевод с английского: П.Купцов

Нет трекбэков

URL для трекбэков: http://perlmonks.org.ru/cgi-bin/MT/engine/mt-tb.cgi/21

Комментариев: 5

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

Почему вы так решили? Структура страницы также задается шаблоном.

Я не увидел, где структура задается шаблоном. В шаблоне, насколько я понял, задаются только блоки. И блоки эти отображаются в сгенерированной странице в том же порядке, в каком мы их рендерим из скрипта. Вот тут, на спане, http://search.cpan.org/~gilad/Template-Recall-0.15/lib/Template/Recall.pm, в самом первом примере, если мы сделаем вызов
print $tr->render('header');
в самом конце, этот этот хтмл и окажется в самом конце.

Парсер лох, в ссылку http://search.cpan.org/~gilad/Template-Recall-0.15/lib/Template/Recall.pm закралась ошибка, запятая прилипла

Да действительно - криво. Даже сбор в переменную вывода а не сразу вывод на принт - проблемы не решает. Модуль в трэш :)

Комментировать

Об этой записи

Сообщение опубликовано 17.06.2009 16:01. Автор — Monks.

Предыдущая запись — Конечный автомат (ДКА).

Следующая запись — Отправка почты с авторизацией на SMTP

Смотрите новые записи на главной странице или загляните в архив, где есть ссылки на все сообщения.

Страницы


 


 

Page copy protected against web site content infringement by Copyscape