
Вот, наконец дошли руки поделиться с людьми одним из множества велосипедов (как сейчас называют личные наработки). До хабраката пару плюсов и минусов этого решения:
Из плюсов:
- скорость jsperf (тест с прекомпилированными шаблонами: 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, но со своей кучей «вкусняшек». Если будет интерес к подобным вещам (это ведь не зарелизеные библиотеки для продакшн) тоже выложу на гитхаб, и напишу статью.
Удачи!
