Во время разработки своего экспериментального WEB-проекта на Node.JS, о котором я рассказал в двух предыдущихстатьях, я столкнулся с проблемой выбора шаблонизатора. Несмотря на то, что готовых решений существует довольно много, мне не удалось найти то, которое бы удовлетворяло меня на 100%. Так родился JUST.
Этот шаблонизатор достаточно популярен среди Node.JS разработчиков. Он обладает хорошим функционалом и скоростью работы, но содержит и спорные моменты:
Отказ от использования тегов в том месте, для которого они, собственно, и были придуманы. С таким подходом я, мягко говоря, не согласен. Конечно, это очень субъективно, но вид разметки страницы без привычных тегов, взрывает мозг. Верстальщик, далёкий от новомодных технологий шаблонизации, не скажет спасибо, если ему придётся вносить изменения в такой код. Также потребуется дополнительная работа при перенесении вёрстки в шаблоны, что замедлит ход разработки.
Перегруженность функционалом. Любой разработчик старается сделать свой продукт максимально универсальным, но иногда нужно уметь вовремя остановиться. На мой взгляд, Jade уже перешёл эту грань.
Маленький, быстрый и довольно удобный шаблонизатор. Он прост и понятен, но, увы, для большого проекта его функционала вряд-ли будет достаточно. Из коробки он не умеет собирать страницу из частей, хотя это частично решается небольшими дополнениями, как например это сделано в Express. Но костыли — не наш метод.
Как можно догадаться из названия, это порт довольно популярного PHP-шаблонизатора на JavaScript. Этот шаблонизатор неплохо сбалансирован, но и его нельзя назвать идеальным. Для реализации логики разработчики добавили свой синтаксис, который должен облегчить разработку. Мне эта идея не кажется удачной, т.к. это увеличивает порог вхождения в разработку. Будь то PHP или JavaScript, верстальщику или программисту придётся тратить время на изучение ещё одного синтаксиса, вникать в его логику и следить за его изменениями. Момент очень спорный. Многие разработчики довольны таким синтаксическим сахаром. Я — нет.
Просто, быстро, удобно
Эти три качества легли в основу JUST. Шаблонизатор это она из важнейших деталей любого WEB-проекта. Если не считать полностью закешированные страницы — он работает при каждом запросе. Чтобы на любом этапе развития проекта работа с шаблонами приносила только радость, шаблонизатор изначально должен быть таким, чтобы его хотелось лизнуть.
JUST это
HTML для разметки
JavaScript для логики
Кеширование шаблонов
Автоматическая перезагрузка изменённых шаблонов (только для Node.JS версии)
Очень простой синтаксис, но серьёзный функционал
Наследование, инъекция, определение блоков
Возможность работы как в Node.JS, так и в браузере
Высокая скорость работы
Ближе к делу
Простой пример шаблонов JUST:
page.html
<%! layout %> <p>Page content</p>
layout.html
<html> <head> <title><%- title %></title> </head> <body><%*%></body> </html>
Код, который сгенерирует из этих шаблонов и переданных данных конечный HTML, выглядит так:
var JUST = require('just'); var just = new JUST({ root : __dirname + '/view' }); just.render('page', { title: 'Hello, World!' }, function(error, html) { console.log(error); console.log(html); });
Более полный пример можно найти в репозитории на GutHub. На клиенте всё работает аналогично. Сначала необходимо подключить JUST к странице:
Теперь JUST можно использовать точно так же, как в Node.JS:
var just = new JUST({ root : '/view' }); just.render('page', { title: 'Hello, World!' }, function(error, html) { console.log(error); console.log(html); });
Т.к. в клиентской версии загрузка шаблонов осуществляется при помощи AJAX, они должны находиться на том же домене, что и страница со скриптом.
В качестве root JUST может использовать JavaScript-объект. Иногда такая возможность может пригодиться.
var just = new JUST({ root : { layout: '<html><head><title><%- title %></title></head><body><%*%></body></html>', page: '<%! layout %><p>Page content</p>' } });
Как оно работает
Шаблон разбирается на части простым алгоритмом и превращается в функцию, которая выполняет конкатенацию строк, а в качестве параметров принимает данные для формирования результата. Эта функция кешируется, и далее работа идёт только с ней. Повторный разбор происходит только при перезагрузке приложения или при изменении файла шаблона (если опция watchForChanges установлена в true). Такой подход позволяет получить отличную производительность. На «Что делать?» страницы с разбором шаблона генерируется 20-60мс. Без разбора шаблона (из закешированной функции) за 1-2мс. Учитывая то, что разбор происходит только 1 раз для каждого шаблона, а приложение живёт долго — временем разбора шаблона можно пренебречь.
Данные передаваемые шаблонизатору видны «насквозь» всем шаблонам участвующим в построении страницы. Это избавляет от необходимости передавать их вручную в родительские или дочерние шаблоны, однако возможность ручной передачи данных есть.
Что JUST умеет
Вывод данных
Вывод данных в шаблон осуществляется при помощи простой конструкции:
<%- varName %>
Данные вставляются в шаблон как есть, т.е. они не проходят никакую предварительную обработку в шаблонизаторе.
JavaScript логика
В шаблонах можно использовать полноценный JavaScript код. Например:
<% for (var i = 0; i < articles.length; i++) { %> <% this.partial('article', { article: articles[i] }); %> <% } %>
Как показала практика, наследование это очень удобный инструмент, позволяющий строить страницу снизу вверх. Операция наследования в JUST выглядит следующим образом:
если нужно передать в родительский шаблон дополнительные параметры. Операция наследования может находиться в любом месте шаблона, но удобнее её вставлять либо сверху либо снизу.
В родительском шаблоне на месте вставки дочернего нужно вставить такую конструкцию:
<%*%>
или
<% this.child(); %>
Инъекция шаблонов
Эта операция позволяет подключить внешний шаблон в место вызова, предав ему необходимые параметры, если это необходимо.
если нужно передать в подключаемый шаблон дополнительные параметры.
Переопределение блоков
Этот механизм чем-то похож на наследование. В родительских шаблонах, кроме места для вставки дочернего, можно объявлять места для вставки блоков. Содержимое этих блоков определяется в дочерних шаблонах. В отличии от наследования, переопределение блоков работает до самого верха, т.е. из любой шаблон может определить содержимое блока любого из своих родителей, а не только непосредственного. Для определения блока используется следующая конструкция:
<%[ blockName %> <p>This is block content</p> <%]%>
или
<% this.blockStart('blockName'); %> <p>This is block content</p> <% this.blockEnd(); %>
В любом из родительских шаблонов нужно определить место вставки для блока используя следующую конструкцию:
<%* blockName %>
или
<% this.child('blockName'); %>
Блоки можно переопределять. В результат попадает блок, определённый на самом нижнем уровне.
Чего в JUST нет и не будет
Фильтры данных
Почти во всех шаблонизаторах присутствуют наборы фильтров, которые позволяют производить манипуляции с данными на этапе генерации HTML. Например: экранирование тегов, приведение регистра символов, обрезание строк. Такой подход в корне не верный и этому функционалу тут не место.
В комментариях на этот счёт обязательно появятся возражения вроде: «Этот подход не соответствует MVC. Экранирование тегов относится только к представлению данных в виде HTML/XML, а мы можем захотеть вывести их в формате где экранирование не нужно, например — PDF. Поэтому этот функционал должен быть именно в шаблонизаторе».
Предотвращая споры по этому поводу, сразу отвечу на этот вопрос.
Шаблонизатор не является компонентом «V» модели MVC в чистом виде. Как частный случай он может им быть, но совершенно не обязан. Компонент «представление» может включать в себя как средство непосредственного построения ответа (шаблонизатор), так и средства подготовки данных для конкретного представления. На этом этапе данные можно подготовить и закешировать. Такой подход позволит не выполнять огромное количество лишних операций с данными. Учитывая, что строковые операции довольно медленные, пренебрегать этим нельзя. Если вам всё равно сколько раз выполнится замена строки, один или десять тысяч, то у меня для вас плохие новости.
Синтаксический сахар в логических выражениях
Ещё один модный тренд — изобретать свой собственный синтаксис шаблонов. Чаще всего это сводится к выпиливанию скобочек и т.п. уменьшению конструкций языка. Выигрыш от этого очень сомнительный. Разработчик должен знать ещё один синтаксис и надееться на то, что автор шаблонизатора в новой версии не сделает его «ещё удобнее», и его шаблоны продолжат работать после обновления. Нативный синтаксис языка знают все, кто причастен к разработке. Новый разработчик может сразу же влиться в дело, а не вникать в работу супер-удобного синтаксиса. Верстальщики, которым чаще всего нет особого дела до того, как там программисты режут их творения на куски, могут сами внести небольшие изменения в шаблоны и даже подправить логику, т.к. JavaScript они всё же знают по долгу службы.
Ложка дёгтя
Один неприятный момент в JUST всё же имеется. Если в шаблоне есть обращение к неопределённой переменной, то при его обработке возникнет ReferenceError при обращении к несуществующей переменной. Хочу немного сказать о причинах такого поведения и о том, почему не получилось это исправить.
Для обращения к переменным внутри шаблона я вижу три пути:
Обращение к переменной через конструкцию %object%.%var%. Например, this.varName или data.varName. В таком случае при обращении к undefined значению ошибки не будет, и в шаблоне на этом месте появится пустая строка, но в итоге мы получим сильно разросшиеся шаблоны из-за префиксов переменных.
Автозамена прямого обращения к переменной на обращение через конструкцию %object%.%var%. Например, если в шаблоне есть конструкция <%- varName %>, при его разборе мы можем легко заменить её на this.varName, таким образом избавившиь от ошибки при неопределённой переменной. Но обращения к переменной могут быть не только в конструкции вывода данных, но и в логике. Например, обход массива в цикле или участие в условных операторах. Без дополнительного синтаксического анализа, в таких местах нет возможности заменять обращения к переменным автоматически.
Добавление объекта с данными шаблона в область видимости при помощи with. Поместив объект с данными в область видимости, мы можем обращаться к ним без префикса объекта, но платим за это ошибкой при обращении к неопределённой переменной.
На данный момент я не знаю как обойти эту ситуацию без дополнительного синтаксического анализа шаблона. Если кто-то видит красивое решение для данной проблемы — мне будет очень интересно о нём прочитать. Пока это нужно принять как особенность данного шаблонизатора. Хотя в каком-то смысле такой подход дисциплинирует и не даёт раскидываться неопределёнными переменными.