https://habrahabr.ru/post/333662/Время зоопарка шаблонизаторов миновало, теперь вокруг бегают динозаврики MVC, а в них используются встроенные шаблонизаторы и билдеры компонентов. Но для замены старых менее удобных шаблонизаторов в Knockout и Backbone иногда нужны они, в основном, остановившиеся в развитии на уровне около 2014 года.
Так случилось и с
DoT.js. Поначалу заброшенный авторами примерно на год в 2013-м, он получил их внимание ненадолго, поднявшись с версии 1.0.1 до 1.1.1, и снова был заброшен (или стабилизирован, смотря как рассуждать). В связи с этим ещё в 2013 году понадобилось (
делать клон DoT.js), а теперь — и апгрейдить его.
Он — такой же быстрый, как и встроенный
_.template()
в Underscore/Lodash, но с улучшенным синтаксисом, при котором необходимость писать JS в шаблонах встречается нечасто, а в Underscor-овском — нужна всегда. Этим скобкам со скриптами даже придумали специальный термин: javascript encapulated sections (JES), и от них, в основном, избавились.
Что получаем дополнительно?
1. Структура шаблонизатора
была переработана (в 2013-м, ссылка оттуда), чтобы лучше читалась и уменьшилось число декодирований функций;
2.
Тесты показали, что быстродействие в среднем не изменилось (колебания -3% — +10% в зависимости от параметров);
3. Добавлена команда работы по структуре, аналогично работе по массиву;
4. 4-й параметр — фильтр элементов структуры или массива;
5. Кое-где замедления вследствие обхода багов скомпенсированы оптимизацией кода и регекспов;
6. Глобальное имя «doT» способно меняться на другое в настройках (у оригинала — нет);
7. Наведён порядок в нумерации версий и версионировании глобальных функций encodeHTML() — инстансов, принятых здесь для оптимизации.
Для справки по нумерации версий
В npm копия версии 1.1.1 один-в-один в package.json названа как 1.1.2, но в файле — остался номер 1.1.1; в ветке 2.0 в репо — то же самое с необновлением номера 1.1.1 и есть всего 1 отличие в var rw = unescape(...)
. В общем, всё сделано для неразберихи. Поэтому считаем, что самая новая версия — 1.1.1, в которой учтём отличие из ветки 2.0. Ветка 2.0 своего звания не заслуживает.
По случаю, удобно сделать документацию (есть
от авторов), с инструментом для её проверки. (Что в Сети нашлось читабельного — ссылки внизу.) Если кратко:
• он сохранил «исконный» синтаксис Underscore
_.template()
, в котором внутри скобок "
{{ ... }}
" можем писать любой JS-код, включая незакрытые фигурные и операторные скобки, а снаружи скобок — HTML-фрагменты текста.
• скобки переопределить можно, переопределив в настройках все регекспы с ними (обычно не нужно);
• имя старшего элемента структуры
'it'
тоже переопределить можно, как и 4 логических настройки поведения;
• поддерживается AMD, commonJS и просто глобальное имя его (
'doT'
);
• кроме базового универсального синтаксиса, имеет ряд команд, подобных стилю Mustache/Handelbars;
• как и они, и
_.template()
, имеет 2 этапа шаблонизации (каррирования параметров) — в функцию и затем в HTML (или другой) код;
• не заточен строго под HTML, но привязан к JS, поэтому его сфера — браузеры и NodeJS;
• не намного больше по объёму, чем исходный код
_.template()
— 3.3 К в сжатом не зипованном виде.
(Для экспериментов с кодом нового или старого шаблонизатора можно воспользоваться старым примером в http://jsfiddle.net/6KU9Y/2/ (но далее есть js-фиддл лучше). Построена и в репозитории удобная страница тестирования 2 движков на общем шаблоне и данных — в test/index.html
. По умолчанию она сравнивает одинаковость результатов в развёрнутой и минифицированной версиях файлов. И второй движок может работать лишь с клоном, т.к. у него возможно глобальное имя, отличное от 'doT'
. Вместо минифицированной можно поставить клон, а на первом месте, например, оригинальный DoT.js 1.1.1 и смотреть различия в парсинге.)
В файле второго движка нужно перед открытием страницы установить имя
globalName:'doTmin'
.
Впрочем, и в онлайне то же самое несложно:
https://jsfiddle.net/spmbt/v3yvpbsu/23/embedded/#Result или с фреймами и редактированием кода, у кого большие экраны:
https://jsfiddle.net/spmbt/v3yvpbsu/23/. Файлы подключать не надо, лишь копипастить содержимое 2 версий DoT.js, а во второй, удобнее это делать в клоне — подправить имя
doT
на
doTmin
(даже если не minimized). По умолчанию выставлены DoT.js 1.1.1 оригинал и DoT12.js 1.2.1 — клон.
Бенчмарк (3-я кнопка) устроен по короткому промежутку времени, поэтому зависит от параллельных процессов, и о скорости синхронных алгоритмов можно судить при усреденении по большому числу измерений (среднее накапливается и позволяет немного судить о скоростях). Встроенный в проект бенчмарк есть, но он сложный, через Ноду, а делает то же самое (по группе тестовых файлов).
В страницу тестов встроены чекбокс автообновления и перехват ошибок, что позволяет проводить тесты по той версии, которая останется исправной. Так, легко сравнить скорости команд
'~'
и
'@'
по массиву. Вторая тоже работать может, но значительно медленнее — на 10-15% (проверяется кнопкой «Bench»). Это связано с необходимостью использовать более медленный цикл for-in во втором случае. Тем не менее, без for-in для структур не обойтись, чтобы не иметь необходимости подготовки массивов из структур для оригинальной версии, не имеющей команды
'@'
.
Сразу традиционно заметим, что 2-е вычисление (расчёт
по компилированному шаблону) идёт в сотню раз
быстрее (для коротких выражений), чем полная компиляция каждый раз (1-е число, по в 100 раз меньшему числу измерений). На скриншоте "
Comp1e3: 99.22ms
" означает: «1000 полных компиляций проведены за время 99.22ms». "
Run5e5: 69.08 ms
" в 5-й строчке означает: «50 тысяч быстрых генераций HTML по шаблону проведены за усреднённое время 69.08ms на каждые 10 тысяч генераций».
Добавлен
спадающий список примеров, из которого можно извлечь типичные примеры применения команд DoT.js и протестировать их в 2 версиях скрипта. Изменения примеров временно сохраняются и к ним можно вернуться, повторно выбрав прежний пункт примера, если страница браузера не была перезагружена.
О командах — подробнее
В начале пункта в кавычках будет приведено имя, которым в коде
DoT12.js
названо регулярное выражение, отвечающее за данную функцию (команду) шаблонизации.
Команда "valEncEval"
Один регексп под этим именем объединяет 3 прежних команды, имеющие сходный синтаксис.
{{ часть выражения и операторов JS }}
Универсальное наследие Underscore. Больше ничего не надо, оно самодостаточно, чтобы описать всё (пробелы у скобок не обязательны). Но читать… Читать больше 40 строк строго не рекомендуется. Незакрытые скобки JS перемежаются с закрытыми такими же фигурными скобками шаблонизатора. За ними идут незакрытые теги HTML. Это — нормальная практика, работает отлично, машина понимает. В то же время, Mustache/Handlebars уже читабельны.
{{= выражение JS }}
Значение выражения выкладывается в окружающий поток HTML, с сохранением тегов HTML и невзирая на незакрытые теговые скобки. Так, встретившийся
"<br>"
в значении выражения, если будет выложен в страницу браузера, будет вести себя не как текст, а как тег — приведёт к переносу строки по правилам HTML.
{{! выражение JS }}
То же, но возвращающее текст в выдачу с «обезопашиванием» кода HTML: html-теги и html-кодированные (
&...;
) символы превращаются в текст;
Команда "conditional"
{{? if-выражение }} then-шаблон {{?}}
Условное включение шаблона (пробелы у скобок не обязательны);
{{? if-выражение }} then-шаблон {{?? [if-else-выражение]}} [if-]else-шаблон {{?}}
ветвление и условные цепочки текстов шаблонов.
Выражение — любое, содержащее глобальные переменные или структуру с именем
'it'
, в которую передаётся параметр при шаблонизации.
if-else легко получить, инвертируя if-выражение. Синтаксис не стали усложнять, видимо, ради скорости.
Команда "use"
{{# выражение, выдающее строку }}
Аналог препроцессора (макрокоманд) — в строку можно вставить вначале любой фрагмент текста, включая непарные скобки шаблонов. Компилироваться будет результат. Например, через
{{# ...}}
можно внести в шаблон текст другого шаблона через переменную. Но задумана команда была для более простых и конкретных дел, в паре с командой «define».
Команда "define"
Появилась с версии 1.0. Поначалу решалось не в тексте шаблонов, а в списке параметров после настроек (3-й параметр в doT.template(шаблон, настройки, параметры)).
Формат определения переменной для «use»-команд в команде «define»:
{{## def.defin1 :что_угодно_до_скобки#}}
или {{## def.defin1 =что_угодно_до_скобки#}} - имеет другой смысл (функция)
Переменные могут быть с точками и $, в них определяется любая строка. Есть ряд хитростей.
Точки в имени (стиль кого-то копировали).
Если первые 4 символа — 'def.', они удаляются.
Если через двоеточие — записываются пары def[code] = {arg: param, text: v};
Через равенство определяется функция 'def' (удобно посмотреть на тестовой странице в списке примеров).
Можно в одном месте определить макрос, чтобы 2 и более раз использовать. Как все макросы — сомнительно с точки зрения качества кода. Если понадобятся макросы — это значит — понадобилось перед кем-то выгородиться, уменьшив размер текстов грязным способом, потому что вовремя не продумано, как правильно с точки зрения проекта его уменьшить. И на хранение переменных тратятся ресурсы скрипта.
Некоторый плюс есть в том, что разделяются окружения и места работы скрипта, как с парой команд use-useParams.
И есть семантическая разница в том, что скрипты выполняются на лету, в момент связывания шаблона с данными, а define и use — на шаг раньше, при связывании функции шаблона с шаблоном. Одним словом — это макросы.
Команда "defineParams"
{{##foo="bar"#}}
Так параметры определяются, накапливаясь во внутренней структруре, а затем используются много раз после.
Команда "useParams"
Использование ранее определённых параметров.
{{#def.foo}}
Ничто не мешает взять, и определить параметры в JS:
{{ операторы JS }}
кроме нежелания смешивать окружения шаблона и остального JS. Тут будет теряться подсветка синтаксиса и анализ кода в IDE и редакторах. Поэтому больше доводов за то, чтобы не писать JS в текстах шаблонов.
Команда "iterate"
Объединение 2 команд с разными скоростями и возможностями. While-шаблон
{{~...}}
— это пробежка по массиву циклом while. (Выбирали, очевидно, из всего и выбрали самое быстрое на тот момент в браузерах.) Работает быстрее на 10-15%, чем её альтернатива
{{@...}}
на for-in-шаблоне, которая может пробегать по массиву или по струкутуре. 4-й параметр — фильтрация элементов по выражению. В оригинальной версии поддерживается только массив и без фильтрации. Не устраивает — всегда есть "
{{ ... }}
" (писать удобно, читать — нет, как Perl или машинный код).
{{~ it : value : index : filter-expression }} while-шаблон {{~}}
где
it
— слово
'it'
(или другое), означающее первый аргумент, или глобальное имя массива, или выражение, возвращающее массив;
value
— любое имя, например,
'v'
или
'value'
без кавычек, которое будет использоваться в for-шаблоне на месте подставляемого значения элемента массива, например, в выражении
{{= value+1}}
;
index
— аналогично, любое имя, определяющее индекс элемента массива.
Да, параметры указаны «навыворот» (сначала value, потом index), но так сложилось у них, менять не будем и здесь, и в следующей похожей команде. Логика в том, что последний (и вообще последние)
index
можно опускать, если не нужен в шаблоне, вместе с двоеточием.
Опускать можно в клоне и другие параметры, оставляя двоеточия. В оригинале — нельзя: хотя бы букву, но писать надо. Первый параметр по умолчанию — 'it' (а точнее —
templateSettings.varname
), у остальных тоже есть умолчания, но они очень технические, незапоминаемые.
В сумме, всё сделано так, чтобы и компиляция, и исполнение шаблонов были максимально быстрыми, без лишних действий и красивостей. Скорее всего, можно ещё что-нибудь улучшить, а некоторые улучшения исполнения ухудшат время компиляции, поэтому во всём — разумный баланс.
{{@ it : value : index : expression }} for-in-шаблон {{@}}
Пробежка по структуре, по первому её уровню. Выполняется в браузере медленнее (% на 10-15), чем в массиве, нужно учитывать особенности порядка выдачи ключей-чисел и остальных ключей (числа идут вначале, потом все остальные ключи, кроме старых версий IE типа 8-й и ниже, Оперы и старых Fx того же прошлого времени. Где числа шли в общем ряду с другими.). Причём, порядок ни один стандарт не гарантирует, но он есть. Использовать его или проверять, или полагаться на массивы — дело разработчика. Самые авторитетные скажут, что доверять на 100% нельзя и будут правы. Такая же история и в Python, и в JSON.
В то же время, использования порядка, в котором пришёл JSON или была определена структура, сокращает объём кода процедур. 4-м параметром добавлено выражение — условие фильтра. Если false — элемент не выводится. (Если нет условия, сохраняется паразитное условие
"if(1)"
? это — плата за баланс.)
Примеры сокращений параметров
{{@::i}} for-in-шаблон {{@}}
это просто проход по всем элементам структуры.
Пример:
{{@::i}} <i>{{=it[i]}}</i> {{@}}
всё равно, что
{{@:v}} <i>{{=v}}</i> {{@}}
Вместо шаблонов — можно использовать массивы в
"@"
-командах. Но наоборот, в
"~"
-командах (в массивах) — структуры — нельзя, ради совместимости с оригиналом.
… Через лет 7 всё это пропадёт и умрёт под грузом новых великолепных фреймворков, где вопросы по-своему и по-другому будут решены. forEach, filter, reduce, JSX — убийцы шаблонов уже на пороге и одной ногой каждый по эту сторону двери. А управлять разметками экрана сможет даже шимпанзе. А пока — спешите поковыряться в костылях, пока они окончательно не стали историй, как К155ЛА3.
Что там за настройки и зачем нужны
varname: 'it',
Эта настройка — довольно понятная. В выражениях шаблонов применяется имя вместо arguments[0] (первого параметра промежуточной скомпилированной функции). arguments[0] далеко не всегда применишь из-за вложенных функций, а имя — почти всегда, если нет конфликтов. Вот если есть конфликты — имя можно сменить, причём, не только прямо в коде библиотечного шаблонизатора, что моветон, но и во 2-м параметре
doT.template()
, в локальных настройках текущей команды. (Есть ещё способ «статической» смены настроек, добравшись к ним по
window.doT.templateSettings
.)
Есть и другие специфические имена у этого шаблонизатора, которые могут вызвать конфликты. Их легко увидеть на странице тестов, нажав кнопку «Show function» на странице тестирования
test/index.html
. Это имена:
out, arr1, arr2
, ..., ll (две малые L), v, i (при проходе по массивам. Они экранируют такие же внешние имена. Но
it
— на особом счету, без неё — никуда, поэтому вынесена в настройки.
strip: true,
Выкидывание лишних пробелов, табов и переносов строк. Если форматирование выходного текста не важно, используем true. Функция и шаблон будут короче, а скорость компиляции, как ни покажется странным, чуть ниже (1-2%); исполнение — по скорости неотличимо. Т.е. для ускорения компиляции нужно ставить
strip: false
.
append: true,
Стиль добавления кусочков HTML-кода и данных в переменную out — суммированием в цепочке или операторами присваивания. Последнее удлиняет текст функции, поэтому, если особо не нужно — выбираем true. (Видимо, когда-то это было вопросом — что выбрать и что быстрее работает.)
log: true,
Не используется. Или забыли удалить, или нужно где-то в соседних скриптах типа NodeJS — express.
selfcontained: false,
Здесь скрывается небольшая история оптимизации. Функция
doT.template(...)
может быть подготовлена в одном общем окружении (
_globals
) и тут же исполнена как
doT.template(...)(...)
, а может — отдельно (прийти по ajax или из файла). В последнем случае нужно true (удлиняет функцию doT.template(...)), а обычно, в первом случае — false. Тогда не приходится в ней генерировать лишнего, а подсчитанное сохраняется в
_globals._encodeHTML
, генерируемой из
_globals.doT.encodeHTMLSource()
, но не всегда, а лишь при наличии команд
{{! выражение}}
в шаблонах.
Другими словами,
selfcontained = true
— значит, что функцию шаблона
doT.template()
будут использовать отдельно от doT.js, поэтому она должна содержать в себе всё для выполнения шаблонизации. Всё — это значит лишь особый случай кодирования HTML-символов командами
{{!}}
. Если они есть, в функцию нужно включить определение функции кодирования — строку
doT.encHtmlStr
при её создании (так сделано в клоне 1.2.1, а в оригинале функция
encodeHTMLSource
преобразуется в строку).
В версии 1.1.1 оригинала есть недоработка — алгоритм всегда «засовывает» код функции в шаблон, без сжатия, даже если
selfcontained = false
, это пришлось исправить. Ещё эта функция занимается связыванием параметра
doNotSkipEncoded
постоянно, хотя это нужно только при создании функции шаблона.
Затем, в оригинальном движке есть проблема конфликта версий, потому что они используют глобальный объект (window, globals) для оптимизации использования функции кодирования HTML. Её решили в клоне 1.2.1 тем, что глобальное имя функции кодирования выбрали зависящим от имени движка и версии. Получилось примерно так:
var encHt = '_'+dS.globalName + doT.version.replace(/\./g,'').
...
encHtmlStr:'var encodeHTML=typeof '+ encHt +'!="undefined"?'+ encHt +':function(c){return((c||"")+"").replace('
+ (dS.doNotSkipEncoded ?'/[&<>"\'\\/]/g':'/&(?!#?\\w+;)|[<>"\'/]/g')
+',function(s){return{"&":"&","<":"<",">":">",\'"\':""","\'":"'","/":"/"}[s]||s})};'
Получаем строку для вставки в функцию шаблона, но если
selfcontained = false
и есть
{{! выражение}}
, то ограничиваемся выполнением её в глобальном объекте, чтобы из него использовать
encodeHTML()
.
doNotSkipEncoded: false,
Аргумент
doT.encodeHTMLSource()
. Работает для функций
{{! выражение}}
— выдачи безопасного (без исполняемых тегов) HTML-кода. Если они есть в любом шаблоне окружения, первый раз определяется функция
_globals._encodeHTML
генерации безопасных символов для экономии повторных её вызовов. Сделано для решения таких багов:
github.com/olado/doT/issues/106. Если true, то не кодируются все коды вида
"&....;"
, и главный результат — некодирование амперсенда в
'&'
в таких выражениях.
Заключение
Для скорости компиляции, очевидно нужны такие параметры, которые требуют меньше действий: по возможности,
selfcontained = false
,
strip: false
,
append: true
. Остальные части doT оптимизированы хорошо, выбраны в среднем самые быстрые решения. Скорость версий зависит от конкретного вида шаблонов, поэтому утверждение о скорости может быть лишь усреднённым по кругу задач.
doNotSkipEncoded
влияет на результат в командах
{{!...}}
: при
true
останутся без изменений кодированные символы вида
&...;
.
В целом, клон компилирует шаблон в функцию несколько медленнее из-за увеличенного объёма анализа кода, который нужно делать для решения некоторых багов. Например, удлинена функция unescape(). Если из неё убрать 2 последних replace, скорость увеличится на 3% (Chrome v.61 Canary), но будут некоторые баги.
Если не обращать внимание на единицы процентов, то шаблонизатор doT.js — один из наиболее быстрых и, в то же время, компактный. ES6 и даже новые методы массивов не использует — написан не в ту эпоху. Это даёт плюс в том, что поддерживается всеми браузерами (должен работать и в IE8). В IE11 протестирован. На тестовой странице test.index.html
performance.now()
полифиллится.
• Сравнение версий, тесты и накопительные бенчмарки для DoT — DoT12:
https://jsfiddle.net/spmbt/v3yvpbsu/22/embedded/#Result• Сравнение версий, тесты и накопительные бенчмарки для DoT — DoT12:
https://jsfiddle.net/spmbt/v3yvpbsu/23/embedded/#Result или с фреймами и редактированием кода:
https://jsfiddle.net/spmbt/v3yvpbsu/23/
•
Гитхаб DoT12.js (клон оригинала),
DoT.js.
•
JSFiddle для экспериментов с шаблонизатором (код 2013 года) и шаблонами (изначально внесён клон Dot; в других соседних номерах фиддлов читатели могли оставить результаты своих экспериментов, которые они могут документировать и оставить ссылку в комментарии; проверить работу своей версии без сохранения — нажать кнопку «Run»).
•
Статья по клону DoT.js 2013 года с тестами производительности.
*
Closure Compiler — сжимает немного лучше в Advanced mode, чем Uglify;
*
Using doT.js (хорошая подборка примеров, 2012)
*
doT.js: chained if-else if in dot.js Способы записать цепочки if-else;
*
Документация с песочницами, от авторов, подробно, с поясняющими примерами по ссылкам.