1. Введение В одной из своих первых статей я описал принцип работы своего собственного шаблонизатора, со временем, код модернизировался и изменялся, но принцип "нарезки" и сборки остался тот же. В итоге собран небольшой модуль в 120 строк + дополнительный модуль раширения о котором поговорим позже. Итак, посмотрим на пример шаблона и пример вывода, что бы в общих чертах понять принцип работы: Как видно из шаблона, в нем определены четыре блока (<!-- [1, 3, 5, 6] -->) и две метки вставки (<!-- [1, 3]_insert -->). Блоки 5 и 6 являются сотавными частями для блока 1, поэтому для них меток вставок нет. Сразу может возникнуть вопрос, по поводу того, почему каждая строка блока имеет свою метку, а не просто определены границы блока, поясняю, в некоторых случаях блоки в шаблоне могут вкладываться друг в друга, а визуальный вид шаблона терять не хочется, разместив блоки по порядку, а не тех местах HTML кода где им положено быть. Так же в шаблоне включены 3 метки вставки (<[inc_...]>) которыые обрабатываюстя отдельным модулем и являются динамическими частями страницы не зависящими от основного кода скрипта, чаще всего это динамические меню, которые присутсвуют на нескольких страницах и зависят только от глобальных установок с небольшими оговорками. 2. Принцип модуля шаблонизатора Определим, какие данные будут хранится в нашем объекте и какие методы мы будем использовать: - Данные объекта:
- HTML код шаблона - ссылка на массив;
- HTML код блоков шаблона - ссылка на хеш ([номер блока] => [HTML код блока]);
- Пользователькие переменные - ссылка на хеш ([параметр ([$...$])] => [значение]);
- Папка, в которой хранятся шаблоны;
- "Флаг" обработки SSI - если обработка SSI не нужна, то и не за чем обрабатывать в пустую;
- Методы объекта:
- Объявление объекта;
- Забор шаблона из файла;
- "Нарезка" блоков;
- "Склеивание" блоков;
- Обработка шаблона;
- Вывод шаблона пользователю;
- Внутренняя процедура обработки SSI - метод не обязательный, но весьма полезный;
1. Во время объявления объекта, мы сразу можем определить пользовательские переменные, папку шаблонов а так же выбрать файл шаблона. "Нарезку" блоков запускать по-умолчанию, по моему мнению не стоит, так как не всегда в шаблоне требуется собирать блоки. Переменные в шаблоне я ограничиваю квадратными скобками и знаком доллара, причем ограничитель начала и конца переменной - разные, что бы проще было обрабатывать регулярным выражением. 2. Во время забора шаблона из файла ничего особенного не происходит, просто быбираем файл в массив и кидаем ссылку на него в объект. 3. Во время "нарезки" блоков шаблон построчно обрабатывается и строки с метками переносятся в хеш с соответсвующим ключем с последующим удалением этих строк из общего шаблона; 4. Во время "склейки" блоков шаблон так же построчно обрабатывается на предмет поиска меток вставки с последующей их заменой на соответсвующий блок; 5. Во время обработки шаблона, готовый ("склееный") шаблон построчно обрабатывается и в нем заменяются все совпадения [$...$] на соответсвующий значение хеша пользовательких переменных, так же можно внедрить обработку стандартных кодов и обработку SSI по требованию. 6. Во время вывода шаблона пользователю отсылается заголовок (если надо) и HTML код шаблона. 7. Во время обработки SSI, просто находится соотвествующий файл и внедряется в основной код шаблона. 3. Код 3.1. Объявление объекта package WM5::Template; # Без этого - "ни шагу со двора" use strict; use warnings; # Локаль, тоже пригодится use locale; use POSIX qw(locale_h); setlocale(LC_CTYPE, 'ru_RU.CP1251'); setlocale(LC_ALL, "ru_RU.CP1251"); # Определим версию our $VERSION = '2.0.1'; sub new { # Получаем переменные my ($self, %common) = @_; # Определяем хеш объекта $self = { template => undef, block => undef, user_vars => undef, # Папка шаблонов определяется сама как [папка месторасположения скрипта]/template/ # хотя такой подход не обязателен, достаточно по умолчанию сделать ./template/ # или определить какую-либо свою folder => ($ENV{'SCRIPT_FILENAME'} =~ /^(.*?)[^\\\/]*$/)[0].'template/', SSI => 'N', }; # "Прицепляем" ссылку на хеш(!) пользовательских переменных если мы её получаем $$self{'user_vars'} = $common{'user_vars'} if $common{'user_vars'}; # "Прицепляем" папку шаблонов, если мы определяем её вручную $$self{'forder'} = $common{'folder'} if $common{'folder'}; # Определяем "флаг" использования SSI. Несколько сложное определение обусловлено # использованием use warnings; (-w), а так же для того что бы ограничить возможность # использования других значений переменной кроме как Y или N $$self{'SSI'} = {'Y' => 'Y', 'N' => 'N'}->{($common{'SSI'} || 'N')} || 'N'; # Если мы передаем file при объявлении объекта, то можно сразу получить шаблон if ($common{'file'}) {$self = &GetTemplateFromFile($self, $common{'file'})} # Теперь можно "благословить" объект... bless $self; # ... и вернуть основному скрипту return $self } 3.2. Выборка шаблона из файла sub GetTemplateFromFile { # Получаем объект и имя файла my ($self, $file) = @_; # Определяем путь к файлу $file = $self->{'folder'}.$file; # Открываем файл и выбираем весь HTML код в массив open (TMP, $file); my @template = <TMP>; close TMP; # Чистим концы строк, не обязательно, но шаблон будет легче chomp @template; # "Цепляем" ссылку на массив объекту $self->{'template'} = \@template; # Возвращаем объект, обязательно, только тогда когда мы обращаемся к данной # процедуре не как к методу, это мы производим из процедуры объявления return $self; } 3.3. "Нарезка" блоков sub SplitTemplate { # Получаем объект my $self = shift; # Незаметно отключаем предупреждения, а то не хочется проверять лишний раз # определен ли у нас элемент хеша блоков ($self->{'block'}) no warnings; # Объявляем счетчик my $i; # "Прогоняем" шаблон... while (${$self->{'template'}}[$i]) { # ... ищем метки ... if (${$self->{'template'}}[$i] =~ s/<!--\s(\d+)\s-->//g) { # ... прицепляем строку к соответсвующему элементу хеша блоков... $self->{'block'}->{$1} .= ${$self->{'template'}}[$i]; # ... удаляем елемент массива ... splice (@{$self->{'template'}}, $i ,1); # ... уменьшаем счетчик на 1 ... $i-- } # ... увеличиваем счетчик на 1 $i++ } } Я не использую цикл foreach или for из-за того, что не просто обнуляю элементы массива шаблона, а удаляю их, при этом количество циклов динамически изменяется. 3.4. "Склейка" блоков sub MergeTemplate { # Получаем объект my $self = shift; # Прогоняем шаблон в цикле foreach (@{$self -> {'template'}}) { # Заменяем метки вставки соответствующими блоками $_ =~s /<!-- (\d+)_insert -->/$self->{'block'}->{$1} || ''/eg } # Удаляем хеш блоков $self->{'block'} = undef } 3.5. Обработка шаблона sub DefaultParce { # Получаем объект my $self = shift; # Формируем регулярное выражение стандартных кодов my %codes = ('url' => $ENV{'SERVER_NAME'}, 'nbsp' => ' ', 'br' => "\n", ); my $regex = '\[((?:'.join(')|(?:', keys %codes).'))\]'; # Прогоняем шаблон в цикле foreach (@{$self->{'template'}}) { # Обрабатываем SSI по требованию $_ =~s /\<!--#include virtual\=\"([\?\&\=\-\w\.\/]+)\"\s*\-+\>/ &_include_ssi($self, $1)/eg if $self->{'SSI'} eq 'Y'; # Замена стандартных кодов $_ =~s /$regex/$codes{lc($1)}/gi; $_ =~s /\[nbsp\:\s*(0-9)\s*\]/' ' x $1/egi; # Замена пользовательских пременных $_ =~s /\[\$([\w\s\-]+)\$\]/$self->{'user_vars'}->{$1} || ''/eg; } } Собственно, окончательная обработка шаблона в основном зависит от программиста, как он привыкэто делать, тем более, что сама процедура, по сути практически независимая. Регулярное выражение стандартных кодов в собранном виде для конкретно нашего случая выглядит так (что бы не гадать лишний раз, что к чему): ~s /\[((?:url)|(?:br)|(?:nbsp))\]/$array{lc($1)}/gi 3.6. Вывод шаблона sub ShowTemplate { # Получаем объект my $self = shift; # Выводим заголовок браузеру, если нужно print "Content-type: text/html; charset=windows-1251\n$self->{'user_vars'}->{'cookies'}\n" if !$ENV{'MOD_PERL'}; # Выводим шаблон print @{$self->{'template'}}; } Собственно, 3 строки, единственно, в заголовке возможна передача Cookies, а так - ничего нового 3.7. Обработка SSI sub _include_ssi { # Получаем объект и ссылку на файл my ($self, $ssi) = @_; # Чистим все после знака ? $ssi =~s /\?.*//i; # Определяем путь к файлу my $file; $file = $ssi !~ /^\/\// ? # Если относительная ссылка './template/'.$ssi : # Если абсолютная ссылка eq.: [//ssi/include_file.html] $ENV{'DOCUMENT_ROOT'}.$ssi; my $html; # Открываем файл и сохраняем данные файла в переменной if (-e $file) { open (FILE, $file); $html = join('',<FILE>); close FILE } else {$html = 'Error including file!!!'} # Возвращаем содержимое файла, или текстовую ошибку return $html } Без прикрас и лишних обработок, хотя, при желании можно и поэкспериментировать. 4. Пример использования Требуется вывести список товаров, разбитый постранично, товары хранятся в базе данных в одной таблице. Итак, код шаблона: <html> <head> <title>Список товаров</title> <meta http-equiv="Content-Type" content="text/html; charset=windows-1251"> </head> <body> <!-- 10 --> <table width="100%" border="0" cellspacing="0" cellpadding="0"> <!-- 10 --> <tr><td>Страницы:</td><td> <!-- 10 --> [$list$] <!-- 11 --> <a href="?page=[$page$]">[$page$]</a>[nbsp] <!-- 12 --> <b>[$page$]</b>[nbsp] <!-- 10 --> </td></tr></table><br> <!-- 10_insert --> <table width="100%" border="1" cellPadding="3" cellSpacing="1"> <!-- 1 --> <tr> <!-- 1 --> <td>[$name$]</td> <!-- 1 --> <td>[$description$]</td> <!-- 1 --> </tr> <!-- 1_insert --> </table><br> <!-- 10_insert --> </body> </html> Код скрипта: #!/usr/bin/perl use strict; use CGI; use DBI; use lib './../lib/'; use PM::Template; # Подключаемся к базе my $dbh = 'DBI'->connect('DBI:mysql:database=test:host=localhost:port=3306','user', 'pass') || die "Cann't connect DataBase!!! $DBI::errstr"; # Создаем объект CGI my $query = new CGI; # Объявляем пользовательские переменные и определяем текущую страницу # и количество строк на странице my %uv; $uv{'page'} = $query->param('page') || 1; $uv{'page'} = int($uv{'page'}) || 1; $uv{'goods_per_page'} = 3; # Создаем объект шаблона и сразу забираем из файла my $template = new PM::Template (user_vars => \%uv, file => 'list.html'); # Нарезаем шаблон $template->SplitTemplate; # Формируем список страниц my $sql = 'SELECT COUNT(*) FROM table2'; my $sth = $dbh->prepare($sql); $sth->execute(); my $nums = $sth->fetchrow_arrayref()->[0] || 1; $sth->finish(); my $pages = int(($nums - 1)/$uv{'goods_per_page'}) + 1; my $lines; # Временная переменная списка for my $i (1..$pages) { my $line = $i == $uv{'page'} ? $template->{'block'}->{'12'} : $template->{'block'}->{'11'}; $line =~s /\[\$page\$\]/$i/gi; $lines .= $line; } $template->{'block'}->{'10'} =~s /\[\$list\$\]/$lines/gi; undef $lines; # Формируем список товаров $sql = 'SELECT name, description FROM table2 ORDER BY name LIMIT '.(($uv{'page'} - 1) * $uv{'goods_per_page'}).','.$uv{'goods_per_page'}; $sth = $dbh->prepare($sql); $sth->execute(); while (my $row = $sth->fetchrow_hashref()) { my $line = $template->{'block'}->{'1'}; $line =~s /\[\$(\w+)\$\]/$$row{$1}/gi; $lines .= $line; } $template->{'block'}->{'1'} = $lines; undef $lines; # Склеиваем шаблон $template->MergeTemplate; # Окончательная обработка шаблона $template->DefaultParce; # Вывод пользователю $template->ShowTemplate; exit; И результат: Вот в общем-то и все, просто и, по идее, понятно. Теперь можно рассмотретьь внедрение динамических блоков в шаблон...
|