javascript

Доработки шаблонизатора DoT.js

  • четверг, 20 июля 2017 г. в 03:12:09
https://habrahabr.ru/post/333662/
  • Node.JS
  • JavaScript
  • HTML


Время зоопарка шаблонизаторов миновало, теперь вокруг бегают динозаврики 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 и смотреть различия в парсинге.)

image

В файле второго движка нужно перед открытием страницы установить имя 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;
* Документация с песочницами, от авторов, подробно, с поясняющими примерами по ссылкам.