Различных шаблонных систем – много.
Они постепенно эволюционировали и развивались.
В этой главе мы разберём, как шёл этот процесс, какие шаблонки «родились», какие бонусы нам даёт использование той или иной шаблонной системы.
Микрошаблоны
Микрошаблоны (англ. microtemplate) мы уже видели на примере _.template
.
Это HTML со вставками переменных и произвольным JS.
Пример:
<div class="menu">
<span class="title"><%-title%></span>
<ul>
<% items.forEach(function(item) { %>
<li><%-item%></li>
<% }); %>
</ul>
</div>
Шаблонная система компилирует этот код в JavaScript-функцию с минимальными модификациями, и она уже, запустившись с данными, генерирует результат.
Достоинства и недостатки такого подхода:
- Жёстко привязан к языку JavaScript.
- При ошибке в шаблоне приходится лезть внутрь «страшной» функции
- Простая и быстрая шаблонная система
- Внутри JS-функции доступен полноценный браузерный отладчик, функция хоть и страшна, но понятна.
Код в шаблоне
Включение произвольного JS-кода в шаблон, в теории, позволяет делать в нём всё, что угодно. Но обратная сторона медали – шаблон вместо внятного HTML может стать адским нагромождением разделителей вперемешку с вычислениями. Что рекомендуется делать в шаблонах, а что нет?
Можно разделить код на два типа с точки зрения шаблонизации:
- Бизнес-логика – код, формирующий данные, основной код приложения.
- Презентационная логика – код, описывающий, как показываются данные.
Например, код, получающий данные с сервера для вывода в таблице – бизнес-логика, а код, форматирующий даты для вывода – презентационная логика.
В шаблонах допустима лишь презентационная логика.
Кросс-платформенность
Зачастую, нужно использовать один и тот же шаблон и в браузере и на сервере.
Например, серверный код генерирует HTML со списком сообщений, а JavaScript на клиенте добавляет к нему новые по мере появления.
…Но как использовать на сервере шаблон с JavaScript, если его основной язык – PHP, Ruby, Java?
Эту проблему можно обойти. На сервер, использующем PHP, Ruby, Java или какой-то другой язык, дополнительно ставится виртуальная машина V8 и настраивается интеграция с ней. Почти все платформы это умеют.
После этого становится возможным запускать JavaScript-шаблоны и передавать им данные в виде объектов, массивов и так далее.
Этот подход может показаться искусственным, но на самом деле он вполне жизнеспособен и используется в ряде крупных проектов.
Прекомпиляция
Эта шаблонка и большинство других систем, которые мы рассмотрим далее, допускают прекомпиляцию.
То есть, можно заранее, до выкладывания сайта на «боевой сервер», обработать шаблоны, создать из них JS-функции, объединить их в единый файл и далее, в «боевом окружении» использовать уже их.
Современные системы сборки (brunch, grunt с плагинами и другие) позволяют делать это удобно, а также хранить шаблоны в разных файлах, каждый – в нужной директории с JS-кодом для виджета.
Хелперы и фильтры
JavaScript-вставки не всегда просты и элегантны. Иногда, чтобы что-то сделать, нужно написать порядочно кода.
Для того, чтобы сделать шаблоны компактнее и проще, в них стали добавлять фильтры и хелперы.
-
Хелпер (англ. helper) – вспомогательная функция, которая доступна в шаблонах и используется для решения часто возникающих задач.
В
_.template
, чтобы объявить хелпер, можно просто сделать глобальную функцию. Но это слишком грубо, так не делают. Гораздо лучше – использовать объект_.templateSettings.imports
, в котором можно указать, какие функции добавлять в шаблоны, или опциюimports
для_.template
.Пример хелпера – функция
t(phrase)
, которая переводитphrase
на текущий язык:_.templateSettings.imports.t = function(phrase) { // обычно функция перевода немного сложнее, но здесь это не важно if (phrase == "Hello") return "Привет"; } // в шаблоне используется хелпер t для перевода var compiled = _.template("<div><%=t('Hello')%></div>"); alert( compiled() ); // <div>Привет</div>
Такой хелпер очень полезен для мультиязычных сайтов, когда один шаблон нужно выводить на десяти языках. Нечто подобное используется почти во всех языках и платформах, не только в JavaScript.
-
Фильтр – это функция, которая трансформирует данные, например, форматирует дату, сортирует элементы массива и так далее.
Обычно для фильтров предусмотрен специальный «особо простой и короткий» синтаксис.
Например, в системе шаблонизации EJS, которая по сути такая же, но мощнее, чем
_.template
, фильтры задаются через символ|
, внутри разделителя<%=: ... %>
.Чтобы вывести
item
с большой буквы, можно вместо<%=item%>
написать<%=: item | capitalize %>
. Чтобы выводить отсортированный массив, можно использовать<%=: items | sort %>
и так далее.
Свой язык
Для того, чтобы сделать шаблон ещё короче, а также с целью «отвязать» их от JavaScript, ряд шаблонных систем предлагают свой язык.
Например:
- Mustache
- Handlebars
- Closure Templates
- …тысячи их…
Шаблон для меню в Handlerbars, к примеру, будет выглядеть так:
<div class="menu">
<span class="title">{{title}}</span>
<ul>
{{#each items}}
<li>{{item}}</li>
{{/each}}
</ul>
<div>
Как видно, вместо JavaScript-конструкций здесь используются хелперы. В примере выше {{#each}} ... {{/each}
– «блочный» хелпер: он показывает своё содержимое для каждого элемента items
и является альтернативой forEach
.
Есть и другие встроенные в шаблонизатор хелперы, можно легко делать свои.
Использование такого шаблона:
// текст шаблона должен быть в переменной tmpl
var compiled = Handlebars.compile(tmpl);
var result = compiled({
title: "Сладости",
items: ["Торт", "Пирожное", "Пончик"]
});
Библиотека шаблонизации Handlebars «понимает» этот язык. Вызов Handlebars.compile
принимает строку шаблона, разбивает по разделителям и, аналогично предыдущему виду шаблонов, делает JavaScript-функцию, которая затем по данным выдаёт строку-результат.
Запрет на встроенный JS
Если «свой язык шаблонизатора» очень прост, то библиотеку для его поддержки можно легко написать под PHP, Ruby, Java и других языках, которые тем самым научатся понимать такие шаблоны.
Если шаблонка действительна нацелена на кросс-платформенность, то явные JS-вызовы в ней запрещены. Всё делается через хелперы.
Если же нужна какая-то логика, то она либо выносится во внешний код, либо делается через новый хелпер – он отдельно пишется на JavaScript (для клиента) и для сервера (на его языке). Получается полная совместимость.
Это создаёт определённые сложности. Например, в Handlebars есть хелпер {{#if cond}} ... {{/if}}
, который выводит содержимое, если истинно условие cond
. При этом вместо cond
нельзя поставить, к примеру, a > b
или вызов str.toUpperCase()
, будет ошибка. Все вычисления должны быть сделаны на этапе передачи данных в шаблон.
Так сделано как раз для переносимости шаблонной системы на другие языки, но на практике не очень-то удобно.
Продвинутые кросс-платформенные шаблонизаторы, в частности, Closure Templates, обладают более мощным языком и умеют самостоятельно разбирать и компилировать многие выражения.
Шаблонизация компонент
До этого мы говорили о шаблонных системах «общего назначения». По большому счёту, это всего лишь механизмы для преобразования одной строки в другую. Но при описании шаблона для компоненты мы хотим сгенерировать не просто строку, а DOM-элемент, и не просто генерировать, а в дальнейшем – с ним работать.
Современные шаблонные системы «заточены» на это. Они умеют создавать по шаблону DOM-элементы и автоматически выполнять после этого разные полезные действия.
Например:
- Можно сохранить важные подэлементы в свойства компоненты, чтобы было проще к ним обращаться из JavaScript.
- Можно автоматически назначать обработчики из методов компонента.
- Можно запомнить, какие данные относятся к каким элементам и в дальнейшем, при изменении данных автоматически обновлять DOM («привязка данных» – англ. data binding).
Одной из первых систем шаблонизации, которая поддерживает подобные возможности была Knockout.JS.
Попробуйте поменять значение <input>
в примере ниже и вы увидите двухстороннюю привязку данных в действии:
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script>
Поменяйте имя: <input data-bind="value: name, valueUpdate: 'input'">
<hr>
<h1>Привет, <span data-bind="text: name"></span>!</h1>
<script>
var user = {
name: ko.observable("Вася")
};
ko.applyBindings(user, document.body);
</script>
Библиотека Knockout.JS создаёт объект ko
, который и содержит все её возможности.
В этом примере работу начинает вызов ko.applyBindings(user, document.body)
.
Его аргументы:
user
– объект с данными.document.body
– DOM-элемент, который будет использован в качестве шаблона.
Он пробегает по всем подэлементам document.body
и, если видит атрибут data-bind
, то читает его и выполняет привязку данных.
Значение <input data-bind="value: name, ...">
означает, что нужно привязать input.value
к свойству name
объекта данных.
Привязка осуществляется в две стороны:
-
Во-первых, библиотека ставит на
input
свой обработчикoninput
(можно выбрать другие события, см. документацию), который будет обновлятьuser.name
. То есть, изменениеinput
автоматически меняетuser.name
-
Во-вторых, свойство
user.name
создано какko.observable(...)
. Технически,ko.observable(value)
– это функция-обёртка вокруг значения: геттер-сеттер, который умеет рассылать события при изменении.Например:
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script> <script> var user = ko.observable("Вася"); // вызов user() возвращает значение alert( user() ); // Вася // вызов user.subscribe(func) ставит обработчик на изменение значения user.subscribe(function(newValue) { alert("Новое значение: " + newValue); }); // вызов user(newValue) меняет значение user("Петя"); // сработает обработчик, назначенный выше </script>
Библиотека Knockout.JS ставит свой обработчик на изменение значения и при этом обновляет все привязки. Так что при изменении
user.name
меняется иinput.value
.
Далее в том же примере находится <span data-bind="text: name">
– здесь атрибут означает привязку текста к name
. Так как <span>
по своей инициативе меняться не может, то привязка односторонняя, но если бы мог, то можно сделать и двухстороннюю, это несложно.
Вызов ko.applyBindings
можно делать внутри компоненты, и таким образом устанавливать соответствия между её объектом и DOM.
Библиотека также поддерживает хранение шаблонов в <script type="text/template">
– см. документацию template-binding, можно организовать прекомпиляцию, добавлять свои привязки и так далее.
Другие библиотеки
Есть другие библиотеки «продвинутой шаблонизации», которые добавляют свои возможности по работе с DOM, например:
Подобная шаблонная система является частью многих фреймворков, например:
Все эти фреймворки разные:
- Ember использует надстройку над Handlebars.
- React использует JSX (JavaScript XML syntax transform) – свой особый способ вставки разметки в JS-код, который нужно обязательно прекомпилировать перед запуском.
- Angular вместо работы со строками использует клонирование DOM-узлов.
При разработке современного веб-приложения имеет смысл выбрать продвинутую шаблонную систему или даже один из этих архитектурных фреймворков.
Итого
Системы шаблонизации, в порядке развития и усложнения:
- Микрошаблонизация – строка с JS-вставками, которая компилируется в функцию – самый простой вариант, минимальная работа для шаблонизатора.
- Собственный язык шаблонов – «особо простой» синтаксис для частых операций, с запретом на JS в случае, если нужна кросс-платформенность.
- Шаблонизация для компонентов – современные системы, которые умеют не только генерировать DOM, но и помогать с дальнейшей работой с ним.
Для того, чтобы использовать одни и те же шаблоны на клиенте и сервере, применяют либо кросс-платформенную систему шаблонизации, либо, чаще – интегрируют серверную часть с V8 и, возможно, с сервером Node.JS.
В главе было много ссылок на шаблонные системы. Все они достаточно современные, поддерживаемые и используются во многих проектах. Среди них вы наверняка найдёте нужную вам.