Что вы делаете, когда вам нужны HTML-шаблоны, но вы не хотите включать какую-либо тяжелую библиотеку или фреймворк? Вы экспериментируете и пишете свою собственную реализацию ... Добро пожаловать в путешествие по DOM, регулярным выражениям и построителям функций.

Эта статья была первоначально опубликована в 2016 году по адресу https://gpf-js.blogspot.com/2016/08/my-own-templates-implementation.html

Необходимость

В библиотеке GPF-JS исходный и тестовый файлы организованы с помощью одного специального конфигурационного файла: sources.json. Это позвоночник проекта, так как на нем основаны все инструменты, связанные с построением, тестированием или даже документированием.

Генерация документации основана на JSDoc и подключаемом модуле grunt, но кодовая база требует дополнительной очистки. Следовательно, в настоящее время рассматривается только несколько файлов.

Это хранилище JSON перечисляет файлы и связывает с ними свойства:

  • Текстовое описание исходного контента
  • Отметьте, чтобы узнать, есть ли у него тестовая копия
  • Необязательный флаг, разрешающий извлечение документации
  • Дополнительные флаги документации, подчеркивающие наиболее важные части источника (такие как реализация класса, имя основного метода…)

Поскольку концепции максимально изолированы, этот файл быстро вырос с 134 строк в апреле до 334 строк в июне, все вводились вручную (с множеством ошибок, ведущих к что происходит?, о нет, опять ничего не работает… ).

В какой-то момент его содержимое заслуживает небольшого контроля не только для обеспечения соблюдения синтаксиса, но и для лучшего обзора того, что он содержит.

Поэтому я решил, что было бы неплохо разработать HTML-представление об этом.

Создание HTML-страниц

Короче говоря, все началось с простого списка, форматирующего содержимое файла. Обновление будет позже. Следовательно, была создана базовая HTML-страница для отображения таблицы (не очень отзывчивой, но на данный момент это не требуется).

Загрузить файл JSON с помощью AJAX-запроса и перебрать его содержимое легко, но тогда… возможны несколько вариантов.

DOM API

Браузеры сейчас предлагают полный (и стандартизированный) API для управления объектной моделью документа. Он позволяет программно заполнять страницу так же, как и со статическим HTML-кодом.

ПРОФИ

  • Быстро
  • Полный контроль над поколением
  • Может быть отлажен

МИНУСЫ

  • Исчерпывающий, но сложный API
  • Требуется больше времени, чтобы развиваться
  • Длинный код для простого вывода
  • Код довольно загадочный и сложный для развития
  • Не легко обслуживается

Пример DOM API

var data = {
    title: "Code sample",
    id: "test",
    checked: "checked",
    label: "it works"
};
var h1 = document.body.appendChild(document.createElement("h1"));
h1.innerHTML = data.title;
var input = document.body.appendChild(document.createElement("input"));
input.setAttribute("id", data.id);
input.setAttribute("type", "checkbox");
input.setAttribute("checked", "");
var label = document.body.appendChild(document.createElement("label"));
label.setAttribute("for", data.id);
label.setAttribute("title", data.label);
label.innerHTML = data.label;

Пример создания HTML-содержимого с использованием DOM API

Дополнительная литература: Введение в DOM

Движок шаблонов

Механизмы шаблонов обычно полагаются на статическое описание конечного результата. Документированный синтаксис предлагает заполнители для представления точек замещения. В зависимости от движка может быть несколько способов ввода значений. Они разработаны, чтобы быть быстрыми, предлагать общие помощники (такие как перечисление) и обширные привязки (с типизацией, преобразованием…).

ПРОФИ

  • Достаточно быстро (зависит от двигателя)
  • Меньше кода для разработки
  • Легко поддерживать
  • Кривая быстрого обучения

МИНУСЫ

  • У каждого движка есть свои соглашения и API.
  • Отладка

Образец усов

var html = Mustache.to_html(document.getElementById("tpl").innerHTML, {
    title: "Mustache sample",
    id: "test",
    checked: "checked",
    label: "it works"
});
document.body.appendChild(document.createElement("div")).innerHTML = html;

Пример создания HTML-контента с использованием шаблонизатора Mustache

где шаблон определяется как:

<script id="tpl" type="text/template">
    <h1>{{title}}</h1>
    <input id="{{id}}" type="checkbox" {{checked}}>
    <label for="{{id}}" title="{{label}}">{{label}}</label>
</script>

Пример шаблона для Mustache

Небольшое примечание о теге сценария с type = ”text / template”, это уловка, которая не позволяет браузеру фактически выполнить содержимое тега сценария. Однако он остается доступным для любого пользовательского кодирования.

Пример ссылки: mustache.js

Фреймворк

Короче говоря, фреймворк преобразует любую веб-страницу в веб-приложение: он инкапсулирует больше, чем просто определение пользовательского интерфейса и поведение.

Я рекомендую прочитать этот манифест против фреймворков, он проводит грань между библиотеками и фреймворками и предлагает интересную точку зрения на то, почему мы должны избегать фреймворков, чтобы продвигать инновации как стандарт.

При этом каждый фреймворк имеет свои особенности, но, что касается построения пользовательского интерфейса, я бы выделил 2 основных типа:

  • Фреймворки на основе виджетов (ExtJS, Open UI5…): каждый элемент пользовательского интерфейса заключен в класс управления. Создание интерфейса может быть выполнено с помощью статических описаний (например, XML) или кода.
  • Фреймворки на основе HTML (AngularJS, EmberJS…): основанные на HTML, затем они дополняются привязками

ПРОФИ

  • Кодовая база (образцы, документация…)
  • Ориентирован на приложения (делает больше, чем создание шаблонов)

МИНУСЫ

  • Тяжелый
  • Долгая кривая обучения
  • Может стать кошмаром для отладки, если что-то пойдет не так
  • Дизайн может выглядеть жестким

Угловой образец

var myApp = angular.module('myApp',[]);
myApp.controller('SampleController', ['$scope', function($scope) {
    $scope.title = "Angular sample";
    $scope.id="test";
    $scope.checked=true;
    $scope.label="it works";
}]);

Пример создания HTML-содержимого с использованием Angular

где тело определяется как:

<html ng-app="myApp">
    <!-- ... -->
    <body ng-controller="SampleController">
        <h1>{{title}}</h1>
        <input id="{{id}}" type="checkbox" ng-checked="checked">
        <label for="{{id}}" title="{{label}}">{{label}}</label>
    </body>
</html>

Пример шаблона, разработанного для Angular

Пример ссылки: Angular JS

Создание простого шаблонизатора

Можно использовать фреймворк, но это слишком много для того, что должно быть достигнуто. И, конечно, усы уместны, но я бы упустил возможность узнавать новое.

Что касается требований, ожидаемые преимущества простого механизма шаблонов:

  • Гибкий и простой способ определения действительного HTML
  • Простые текстовые привязки
  • Внедрение JavaScript

Движок должен сгенерировать функцию, принимающую как минимум два параметра:

  • Объект, предоставляющий значения для подстановки
  • Индекс, который будет различать объекты при использовании в перечислении

Результатом будет DOM-узел, который можно разместить где угодно (например, с помощью appendChild).

С точки зрения синтаксиса в определении шаблона будут приняты следующие шаблоны:

  • {{fieldName}} следует заменить полем объекта с именем fieldName: его можно использовать внутри любого текстового содержимого (атрибутов или текстовых узлов).
  • {% JAVASCRIPT CODE%} для внедрения JavaScript (с некоторыми ограничениями, см. ниже)

Во введенном коде будут предоставлены помощники JavaScript для изменения / изменения вывода:

  • $ write () для вывода любого HTML-кода (следует использовать осторожно)
  • $ object возвращает текущий объект
  • $ index дает текущий индекс

Случай флажка

Большая часть генерируемого контента просто состоит в замене заполнителей текстом, исходящим от объекта (имя источника, описание…). Это может быть текстовый узел (между элементами) или значение атрибута (например, для идентификаторов…).

Однако когда дело дошло до рендеринга логических параметров, возникла неожиданная проблема.

Действительно, более простой способ представить логическое значение - использовать ввод с типом, установленным в поле флажка.

Но флажок будет установлен или нет, в зависимости от наличия отмеченного атрибута, независимо от его значения.

Таким образом, механизм шаблонов должен предлагать способ изменения определения элемента путем добавления атрибутов.

Работая над синтаксисом, пробовал разные подходы. Первая попытка выглядела так:

<input type="checkbox" {% JAVASCRIPT CODE %}>

Это просто, однако результат анализа генерирует странную строку HTML:

"<input type=\"checkbox\" {%=\"\" javascript=\"\" code=\"\" %}=\"\">"

Один из простых способов узнать этот проанализированный результат - открыть отладчик, захватить дескриптор родительского элемента и запросить свойство innerHTML.

Действительно, каждый блок символов распознается как отдельный атрибут.

Итак, я попробовал следующий:

<input type="checkbox" {%%}="JAVASCRIPT CODE">

И на этот раз строка выглядела хорошо:

"<input type=\"checkbox\" {%%}=\"JAVASCRIPT CODE\">"

Это также означает, что код JavaScript правильно экранирован, чтобы соответствовать значению атрибута. Например, он может использовать одинарные кавычки для строк вместо двойных кавычек.

Перечитывая эту часть, я понимаю, что могу также использовать атрибут с именем {isChecked} и установить для поля isChecked значение «checked» или «что угодно» в зависимости от того, хочу ли я установить флажок или нет. Однако в этом случае значение должно быть предварительно отформатировано, чего я хочу избежать.

Тег шаблона

Допустим, вы хотите определить файл конфигурации, который должен использоваться приложением JavaScript. Как бы вы определили его синтаксис и содержание? Некоторые могут изобрести специальный API и запросить файл как действительную программу JavaScript. Другой может указать синтаксис для декларативной установки конфигурации.

У каждой версии есть свои достоинства и недостатки:

  • программный подход максимизирует возможности при настройке конфигурации (обнаружение среды, условия, петли ...), но с затратами с точки зрения обслуживания, совместимости и миграции
  • декларативный подход упрощает файл, но также дает ограничения на то, что вы можете с ним делать.

На мой взгляд, декларация должна быть предпочтительнее реализации, поэтому, наверное, я использую grunt вместо gulp. Основная причина в том, что меньше кода означает меньше ошибок.

Когда синтаксический анализатор уже существует (браузер в нашем случае или формат JSON в предыдущем примере), это обеспечивает соблюдение синтаксиса и упрощает реализацию.

Элемент шаблона - это тег HTML, который принимает любое содержимое HTML. Когда вы обращаетесь к нему, этот элемент DOM предоставляет свойство содержимого, которое можно использовать и изменять почти так же, как и любой другой элемент.

Также вы можете получить доступ к члену innerHTML.

Обратите внимание, что этот элемент не поддерживается IE

Фактически, практически любой элемент HTML можно использовать таким же образом. Однако у первого шаблона есть два существенных преимущества:

  • Он анализируется, но не отображается: он ускоряет загрузку страницы и не требует специальной обработки, чтобы скрыть его.
  • Он принимает любое содержимое HTML: попробуйте установить для innerHTML значение ‹tr› ‹/tr› в элементе DIV, он его не примет.

Итак - после нескольких изменений - вот содержание шаблона, иллюстрирующее все функции:

<body>
    <template id="tpl_row">
{%
        function check(a, b) {
            if ($object[a] && (b === undefined || $object[b])) {
                $write("checked");
            }
        }
%}
        <tr>
            <td>{{name}}</td>
            <td>{{description}}</td>
            <td><input type="checkbox" {%%}="check('load');"></td>
            <td><input type="checkbox" {%%}="check('load', 'test');"></td>
            <td><input type="checkbox" {%%}="check('doc');"></td>
        </tr>
    </template>
</body>

Токенизация

Теперь, когда у нас есть контент, давайте посмотрим, как мы можем изолировать каждую часть, чтобы отличить статический текст от мест, где требуются замены. Этот процесс называется токенизация.

До недавнего времени я не был большим поклонником регулярных выражений. У меня создалось впечатление, что они были медленными и бесполезными, потому что они только сообщали вам, соответствует ли строка заданному шаблону.

Затем я прочитал книгу JavaScript The Good Parts от Дугласа Крокфорда. Глава 7 открыла глаза. Действительно, помимо сопоставления с шаблоном (и предоставления информации о том, что и где), он также может извлекать из него конкретную информацию, используя группы захвата (скобки).

Я также настоятельно рекомендую прочитать этот сайт, на котором представлена ​​ценная информация о движке.

Что касается производительности, они могут быть эффективными или очень медленными в зависимости от того, как вы их пишете, проверьте следующую документацию:

По-прежнему существуют ситуации, когда регулярные выражения JavaScript не подходят. Например, когда строка, которую вы хотите сопоставить, является потоком, вам нужен текстовый движок, который можно прерывать. Я начал внедрять такой механизм (проверено) в GPF-JS.

Итак, для каждого шаблона регулярное выражение способно найти его и извлечь его содержимое:

Когда все три объединяются с дополнительным регистром по умолчанию, получается шаблон токенизации.

Оттуда алгоритм состоит в сопоставлении строки через это регулярное выражение и обработке каждого токена по очереди.

Не забудьте сбросить свойство lastIndex перед сопоставлением новой строки.

Генерация кода

Если у вас хватило терпения прочитать эту статью до этой части: поздравляем! Вы дошли до самой забавной части.

Первая версия помощника возвращала функцию, которая не только токенизировала HTML-код шаблона, но и заменяла все токены вместе. Затем я понял, что было бы быстрее сначала токенизировать контент, а затем сгенерировать функцию, которая выполняет только замену.

Поэтому я переписал его, чтобы динамически создавать функцию шаблона на основе описания шаблона.

Меня по-настоящему увлекает процесс генерации кода: в большинстве случаев результат оказывается быстрее, чем при традиционном способе работы, поскольку он создает узкоспециализированные функции.

Есть несколько способов сгенерировать код на JavaScript, два наиболее распространенных:

Есть и другие способы, которые более разработаны. Например, можно также загрузить скрипт из URL-адреса данных. Но давайте будем простыми.

В целом eval - плохая идея: он объединяет контент в локальной области видимости и является открытой дверью для нежелательной инъекции кода или неожиданных побочных эффектов. Строгий режим имеет некоторые механизмы безопасности, но большинство линтеров все равно его отвергнет. И я согласен: eval - это зло.

С другой стороны, конструктор Function создает новую функцию в изолированной области. Это полная противоположность, потому что в результате вы не можете получить доступ к каким-либо символам вашего приложения. Тем не менее, это открытая дверь для внедрения кода, если вы не контролируете то, что помещаете в тело функции, но, по крайней мере, влияние будет ограничено.

Большинство скриптовых движков предлагает точку доступа к основному контекстному объекту (также известному как область верхнего уровня), то есть окну для браузеров или глобальному в NodeJS. Вы также можете получить к нему доступ, вызвав функцию с нулевой областью видимости в нестрогой среде. Оттуда вы можете получить доступ ко всем глобальным определениям.

Я также рекомендую эту интересную статью Написание среды JavaScript - оценка изолированного кода: в ней предлагается альтернатива ES6 для создания реальных изолированных сред.

Конструктор фабрики поддерживает массив строк кода (с именем code) для окончательного создания функции.

Лучший способ объяснить, как это работает, - показать результат на образцах, используемых в сценариях тестирования.

Сгенерированные функции были извлечены с помощью chrome development tools и переформатированы с помощью кнопки pretty print.

Начнем с образца 1:

<template id="sample1">This is a static one</template>

Сгенерированная функция:

(function() {
    var __a = arguments
      , $object = __a[0]
      , $index = __a[1]
      , __r = []
      , __d = document
      , __t = __d.createElement("template");
    function __s(v) {
        if (undefined === v)
            return "";
        return v.toString();
    }
    function $write(t) {
        __r.push(__s(t));
    }
    __r.push("This is a static one");
    __t.innerHTML = __r.join("");
    return __d.importNode(__t.content, true);
}
)

Поскольку шаблон допускает внедрение JavaScript, важно убедиться, что введенный код не будет конфликтовать с тем, который обеспечивает механизм шаблона (в основном все, кроме строки, содержащей Это статический код). Я до сих пор помню это забавное предупреждение PHP об использовании магического __ в именах, поэтому я решил добавить его к внутренним именам.

Так что у тебя есть:

  • __a ярлык для аргументов
  • __r результирующий HTML-массив (массив используется для ускорения конкатенации)
  • __t новый элемент шаблона (который получит HTML-код результата)
  • __s метод, преобразующий свой параметр в строку (требуется для $ write и базовой привязки).

Теперь, если мы посмотрим на пример 4: он вводит простую привязку ($ object.title и $ object.content).

<template id="sample4"><h1>{{title}}</h1>{{content}}</template>

Сгенерированная функция:

(function() {
    var __a = arguments
      , $object = __a[0]
      , $index = __a[1]
      , __r = []
      , __d = document
      , __t = __d.createElement("template");
    function __s(v) {
        if (undefined === v)
            return "";
        return v.toString();
    }
    function $write(t) {
        __r.push(__s(t));
    }
    __r.push("<h1>;");
    __r.push(__s($object.title));
    __r.push("</h1>;");
    __r.push(__s($object.content));
    __t.innerHTML = __r.join("");
    return __d.importNode(__t.content, true);
}
)

Шаблон {{name}} заменяется на __r.push (__ s ($ object. name));

Пример 7 иллюстрирует версию атрибута внедрения кода.

<template id="sample7"><input type="checkbox" {%%}="if ($object.check) $write('checked=\'true\'');"></template>

Сгенерированная функция:

(function() {
    var __a = arguments
      , $object = __a[0]
      , $index = __a[1]
      , __r = []
      , __d = document
      , __t = __d.createElement("template");
    function __s(v) {
        if (undefined === v)
            return "";
        return v.toString();
    }
    function $write(t) {
        __r.push(__s(t));
    }
    __r.push("<input type=\"checkbox\" ");
    if ($object.check)
        $write('checked=\'true\'');
    __r.push(">");
    __t.innerHTML = __r.join("");
    return __d.importNode(__t.content, true);
}
)

Код вставляется в функцию результата «как есть».

Наконец, пример 8 показывает внедрение JavaScript в генерацию условия:

<template id="sample8">{% if ($object.condition) { %}<span>{% $write("Hello"); %}</span>{% } else { %}<div></div>{% } %}</template>

Сгенерированная функция:

(function() {
    var __a = arguments
      , $object = __a[0]
      , $index = __a[1]
      , __r = []
      , __d = document
      , __t = __d.createElement("template");
    function __s(v) {
        if (undefined === v)
            return "";
        return v.toString();
    }
    function $write(t) {
        __r.push(__s(t));
    }
    if ($object.condition) {
        __r.push("<span>");
        $write("Hello");
        __r.push("</span>");
    } else {
        __r.push("<div></div>");
    }
    __t.innerHTML = __r.join("");
    return __d.importNode(__t.content, true);
}
)

То же решение: код дословно копируется в тело функции.

Тестирование

Конечно, как убежденный практик TDD, я создал тестовые сценарии для проверки наиболее распространенных вариантов использования этой библиотеки шаблонов.

Заключение

Целью этого упражнения было попытаться реализовать минималистичный шаблонизатор, который бы соответствовал моим потребностям, я бы не стал делать вид, что он идеальный или полностью функциональный. Однако, помимо реализации, меня больше интересуют уроки, которые я извлек из нее. Я надеюсь, что вы тоже извлечете уроки из этого.

Как всегда, приветствуются любые отзывы.

Сейчас я доволен результатом: уменьшенная версия mustache.js занимает около 9 КБ, моя - всего 1 КБ (при использовании jscompress.com).

Но можно было бы еще много чего добавить, например:

  • управление ошибками (относитесь к исходной строке шаблона, если что-то не так при построении функции)
  • Двустороннее крепление (я бы хотел попробовать это…)
  • помощники по перечислению (например, для каждого свойства объекта)
  • условные помощники