Pull to refresh

Javascript шаблонизатор с серверной частью на perl

Reading time11 min
Views2K
Мы начали с ( javascript шаблонизатора), что использовать один и тот же шаблон на стороне клиента и на стороне сервера, это удобно. Готовых рализаций такого подхода кот наплакал. Кровь приливает к голове, мы решаемся написать своё решение.

Обрисуем исходные данные и задачу



Пусть у нас есть шаблоны, от нашего шаблонизатора. На сервере они обрабатываются perl'ом. В конечном итоге мы хотим некоторые из имеющихся шаблонов, или их частей, не вынесенных в отедельный файл, использовать на стороне клиента. Необходимо с минимальными затратами(в потерере функциональности) воспроизвести процедуры генерации html. В результате обработки шаблонов, мы хотим получить наборы предварительно указанных шаблонов и блоков, оформленных в виде библиотеки javascript. Сказано – сделано.

Оговорка по ...



Далее будут приведены примеры реализации описанного парсера, с той поправкой, что описания работы парсера на серверном языке пока просто нет… То есть сейчас можно будет посмотреть на работу, исходный код и примеры шаблонизатора, по схеме: шаблоны на сервере –> компиляция в tmpl.js –> генерация html на основе данных в JSON формате. Ну а дальше, если самолет взлетит, можно перейти к шестнадцатому акту Мерлезонского балета.

Что нам понадобится?



Шаблонизатор работает под управлением perl. Нам понадобятся:
– конфигурационный файл, в котором мы опишем соответствие шаблонов с библиотеками .js (мы не хотим иметь все шаблоны в одном файле) и некоторые необходимые данные для шаблонизатора;
– сам модуль .pm который содержит различные методы для разбора шаблонов;
– скрипт, запуская который мы будем компилировать наши библиотеки;
– как ни странно, шаблоны;
В соответсвии с требованиями у нас формируется следующая структура файлов и директорий (если вы не против, то я не буду переписывать пути на анонимные, дабы упростить себе жизнь, а сразу приведу свой рабочий вариант):

# корневая директория проекта
/Users/zaur/www/blacktiger
# файл конфигураций
/Users/zaur/www/blacktiger/bin/Core/JS/Config.pm
# модуль с добрыми методами
/Users/zaur/www/blacktiger/bin/Core/JS/Parse.pm
# скрипт для компиляции шаблонов
/Users/zaur/www/blacktiger/bin/jsgen
# дикректории с шаблонами
/Users/zaur/www/blacktiger/tpl
/Users/zaur/www/blacktiger/inc
# здесь будут храниться промежуточные файлы библиотек (создаётся автоматически)
/Users/zaur/www/blacktiger/source_js
# директория, куда будем складывать готовые библиотеки
/Users/zaur/www/blacktiger/js/tpl
# Директория со статическими html файлами, для тестов
/Users/zaur/www/blacktiger/html


* This source code was highlighted with Source Code Highlighter.


Что мы сможем реализовать?



Прежде чем описывать функционал, немного расскажу вам о том, что собственно происходит. Мы скомпилировали javascript код. Посредством вызова различных функций, операторов, условий с помощью простой конкатенации чередующихся строк и переменных формируется переменная значение выходной переменной (out). По ходу выполнения кода, мы итерируем элементы массивов (в основном мы пробегаемся по массивам хэшей, которые в свою очередь так же содержат ссылки на вложенные структуры) и заполняем стэк с текущими хэшами, внутри которых мы находимся. В примерах я постараюсь расшифровать эту головоломку, посредством вывода значений внутренних переменных. Не унывайте.

Вот список того, что мы можем воплотить в наших шаблонах, имея в запазухе помимо перечисленного данные JSON:
– доступ к переменным с различным уровнем вложенности;
– итерация элементов массивов;
– условные операторы if / else if / else;
– инклуды;
– повторное использование блоков, в пределах шаблона;
– использование для генерации html выделенных в шаблонах блоков(аналог инклуда, за тем лишь исключением, что не нужно создавать отдельного файла);

Давайте уже начнем...


Шаблон
/Users/zaur/www/blacktiger/tpl/sambuka.htm
  1. <h3>{$cfg.host}</h3>
  2. <h4>{#title}</h4>
  3. <ul>
  4.   {%topics}
  5.     <li id="li-{#id}"{?#id==3} class="sel"{/?}>{#title}</li>
  6.   {/%topics}
  7. </ul>
* This source code was highlighted with Source Code Highlighter.

Наш javascript код и данные:
var d1 = {
  title: 'Всё будет хорошо – верьте!',
  topics: [
    {title: 'Новость', id: 1},
    {title: 'Статья', id: 2},
    {title: 'Фото',  id: 3},
    {title: 'Видео',  id: 4}
  ]
};
var jstpl = new myfirst();
var html1 = jstpl.tpl('tpl_sambuka',d1);

* This source code was highlighted with Source Code Highlighter.

Результирующий html


<h3>blacktiger</h3>
<h4>Всё будет хорошо – верьте!</h4>
<ul>
  <li id="li-1">Новость</li>
  <li id="li-2">Статья</li>
  <li id="li-3" class="sel">Фото</li>
  <li id="li-4">Видео</li>
</ul>

* This source code was highlighted with Source Code Highlighter.

Разберёмся ...


Идем по строчкам шаблона:
1. В файле Config.pm есть хэш %jsd. Написав {$cfg.host}, мы получили значение $jsd{'host'}. Если мы усложним это выражениек примеру {$cfg.path.to.element}, то у нас подставится значение $jsd{'path'}{'to'}{'element'}. Это и есть доступ к переменным с различным уровнем доступа.
2. В шаблон мы передали хэш d1. Запись {#title} в данном случае эквивалентна d1.title. При этом фактически туда передалось jstpl.data.cur_arg.title.
3. Спокойно, это простой html.
4., 6. {%topics} {/%topics} Превратилось в конструкцию типа
for (var i=0; i<d1.topics.length; i++) {
   //...
}


* This source code was highlighted with Source Code Highlighter.

Обвешанную служебными операциями занесения в стэк, вычислений итератора и т.п.
Это конструкция именуется как объект данных. Обычно это массив хэшей, но, возможно, использование массива массивов. Ниже описан полный синтаксис для вызова объекта, включающий указание источника данных, среза, метода, исполняемого на каждой итерации.
5. а. Мы видим вызов переменной {#title}. В отличии от предыдущего раза, здесь контекст вызова этой переменной – текущий итерируемый хэш массива topics. Как в пределах этого контекста получить значение d1.title? Нужно воспользоваться аргументом шаблона 'tpl_sambuka', выглядит это так {@tpl_sambuka.title}. У переменной d1.topics[i].title полный синтаксис вызова это {#topics.title}, т.е. он всегда начинается с имени объекта. Доступ к вложенным элементам объекта всегда начинается с указания имени объекта.
5. б. Условный оператор if (exp) { then }. В качестве exp могут использоваться вышеописанные переменные (но уже без фигурных скобок) и операторы javascript. А вот тут нужно быть осторожным. Мы ведь помним, что шаблоны используются и серверным языком, так что нужно помнить о совместимости.
7. И вот он, снова html, просто html.

Теперь немного подробнее об операторах

– Переменные, с чем их едят и как можно использовать:
1. {$cfg.path.to.element} доступ к $jsd{'path'}{'to'}{'element'}
{#title}, {#topics.title}, {#_current.title} – это все ссылки на одну и ту же переменную
{#list.path.to.element} вложенные элементы объекта list
2. {@irow[#topics._level_irow-1]title} стэк текущих хэшей объектов и аргументов шаблонов. #topics._level_irow уровень вложенности текущего хэша объекта topics. Итого это значение переменной d1.title
3. Переменная итератор цикла.
{#}, {#topics.#}, {#_iterator}, {#topics._iterator}
Это всё значения одной переменной ( i в цикле) с тем лишь условием (для человеческой, а не машинной логики), что начинается с 1. То есть для цикла с индексами 0… 5, переменная примет значение 1… 6
4. Полный синтаксис объекта
{%nameobj => #source.obj [3..5] :myloopfunction}… {/%nameobj}
Это полный синтаксис вызова объекта.
Если мы не укажем источник ( #source.obj ), то данные будут браться из переменной {#nameobj} текущего хэша.
5. Блоки
Если мы заключим какой то участок кода в тэги {@myblockname} html {/@myblockname}, то вдальнейшем в этом же шаблоне мы можем использовать этот блок {@block.myblockname.html} — на это место встанет то, что мы обрамили тэгами ранее. При этом модификатор .sub произведет вставку обрамленного участка, исполнив при этом шаблон, заключенный в тэгах.
6. Блоки из внешних шаблонов
Пример нашей записи в Config.pm
@jslib = (
  {
    name => 'myfirst',
    list_tpl => [
      'tpl_sambuka'
    ],
    blocks => {
      tpl_auth => [
        'auth_form'
      ]
    }
  }
);


* This source code was highlighted with Source Code Highlighter.

Мы записали, что хотим использовать блок auth_form из шаблона tpl_auth. В шаблоне можно использовать
{@block.tpl_auth.auth_form.html} вставить просто как html
{@block.tpl_auth.auth_form.sub} вставить в шаблон, выполнив указанный код
6. Инклуды
{&tpl_myother_tpl}
Вставит в текущий шаблон, шаблон tpl_myother_tpl, при этом в нём текущим хэшом будет хэш из вызванного контекста. Ох запутанно как.
{&tpl_myother_tpl => #source.data}
Вызывая инклуд таким образом – мы указываем ему в качестве текущего хэша #source.data, это будет и аргументами инклуда, доступными по
{@tpl_myother_tpl.title} в пределах шаблона myother эквивалентно #source.data.title

Пример с блоками, инклудами и рекурсией



Вот вам последний пример, в котором мы будем использовать блоки из внешних шаблонов, инклуды, блоки с текущего шаблона для создания рекурсивного обхода по дереву
Шаблон tpl/area.htm
  1. <h1>{#title}</h1>
  2. {&inc_header => #headerinfo}
  3.  
  4. {@tree}
  5. <ul>
  6.   {%menu => #childs}
  7.   <li id="li-{#id}">
  8.     <p>[ {#} ] {#title}</p>
  9.     {?#childs}
  10.     {@block.tree.sub}
  11.     {/?}
  12.   </li>
  13.   {/%menu}
  14. </ul>
  15. {/@tree}
  16.  
  17. <h4>Вот вам блок из другого шаблона</h4>
  18. {@block.tpl_auth.auth_form.html}
* This source code was highlighted with Source Code Highlighter.


Блок auth_form из шаблона tpl/auth.htm
{@auth_form}
<div id="d-auth">
  Логин: <input type="text" value=""><br>
  Пароль: <input type="password" value=""><br>
  <input type="button" value="Войти">
</div>
{/@auth_form}


* This source code was highlighted with Source Code Highlighter.


Код javascript и данные
var d2 = {
  headerinfo : {
    title: 'Передаем инклуду данные'
  },
  title: 'Всё будет хорошо – верьте!',
  childs : [
    { id : 1, title : 'первый уровень Петя' },
    {
      id : 2,
      title : 'первый уровень Вася',
      childs : [
        { id : 5, title : 'второй уровень Лёша' },
        {
          id : 6,
          title : 'второй уровень Миша',
          childs : [
            { id : 8, title : 'третий уровень Маша' },
            { id : 9, title : 'третий уровень Анна' }
          ]
        },
        { id : 7, title : 'второй уровень Тёма' }
      ]
    },
    { id : 3, title : 'первый уровень Лёша' },
    { id : 4, title : 'первый уровень Саша' }
    
  ]
  
};
var jstpl = new myfirst();
var html2 = jstpl.tpl('tpl_area',d2);


* This source code was highlighted with Source Code Highlighter.


Получим вот такой html:
<h1>Всё будет хорошо – верьте!</h1>
<h3>Передаем инклуду данные</h3>
<ul>
  <li id="li-1"><p>[ 1 ] первый уровень Петя</p></li>
  <li id="li-2">
    <p>[ 2 ] первый уровень Вася</p>
    <ul>
      <li id="li-5"><p>[ 1 ] второй уровень Лёша</p></li>
      <li id="li-6">
        <p>[ 2 ] второй уровень Миша</p>
        <ul>
          <li id="li-8"><p>[ 1 ] третий уровень Маша</p></li>
          <li id="li-9"><p>[ 2 ] третий уровень Анна</p></li>
        </ul>
      </li>
      <li id="li-7"><p>[ 3 ] второй уровень Тёма</p></li>
    </ul>
  </li>
  <li id="li-3"><p>[ 3 ] первый уровень Лёша</p></li>
  <li id="li-4"><p>[ 4 ] первый уровень Саша</p></li>
</ul>
<h4>Вот вам блок из другого шаблона</h4>
<div id="d-auth">
  Логин: <input id="login" class="inp" value="" type="text">
  Пароль: <input id="pswd" class="inp " value="" type="password">
  <input id="btn-login" value="Войти" type="button">
</div>


* This source code was highlighted with Source Code Highlighter.


Поразбираемся...



Еще нигде не упомянул про то, что после правки шаблонов нужно каждый раз запускать скрипт ./bin/jsgen – это одна из темных сторон решения. Перегенерироваться будут только изменные шаблоны (отслеживается timestamp времени модификации файлов). Для принудительной перегенерации всех шаблонов нужно запустить скрипт с ключиком -all
./bin/jsgen -all
Не забываем ставить права на jsgen соответсвеющие
chmod 775
Еще один момент. В Config.pm у нас указан массив
@templates = (
  { pm  => "Tpl", ext  => '.htm',  dir  => "tpl" },
  { pm  => "Inc", ext  => '.inc',  dir  => "inc" }
);


* This source code was highlighted with Source Code Highlighter.

Он нам говорит, что в корневой директории есть две папки, tpl и inc, которые содержат в себе файлы шаблонов, с расширениями .htm и .inc соответственно. В этих папках могут быть подпапки (это в конфиге отражать никак не надо). Допустим мы создали файл
inc/news/rubric/index.inc
Обращаться к такому шаблону нужно по имени 'inc_news_rubric_index'

Файл myfirst.js лежит в директории /js/tpl (Config.pm => $cfg{jstpl})

Как упоминал выше, мы хотим делать различные сборки шаблонов.
Снова приведу пример нашей записи в Config.pm
@jslib = (
  {
    name => 'myfirst',
    list_tpl => [ ... ],
    blocks => {...}
  },{
    name => 'mysecond',
    list_tpl => [ ... ],
    blocks => {...}
  }
);


* This source code was highlighted with Source Code Highlighter.

Массив @jslib содержит информацию о таких сборках
name — название файла и класса одновременно.
list_tpl — все шаблоны (и из папки tpl, и из inc, и любых других, добавленных вами(невложенные в данные) и описанных в конфиге )
blocks — имена шаблонов и блоков в них, которые будете задействовать в сборке

По номерам строк в шаблоне area.htm:
2. Вызываем инклуд inc/header.inc и передаем ему в качестве текущего хэша d2.headerinfo
4.,15. Объявляем блок tree
6.,13. Объявляем объект menu, в качестве источника данных передается #childs из текущего хэша. Для рекурсии важно, чтобы это одинаково было обозначено в корневом элементе и в потомках. Пример данных приведен выше.
10. Вызываем обозначенный блок как функцию. То есть весь код, обрамленный тэгами блока выполнится с текущим хэшем (условие проверяет наличие потомков #childs)
18. Вызываем внешний блок auth_form из шаблона tpl/auth.htm как простой html

Конец



Вот так, достаточно громоздко с виду, мы получаем довольно-таки многофункциоанльный шаблонизатор для javascript. Он мало чем уступает шаблонизатору работающему на серверных скриптах (свои ньюансы). А за счет отсуствия регулярных выражений в скомпилированном коде, может похвастать солидным быстродействием. Минус который меня убивает порой – это необходимость после изменения шаблона запускать скрипт перегенерации…

Прошу прощения, если всё запутанно объяснил, подробной документации еще нет. Если кому-то интересно, можно продолжить описание, но уже и с шаблонизатором на perl на серверной стороне, чтобы охватить уже всю связку

Исходный код всех упомянутых файлов, ссылка
Не забываем подправить файл bin/Core/JS/Config.pm
Tags:
Hubs:
Total votes 22: ↑18 and ↓4+14
Comments14

Articles