Во время разработки своего экспериментального WEB-проекта на Node.JS, о котором я рассказал в двух предыдущих статьях, я столкнулся с проблемой выбора шаблонизатора. Несмотря на то, что готовых решений существует довольно много, мне не удалось найти то, которое бы удовлетворяло меня на 100%. Так родился JUST.
Этот шаблонизатор достаточно популярен среди Node.JS разработчиков. Он обладает хорошим функционалом и скоростью работы, но содержит и спорные моменты:
Маленький, быстрый и довольно удобный шаблонизатор. Он прост и понятен, но, увы, для большого проекта его функционала вряд-ли будет достаточно. Из коробки он не умеет собирать страницу из частей, хотя это частично решается небольшими дополнениями, как например это сделано в Express. Но костыли — не наш метод.
Как можно догадаться из названия, это порт довольно популярного PHP-шаблонизатора на JavaScript. Этот шаблонизатор неплохо сбалансирован, но и его нельзя назвать идеальным. Для реализации логики разработчики добавили свой синтаксис, который должен облегчить разработку. Мне эта идея не кажется удачной, т.к. это увеличивает порог вхождения в разработку. Будь то PHP или JavaScript, верстальщику или программисту придётся тратить время на изучение ещё одного синтаксиса, вникать в его логику и следить за его изменениями. Момент очень спорный. Многие разработчики довольны таким синтаксическим сахаром. Я — нет.
Эти три качества легли в основу JUST. Шаблонизатор это она из важнейших деталей любого WEB-проекта. Если не считать полностью закешированные страницы — он работает при каждом запросе. Чтобы на любом этапе развития проекта работа с шаблонами приносила только радость, шаблонизатор изначально должен быть таким, чтобы его хотелось лизнуть.
Простой пример шаблонов JUST:
page.html
layout.html
Код, который сгенерирует из этих шаблонов и переданных данных конечный HTML, выглядит так:
Более полный пример можно найти в репозитории на GutHub.
На клиенте всё работает аналогично. Сначала необходимо подключить JUST к странице:
Теперь JUST можно использовать точно так же, как в Node.JS:
Т.к. в клиен��ской версии загрузка шаблонов осуществляется при помощи AJAX, они должны находиться на том же домене, что и страница со скриптом.
В качестве root JUST может использовать JavaScript-объект. Иногда такая возможность может пригодиться.
Шаблон разбирается на части простым алгоритмом и превращается в функцию, которая выполняет конкатенацию строк, а в качестве параметров принимает данные для формирования результата. Эта функция кешируется, и далее работа идёт только с ней. Повторный разбор происходит только при перезагрузке приложения или при изменении файла шаблона (если опция watchForChanges установлена в true). Такой подход позволяет получить отличную производительность. На «Что делать?» страницы с разбором шаблона генерируется 20-60мс. Без разбора шаблона (из закешированной функции) за 1-2мс. Учитывая то, что разбор происходит только 1 раз для каждого шаблона, а приложение живёт долго — временем разбора шаблона можно пренебречь.
Данные передаваемые шаблонизатору видны «насквозь» всем шаблонам участвующим в построении страницы. Это избавляет от необходимости передавать их вручную в родительские или дочерние шаблоны, однако возможность ручной передачи данных есть.
Вывод данных в шаблон осуществляется при помощи простой конструкции:
Данные вставляются в шаблон как есть, т.е. они не проходят никакую предварительную обработку в шаблонизаторе.
В шаблонах можно использовать полноценный JavaScript код. Например:
или
или
Как показала практика, наследование это очень удобный инструмент, позволяющий строить страницу снизу вверх. Операция наследования в JUST выглядит следующим образом:
или
если нужно передать в родительский шаблон дополнительные параметры. Операция наследования может находиться в любом месте шаблона, но удобнее её вставлять либо сверху либо снизу.
В родительском шаблоне на месте вставки дочернего нужно вставить такую конструкцию:
или
Эта операция позволяет подключить внешний шаблон в место вызова, предав ему необходимые параметры, если это необходимо.
или
если нужно передать в подключаемый шаблон дополнительные параметры.
Этот механизм чем-то похож на наследование. В родительских шаблонах, кроме места для вставки дочернего, можно объявлять места для вставки блоков. Содержимое этих блоков определяется в дочерних шаблонах. В отличии от наследования, переопределение блоков работает до самого верха, т.е. из любой шаблон может определить содержимое блока любого из своих родителей, а не только непосредственного. Для определения блока используется следующая конструкция:
или
В любом из родительских шаблонов нужно определить место вставки для блока используя следующую конструкцию:
или
Блоки можно переопределять. В результат попадает блок, определённый на самом нижнем уровне.
Почти во всех шаблонизаторах присутствуют наборы фильтров, которые позволяют производить манипуляции с данными на этапе генерации HTML. Например: экранирование тегов, приведение регистра символов, обрезание строк. Такой подход в корне не верный и этому функционалу тут не место.
В комментариях на этот счёт обязательно появятся возражения вроде: «Этот подход не соответствует MVC. Экранирование тегов относится только к представлению данных в виде HTML/XML, а мы можем захотеть вывести их в формате где экранирование не нужно, например — PDF. Поэтому этот функционал должен быть именно в шаблонизаторе».
Предотвращая споры по этому поводу, сразу отвечу на этот вопрос.
Шаблонизатор не является компонентом «V» модели MVC в чистом виде. Как частный случай он может им быть, но совершенно не обязан. Компонент «представление» может включать в себя как средство непосредственного построения ответа (шаблонизатор), так и средства подготовки данных для конкретного представления. На этом этапе данные можно подготовить и закешировать. Такой подход позволит не выполнять огромное количество лишних операций с данными. Учитывая, что строковые операции довольно медленные, пренебрегать этим нельзя. Если вам всё равно сколько раз выполнится замена строки, один или десять тысяч, то у меня для вас плохие новости.
Ещё один модный тренд — изобретать свой собственный синтаксис шаблонов. Чаще всего это сводится к выпиливанию скобочек и т.п. уменьшению конструкций языка. Выигрыш от этого очень сомнительный. Разработчик должен знать ещё один синтаксис и надееться на то, что автор шаблонизатора в новой версии не сделает его «ещё удобнее», и его шаблоны продолжат работать после обновления. Нативный синтаксис языка знают все, кто причастен к разработке. Новый разработчик может сразу же влиться в дело, а не вникать в работу супер-удобного синтаксиса. Верстальщики, которым чаще всего нет особого дела до того, как там программисты режут их творения на куски, могут сами внести небольшие изменения в шаблоны и даже подправить логику, т.к. JavaScript они всё же знают по долгу службы.
Один неприятный момент в JUST всё же имеется. Если в шаблоне есть обращение к неопределённой переменной, то при его обработке возникнет ReferenceError при обращении к несуществующей переменной. Хочу немного сказать о причинах такого поведения и о том, почему не получилось это исправить.
Для обращения к переменным внутри шаблона я вижу три пути:
Конкуренты
Jade
github.com/visionmedia/jadeЭтот шаблонизатор достаточно популярен среди Node.JS разработчиков. Он обладает хорошим функционалом и скоростью работы, но содержит и спорные моменты:
- Отказ от использования тегов в том месте, для которого они, собственно, и были придуманы. С таким подходом я, мягко говоря, не согласен. Конечно, это очень субъективно, но вид разметки страницы без привычных тегов, взрывает мозг. Верстальщик, далёкий от но��омодных технологий шаблонизации, не скажет спасибо, если ему придётся вносить изменения в такой код. Также потребуется дополнительная работа при перенесении вёрстки в шаблоны, что замедлит ход разработки.
- Перегруженность функционалом. Любой разработчик старается сделать свой продукт максимально универсальным, но иногда нужно уметь вовремя остановиться. На мой взгляд, Jade уже перешёл эту грань.
EJS
github.com/visionmedia/ejsМаленький, быстрый и довольно удобный шаблонизатор. Он прост и понятен, но, увы, для большого проекта его функционала вряд-ли будет достаточно. Из коробки он не умеет собирать страницу из частей, хотя это частично решается небольшими дополнениями, как например это сделано в Express. Но костыли — не наш метод.
TwigJS
github.com/fadrizul/twigjsКак можно догадаться из названия, это порт довольно популярного 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 к странице:
<script src="https://raw.github.com/baryshev/just/master/just.min.js" type="text/javascript"></script>
Теперь 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] }); %>
<% } %>
или
<% if (user.authenticated) { %>
<%@ partials/user %>
<% } else { %>
<%@ partials/auth %>
<% } %>
или
<%= new Date().toString() %>
Наследование шаблонов
Как показала практика, наследование это очень удобный инструмент, позволяющий строить страницу снизу вверх. Операция наследования в JUST выглядит следующим образом:
<%! layout %>
или
<% this.extend('layout', { customVar: 'Hello, World!' }); %>
если нужно передать в родительский шаблон дополнительные параметры. Операция наследования может находиться в любом месте шаблона, но удобнее её вставлять либо сверху либо снизу.
В родительском шаблоне на месте вставки дочернего нужно вставить такую конструкцию:
<%*%>
или
<% this.child(); %>
Инъекция шаблонов
Эта операция позволяет подключить внешний шаблон в место вызова, предав ему необходимые параметры, если это необходимо.
<%@ partial %>
или
<% this.partial('partial', { customVar: 'Hello, World!' }); %>
если нужно передать в подключаемый шаблон дополнительные параметры.
Переопределение блоков
Этот механизм чем-то похож на наследование. В родительских шаблонах, кроме места для вставки дочернего, можно объявлять места для вставки блоков. Содержимое этих блоков определяется в дочерних шаблонах. В отличии от наследования, переопределение блоков работает до самого верха, т.е. из любой шаблон может определить содержимое блока любого из своих родителей, а не только непосредственного. Для определения блока используется следующая конструкция:
<%[ 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. Поместив объект с данными в область видимости, мы можем обращаться к ним без префикса объекта, но платим за это ошибкой при обращении к неопределённой переменной.