Программисты всегда признавали, что отделение логики от представления - хорошая вещь. Сообщество 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
Перевод с английского: П.Купцов



Получается, что мы переносим всю структуру страницы в код. То-есть, если мы хотим, к примеру, перенести облако тегов из правой колонки в левую, надо лезть в код. Не думаю, что это хорошая идея.
Почему вы так решили? Структура страницы также задается шаблоном.
Я не увидел, где структура задается шаблоном. В шаблоне, насколько я понял, задаются только блоки. И блоки эти отображаются в сгенерированной странице в том же порядке, в каком мы их рендерим из скрипта. Вот тут, на спане, в самом первом примере, если мы сделаем вызов
print $tr->render('header');
в самом конце, этот этот хтмл и окажется в самом конце.
Парсер лох, в ссылку закралась ошибка, запятая прилипла
Да действительно - криво. Даже сбор в переменную вывода а не сразу вывод на принт - проблемы не решает. Модуль в трэш :)