Вот, наконец дошли руки поделиться с людьми одним из множества велосипедов (как сейчас называют личные наработки). До хабраката пару плюсов и минусов этого решения:
Из плюсов:
- скорость jsperf
- расширяемость := кастомные контролы, трансформация шаблонных данных
- data bindings
- компиляция в json для дальнейшего кэширования
- приятный синтаксис (без мешанины логики и структуры)
Из недостатков (хотя это реализуемо, просто мне это без надобности):
- шаблонные данные могут находиться только в атрибутах и литералах
Если тема интересная —
Вступление
Давайте я сразу принесу свои извинения за грамматику/стилистику. Хочется написать все очень кратко, но в тоже время, чтобы все меня поняли, и к тому же правильно поняли. Вы же не компиляторы, которые всё «понимают» буквально — а каждый со своим опыт, суждением и прочим. А так как опыта в написании статей у меня мало, как и опыта в писании на русском — то это в корни перечёркивает надежду написать всё так, как я это представляю. Но я попробую, а вы не ругайтесь.
Немного истории
Пару лет назад мне понадобилась функция вида String.format('N: #{a} : #{b}',{a:1,b:2})
.
Вскоре я понял, что используя её для форматирования html, у меня, по большей части, связаны руки. Ведь так хочется форматирование по условию, условной видимости и списков. Посмотрев на разные шаблонные движки, ощутил дикое отвращение к смеси html и javascript, плюс использование with(){}
и eval/new Function('')
тоже не радовало. Подумав, что «мне то совсем чуть-чуть надо» и решил написать для себя сам. Так родились два тэга list
и visible
и формат вида #{a==1?1:-1}
. Mне достаточно было лишь найти рэгэкспом эти тэги, ну а дальше String.format
. И вот полтора года этот движок отлично нёс свою службу — был шустрее шустрых и надёжнее надёжных.
«А нам всегда мало...»
Как не прискорбно, но мы так устроены — ну, по крайней мере, я. Захотелось ещё быстрее, ещё более расширяемо и чтобы дальше без месива html/javascript. На этот момент я знал точно: хочу кастомные тэги — что бы по десять раз не писать одну и ту же html структуру и без placeholder-ов(что бы после вставки в дом туда рэндерить). И вот как только я сел допиливать парсер для обработки дополнительных тэгов, тут как назло проснулось стремление к искусству, которое начало мусолить — "Перепиши. Перепиши код. Это же Ford Mondeo 93-го. А сейчас уже даже миллениум давно был. Да вот ещё и синтаксис шаблонов другой надо. Ты что не видел CoffeeScript Sass/Less ZenCoding? Перепиши, кому говорят, иначе уснуть не дам — так и знай". И под этим давлением я не устоял. Но все же главным приоритетом, это была скорость движка — никому не нужен красивый, но со 100 л.с. болид на старте.
Переходим к делу: Дерево
Так как используем свой синтаксис, его надо преобразовывать в дерево. Имеем два типа узлов: Тэг = {tagName:'someTagName', attr: { key:'value'}, nodes:[] }
и Литерал = {content:'Some String'}
. А так как за 1.5 года использования старого шаблонизатора, я ни разу не помню, что бы подставлял шаблонные данные в название тэга, или названия атрибута, то для простоты шаблона и анализатора, сделаем возможным вставки данных только в них. Поэтому узлы будут следующего вида: Тэг := {tagName:'name', attr: { key:('value'||function)}, nodes:[] }
и Литерал := {content:('Some String'||function)}
. Где function
— это функция которая подставляет шаблонные данные и она только у тех значениях, которые их требуют. Вот мы и посадили дерево, пока что ничего сложного( дальше сложнее тоже не будет).
Анализатор/Парсер
Интересные моменты:
- анализируем линейно, по возможности даже перепрыгнем участки, для дальнейшего «substring»
- минимально используем вызовы функций, рекурсию совсем не используем
- для анализа используем объект:
var T = {index:0 /* currentIndex */, length:template.length, template:template}
. При вызове дополнительных функций, передаём его(вернее передаётся ссылка на него). Таким образомtemplate string
не копируется - charCodeAt — не особо быстрее charAt или [], но дальнейшая робота с number быстрее
Сам парсинг до боли прост — составляет 40 строк. (парсинг атрибутов надо было бы не выносить в отдельную функцию, но потерялась бы наглядность)
var current = T;
for (; T.index < T.length; T.index++) {
var c = T.template.charCodeAt(T.index);
switch (c) {
case 32: //" "
continue;
case 39: // "'" парсим литерал
T.index++;
var content = T.sliceToChar("'");
// в sliceToChar используется indexOf с проверкой на 'escape character'
// поэтому след. indexOf имеет смысл, так как, это быстрее чем линейно проверять charCodeAt/charAt/[]
if (~content.indexOf('#{')) content = T.serialize == null ? this.toFunction(content) : {
template: content
};
current.nodes.push({
content: content
});
if (current.__single) {
//если это одинарный тэг, переходим к предку, который не одинарный; пример div > ul > li > span > 'Some'
if (current == null) continue;
do (current = current.parent)
while (current != null && current.__single != null);
}
continue;
case 62: /* '>' */
current.__single = true;
continue;
case 123: /* '{' */
continue;
case 59: /* ';' */
case 125: /* '}' */
if (current == null) continue;
// тэг закрыт ; , или закончился } - переходим к предку
do(current = current.parent)
while (current != null && current.__single != null);
continue;
}
//знакомые символы не встретились - парсим tag с атрибутами
var start = T.index;
do(c = T.template.charCodeAt(++T.index))
while (c !== 32 && c !== 35 && c !== 46 && c !== 59 && c !== 123); /** while !: ' ', # , . , ; , { */
var tag = {
tagName: T.template.substring(start, T.index),
parent: current
};
current.nodes.push(tag);
current = tag;
this.parseAttributes(T, current);
//парсинг атрибута закончится на ; > {, чуть чуть назад отступим
T.index--;
}
Конструктор
Имея дерево, негоже строить html string
для вставки в документ, надо строить сразу documentFragment
(хотя function renderHtml
тоже оставил, на всякий случай). Этим мы сильно компенсируем затраченное время на парсинг.
Сам процесс снова тривиальный:
function buildDom(node, values, container) {
if (container == null) container = document.createDocumentFragment();
if (node instanceof Array) {
for (var i = 0, length = node.length; i < length; i++) buildDom(node[i], values, container);
return container;
}
if (CustomTags.all[node.tagName] != null) {
var custom = new CustomTags.all[node.tagName]();
for (var key in node) custom[key] = node[key];
custom.render(values, container);
return container;
}
if (node.content != null) {
//это литерал
container.appendChild(document.createTextNode(typeof node.content === 'function' ? node.content(values) : node.content));
return container;
}
var tag = document.createElement(node.tagName);
for (var key in node.attr) {
var value = typeof node.attr[key] == 'function' ? node.attr[key](values) : node.attr[key];
if (value) tag.setAttribute(key, value);
}
if (node.nodes != null) {
buildDom(node.nodes, values, tag);
}
container.appendChild(tag);
return container;
}
Кастомные контролы
Как видно выше из кода, здесь на сцену выходят кастомные контролы. Если конструктор встретит зарегистрированный обработчик тэга, он создаст объект обработчика, сделает shallow copy
значений attr
и nodes
и передаст контекст сборки в функцию render. То есть наш контрол должен реализовать в прототипе функцию .render(currentValues, container)
toFunction(templateString)
Собственно это то место, где происходит магия. Правда магией это не назовёшь, а так хотелось бы. На самом деле тут получаем части для вставки наших данных, это
- или ключи к свойствам входных данных, возможна «рекурсия»
#{obj.other.value}
- или обращение к функции трансформаторе
#{fnName:line}
. Если fnName пустая строка считается что line это условие и будет выполнено функциейValueUtilities.condition(line, values)
, обрабатываем их и вставляем в template.
Ну вот, кажется все осветил. В примерах можно больше интересного увидеть, там же реализация дата биндингов, через контролы. Посмотрите также исходники страницы примеров.
Примеры
Исходники
оффтоп:
В арсенале много ещё каких «великов», например, IncludeJS — похоже на Require, но со своей кучей «вкусняшек». Если будет интерес к подобным вещам (это ведь не зарелизеные библиотеки для продакшн) тоже выложу на гитхаб, и напишу статью.
Удачи!
Автор: tenbits