Правильная шаблонизация
Шаблоны — слой преобразования (или схема, набор инструкций) данных из одного формата в другой.
Другими словами, это функция, которая что-то принимает
на вход, кашеварит, и выдает результат.
Функция (отображение, оператор, преобразование) — математическое понятие, отражающее связь между элементами множеств. /Wikipedia
Исходя из этого, окружение не должно влиять на преобразование, и оно само не должно влиять на окружение (т.е. функция должна быть чистой). Это необходимо, в т.ч., для мемоизации (или кеширования) результата работы.
В общем случае, шаблонный движок является функцией из двух переменных, одна из которых — правила преобразования, а вторая — данные. Компиляция шаблона — ничто иное, как частичное применение этой функции.
Веб-шаблоны
Веб-шаблоны — это подмножество шаблонов, ориентированное
на веб-ресурсы. Конечный результат исполнения таких шаблонов — HTML-код.
Шаблонами чаще всего называются шаблонизаторы для HTML, но частным случаем применения можно также считать различные CSS- (LESS, SASS, Stylus, Roole), JS-препроцессоры (Babel, CoffeeScript), постпроцессоры (PostCSS, Autoprefixer) и аналогичные инструменты, поскольку эти преобразования суть частичное применение функции преобразования.
«Родословная» шаблонизаторов
Если не брать во внимание примеры из обычной жизни (вроде трафаретов и образцов), то корни шаблонизации в разработке уходят в препроцессоры для текста. Например, широкое распространение получили препроцессоры в Си, они до сих пор используются во всех основанных на Си языках. В случае
с исходными кодами — это необходимо для упрощения процесса сборки: заголовочные файлы и условные вставки (#include), исходя из переменных окружения. И вопрос шаблонизации стоит тем острее, чем сложнее становится писать и отлаживать код.
В пример так же можно привести Lua, который изначально использовался для описания процессов и сценариев в игровых движках, а теперь используется и в Web (см. Модуль Lua для Nginx и Tarantool). И эти решения вполне оправданы — код на Lua работает в своей «песочнице» (виртуальной машине), которая предоставляет в рантайм нужные ручки к окружению, язык же
не имеет строгой типизации (что сильно упрощает вывод), и компилируется на лету (Just-In-Time или JIT).
И все это замечательно, но при чем тут шаблонизация?
Дело в том, что задачи, которые решает Lua, очень схожи с задачами, которые решались путем серверной шаблонизации с самого рассвета Интернета. И дело не только в строгой типизации в классических языках или сложной поддержке, хотя, конечно, и в этом тоже, но самое главное — в упрощении и ускорении процесса разработки. Шаблонный язык, часто, синтаксически сильно проще базового языка, но более специализирован. Правда, часто дело доходило до того, что было стыдно не выделять отдельное View из логики приложения, даже когда прямой необходимости в шаблонизации не было.
Именно эти факторы: переиспользование кода, условные операторы, рантайм или VM, упрощающий процесс отрисовки, — породили нишу шаблонных движков и начали эпоху шаблонизаторов.
Потребность в шаблонизации
Всеми любимый PHP появился как движок на Perl — поскольку не хватало выразительности и решал именно эти задачи: он позволял разбивать код на кусочки, которые в последствии можно было использовать, предоставлял доступ к окружению Perl, уменьшал кол-во мусорного кода. Фактически, до 4-ой версии код на PHP являлся ничем иным, как html-файлами со вставками логики.
В 2000-ом году, когда PHP дорос до 4-ой версии, и когда на нем начали писаться более серьезные сайты и CMS (грубо говоря, это шаблонизатор, написанный на шаблонизаторе), появился Smarty.
Он еще сильнее отделял представление от логики, с технической точки зрения это был синтаксический «сахар» для PHP.
Т.е. шаблонизаторы привносят некоторое дополнительное удобство, предоставляя специализированный функционал для вывода данных и решения типовых задач при выводе. Опять же, PHP за 20 лет синтаксически практически не изменился: до сих пор код находится внутри специальных вставок `<?php` и `?>`, имеет укороченные тэги `<?` и `<?=` для блока кода и вывода результата работы. Важно отметить принципиальную особенность — весь текст вне этих вставок по умолчанию возвращается как есть, как результат работы шаблона (опять вспоминаются препроцессоры).
<%- include('header') -%>
<h1>Title</h1>
<p>My page</p>
<%- include('footer') -%>
Важным был и процесс «засахаривания» синтаксиса — в какой-то момент внезапно оказалось, что кол-во желающих разрабатывать сайты росло несоразмерно с кол-вом специалистов. И в какой-то момент было решено передать работу по написанию шаблонов HTML-верстальщикам, которые уже начали появляться.
Упрощение и «засахаривание» продолжается до сих пор — принцип WYSIWYG продолжает работать и это позволяет сильно снизить порог входа. К слову, EJS, ERB работают по тому же принципу, что и PHP, а Haml и Jade, как явное продолжение истории, еще более «захасарили» синтаксис.
Кроме того, как уже было отмечено выше, шаблоны позволяют полностью отделить логику представления (преобразования в нужный формат) из общей логики приложения. Этот уровень абстракции так же позволяет снижать порог входа в профессию
и дает возможность работать с кодом более широкой аудитории.
HTML
Возвращаясь к HTML, шаблонизация это уже не просто генерация строки с тегами. Средств классического шаблонизатора уже сильно не хватает — часто шаблоны нужны в клиентском приложении, иногда нужны привязки объектов на сервере
к сущностям, представляемым ими на «клиенте». И ситуация усложняется еще и тем, что классический вариант передачи страницы в браузер — это HTML-строка, при сериализации
в которую теряются связи логических сущностей
с DOM-представлением.
Это очень тонкий момент, связанный с природой самого HTML:
в момент генерации шаблонизатором на сервере — это данные, переводимые в строку, когда как
в браузере — это уже строка, переводимая в DOM-узлы. Очень мало шаблонизаторов учитывают эту особенность
и на «клиенте» генерируют HTML так же, как и на сервере, что несет дополнительные накладные расходы при перерисовке интерфейса — ведь зачем нужна строка, если можно напрямую работать с DOM-деревом?
<div ng-repeat="phone in phones | orderBy:orderProp">
<span>{{phone.name}}</span>
<p>{{phone.snippet}}</p>
</div>
Кроме того, у HTML есть еще одна особенность. С одной стороны, HTML — это формат описания документов, входящий
в подмножество (с некоторыми упрощениями) формата хранения и передачи данных XML; с другой стороны, это набор инструкций
для браузера (т.е. программа) описывающая правила отрисовки структуры конечной страницы и формирования DOM-дерева. Таким образом, HTML это и формат для хранения и передачи данных, и, вместе с этим, декларативный язык программирования для браузеров.
Внутренние проблемы шаблонизации
Энтропия
Как водится, жизнь не идеальна, не все шаблонизаторы одинаково полезны, и результат работы может меняться независимо от входных данных. Некоторые шаблонные движки, по простоте душевной и историческим факторам, позволяют использовать данные из глобальной области видимости, догружать файлы в рантайме, и другие, сложные к оптимизации, вещи, которые неявно влияют на результат и осложняют оптимизацию логики. Это допустимо, когда единственный способ передать данные
в шаблон — это окружение (или глобальная область видимости).
Чтобы понять, почему это так прочно засело в современные шаблонизаторы, стоит опять вспоминать препроцессоры
и PHP — предлагаю не останавливаться на этом и лишь заметить, что на «клиенте» произвольная вставка и исполнение других файлов не возможна и, следовательно,
если шаблоны нужны на «клиенте» — преобразование в HTML
не должно использовать такие возможности.
Результат работы идеального сферического шаблонизатора
в вакууме может зависеть только от входного потока данных
и логики самого шаблона (2 переменные функции).
При появлении любой энтропии, функция (в которую превращается шаблон при компиляции) перестает быть чистой
и становится непредсказуемой. Это, в свою очередь, не позволит ни протестировать её, ни закешировать результат при тех же вводных, ни как-то иначе предсказать её результат.
Энтропия влияет и на статический анализ полученной функции
и, соответственно, на оптимизацию её алгоритмов.
Наличие любой энтропии в функции (при чем, как в шаблонах, так и в самом шаблонизаторе, если язык шаблона позволяет привности энтропию) серьезно снижают скорость её работы.
По возможности следует выносить всю энтропию
из шаблонизатора и шаблонов, будь вы разработчик шаблонизатора или автор шаблонов.
Денормализация
Кроме логики самих шаблонов, серьезную роль играют формат
и структура входных данных. Если передавать в функцию неподготовленные данные, то для работы с ними нужно будет
не только получить их, но и структурировать. Это усложнение очень сильно мешает, и часто делает функцию зависимой
от окружения (например, при использовании файловых вставок или получении текущей даты). И это, естественно, несовместимо по определению с чистой функцией и исключает мемоизацию.
Мемоизация (от memoization)— сохранение результатов выполнения функций для предотвращения
повторных вычислений. /Wikipedia
Процесс денормализации часто связывают с ускорением работы чтения из баз данных. Характеризуется он добавлением в структуры избыточных данных, связанных с исходными сущностями.
Это можно сравнить с наличием под рукой нужного справочника в нужное время; представьте как было бы замечательно, если бы в любой момент времени можно было засунуть руку в карман и достать нужный справочник, открытый на нужной странице.
Это же актуально и для данных, использующихся в шаблонах — эти данные должны поступать денормализованными, чтобы все, что используется внутри, было «под рукой» в момент генерации результата. И камнем преткновения встает вопрос формата входных в шаблонизатор данных и его внутренней логики:
- классические препроцессоры на вход получают файл-шаблон
и набор переменных окружения и параметров конфигурации; - классические веб-шаблонизаторы — опять же шаблон,
но к переменным окружения добавляется еще и некоторый набор входных данных в формате базового языка
(суть — одно и то же); - HTML-ориентированные — на вход получают HTML
(или структуру, преобразуемую в HTML) как уже готовую разметку, не учитывает окружение, но все еще используют глобальную область видимости; уже подразумевается, что будет 2 слоя шаблонизации: первый на сервере с разметкой для «клиента», и второй непосредственно на «клиенте» — см. Haml, Jade, Knockout.js, Angular.js, React-Templates, PURE, json2html; - предметно-ориентированные — постепенно возвращаются к заветам XSLT, переосмысливают их, и возвращают некоторую структуру, описывающую сущности интерфейса, а не саму HTML разметку, и в шаблонах имеют слой преобразования — см. React JSX, BH, BEMHTML.
Классический подход совершенно неприемлем для браузерных приложений. Переходный подход, с описанием конечной структуры, тоже не идеален — срок жизни веб-сайта много больше срока устаревания технологий, а это не дает просто и быстро вносить точечные правки в шаблоны без правок JS и CSS (хотя и этого вполне достаточно для сайтов-однодневок). А предметно-ориентированные — предоставляют слой абстракции над HTML структурой в виде дерева сущностей (или объектов) со своей внутренней HTML структурой, которая очень сильно упрощает разработку и поддержку проектов. Ту же самую (или очень близкую) абстракцию предоставляют экспериментальные
Web Components.
Web Components consists of several separate technologies. /MDN
Двухпроходность
Что именно в себя включают сущности и во что они включены — это тоже информация. И если для вывода нужны эти данные, то следует их получить до передачи в функцию, чтобы в последнюю попадало уже сформированное view-ориентированное дерево
с информацией о структуре конечного документа.
Поэтому, очень часто бывает полезно выделить еще один слой преобразований
из нормализованных
(или сырых) данных
в подготовленные к выводу, избыточные структуры — это позволит разделить уровень ответственности шаблонизатора на более конкретные части и вынести из шаблонов всю энтропию.
Процесс денормализация, построение view-ориентированной структуры — более медленный процесс, нежели преобразование в HTML, в т.ч. из-за неизбежной энтропии, учета текущего состояния приложения, получения данных из БД и окружения.
И вторая часть — преобразование view-ориентированных данных
в HTML для браузера, с разметкой необходимых DOM-нод
для последующего использования их в процессе работы
клиентского приложения после десериализации HTML и, в идеале, связывающая таким образом DOM-ноды с представлением о них
в JS-библиотеках (некими JS-объектами, знающими о внутренней DOM-структуре сущностей). См. снова Web Components.
Такой подход позволяет не только более детально прорабатывать алгоритмы оптимизации (и ускорять работу функций), но еще и разделяет уровень отвественности разработчиков, продолжая снижать порог входа, и, параллельно, упрощает процесс шаблонизации на «клиенте».
Компиляция и рантайм
Стоит сказать пару слов о компиляции. В классическом подходе компиляция напрямую не использовалась, но сейчас она уже плотно вошла в жизнь веб-разработчиков вместе
с CSS-препроцессорами и, отчасти, CoffeeScript/TypeScript/ES6-копиляторами. Возвращаясь к шаблонизаторам, компиляция обусловлена возможностью исключить повторный запуск предварительных преобразований; проще говоря, если что-то можно сделать один раз — это делается только единожды. И сама сборка шаблонов в функции (компиляция), исполняемые
на базовом языке и его VM, сейчас очень важный
атрибут шаблонизатора.
Среда выполнения (execution environment или «рантайм») — вычислительное окружение, необходимое для выполнения компьютерной программы и доступное во время её выполнения. /Wikipedia
Ряд шаблонизаторов исполняется в своем окружении, но в большинстве своем они или беднее по возможностям, или медленнее. Но это не играет роли, когда во главу угла встает «безопасность» шаблонизатора, и это совсем другой путь — ограничение функциональности как защита от человеческого фактора. На мой взгляд есть более разумные способы борьбы
с потенциальными ошибками — например, код-ревью.
Итак, чтобы ускорить скомпилированный код нужно выбросить всё лишнее, для этого нужен статический его анализ
с оптимизацией, учитывающей особенности платформы, под которую он собирается. По результатам можно выбросить неиспользуемые куски, избавиться от чрезмерной сложности (cyclomatic complexity), собрать зависимости, подготовить вставки подшаблонов и другое. И, очевидно, что делать это каждый раз в рантайме крайне неразумно. Также бывает полезно делать анализ не только полученного кода (в терминах базового языка), но и исходного (в терминах шаблонизатора).
Цикломатическая сложность программы (Cyclomatic complexity
of a program) — структурная (или топологическая) мера сложности программ, используемая для измерения качества программного обеспечения, основанная на методах статического анализа кода. /Wikipedia
После решения проблем оптимизации кода нельзя забывать и про рантайм — чем он аккуратнее, тем меньше лишнего кода будет запущено при каждой трансформации, и тем быстрее будет работать итоговый код. Размер и скорость работы пустого рантайма (без данных) я бы отметил как отдельную характеристику для оценки скорости шаблонизаторов.
А можно ли вообще без рантайма? Можно, но рантайм предоставляет набор полезных методов, которые, зачастую, оптимизируются при многократном запуске и изначально пишутся максимально оптимально; и если они все равно нужны, то нет смысла избавлятся от рантайма и заставлять разработчиков шаблонов реализовывать эти методы каждый раз самостоятельно.
Условная классификация
Попытаемся отделить зерна от плевел (по мотивам доклада Сергея Бережного).
Задачи
- исходный код → структура — формируют из исходного кода некое AST (Bison, PEG, OMeta и аналоги);
- структура → код — генерируют некий код, в т.ч. HTML;
сюда относится основная масса шаблонизаторов; - структура → структура — генерируют некую структуру,
в т.ч. HTML (e.g. XSLT, XJST, Angular.js, Knockout.js); - исходный код → код — (препроцессоры, m4, CoffeeScript, LESS, SASS, Babel, etc.).
Принцип
- Императивные (PHP, ERB, EJS, препроцессоры) — код выполняется последовательно, обычно в специальных вставках.
- Декларативные (XSLT, PURE) — преобразования описываются декларациями, на вход принимают структуры данных (чаще, деревья).
- Смешанные (XJST, React JSX, BH, BEMHTML, Estraverse) — берут лучшее от обоих подходов: преобразования описываются декларациями, но далее остается возможность использовать выполняемые вставки на базовом языке.
Формат входных данных
- Структурированное дерево (XML, JSON, BEMJSON, AST) —
view-ориентированная структура. - Нормализованные данные (Native, «плоский» XML, JSON) —
сырые или сериализованные данные; чаще всего, результат выполнения *QL запросов, или запросов к API. - Текст (строка) — неподготовленные данные, которые надо
чем-то прочитать и нормализовать, структурировать, в т.ч. код.
Синтаксис
- Текст + вставки (интерполяция) — отличаются тем, что вставки оборачиваются специальными символами (ERB, EJS, Dust.js, Smarty, Mustache, Jinja, TT2).
- Сокращенный синтаксис (Jade, Haml) — обычно, имеют свой синтаксис.
- Предметно-ориентированный синтаксис (XSL, PURE, XJST, BH, Plates, Estraverse).
Базовый язык
- Монолингвальные — часто позволяет делать вставки на базовом языке (EJS, ERB, Smarty).
- Мультилингвальные — чаще всего, не имеют компилятора, имеют свой язык, и под каждую платформу—жирный рантайм (XSL, Mustache, Histone).
Среда выполнения
- Компилируемые и выполняемые в среде VM базового языка (EJS, ERB, XJST) — отличаются отсутствием отдельного рантайма.
- Компилируемые, но требующие рантайм (React JSX, BH, BEMHTML, Smarty).
- Собственная среда выполнения (XSLT, Mustache, Histone).
Идеальный веб-шаблонизатор
Таким образом получается, что идеальный шаблонизатор — понятие очень ситуативное и не может объективно существовать: всегда есть какие-то условия, в которые невозможно вписать некий идеальный шаблонизатор. Но если рассматривать
не «коня в вакууме», а какие-то конкретные случаи, то под эти случаи вполне можно определиться и с характеристиками шаблонизатора:
- компактность и модульность шаблонов — отдельные шаблоны можно и переписать при необходимости;
- возможность частичной отрисовки — «перерисовывать» только нужное, при получении и изменении данных;
- аккуратный и быстрый рантайм — скорость при необходимых возможностях, она не бывает лишней;
- чистота и независимость от внешней среды — для запуска одних и тех же шаблонов на сервере и «клиенте»;
- предметная ориентированность — итоговый HTML может быть тесно связан с JS и CSS, но не с другими шаблонами элементов, см. Web Components;
- скорость при масштабируемости — время преобразования должно расти линейно и стремится к коду на базовом языке;
- «безопасность» и правильность результата — экранирование, XSS, учет HTML-тегов;
- удобство разработки и отладки — процесс разработки не менее важен, чем все остальное — это время разработчика;
- инфраструктура — линтеры, хайлатеры, анализаторы;
- простота синтаксиса — viva la WYSIWYG, но устроит и любой знакомый синтаксис (HTML, JS).
Стоит напомнить, что если вам нужно экономить каждую ms, то пользоваться шаблонизаторами не имеет смысла, или же пользоваться самыми простыми из них, и собирать минимально требуемый код. По возможности, экранировать заранее,
и в целом подготавливать все, что возможно, к выводу.
Из чистоты и предметной ориентированности вытекает еще один пункт: автоматическая связка HTML с рантаймом браузера
и JS-библиотеками, — это умеют делать React JSX, BEMHTML, BH.
Это означает ровно то, что к каждой сущности может быть привязан шаблон, JS-объект, с внутренней логикой,
и набор стилей — прямой отсыл к Web Components. И это же позволяет переиспользовать код несколько раз на странице,
и на других страницах, путем простого переноса.
Проблемы
React страдает сложностью шаблонов (надстройка над JS)
и императивностью, сложно реализована связь между JS и сущностями в рантайме.
BH — умеет генерировать view-ориентированное дерево после применения функции (шаблонов), но не умеет генерировать
DOM-элементы, и не знает про Virtual DOM (хотя можно попытаться это сделать самостоятельно).
BEMHTML — умеет генерировать только строки.
Но и React, и BH, и BEMHTML знают о предметной области очень много, работают с абстракциями над HTML, и недостатки, рано или поздно, будут устранены, поскольку никаких принципиальных проблем там нет. И, исходя из всего вышеизложенного, эти шаблонизаторы — лучшее, что есть сейчас для веб-приложений как на сервере, так и на «клиенте».
Материалы
- Чистые и детерминированные функции, перевод статьи Джастина Этериджа Pure and Deterministic Functions;
- BEMHTML и другие шаблонизаторы, раздел BEMHTML и другие шаблонизаторы в статье на bem.info;
- Шаблонизаторы, лекция Сергея Бережного на ШРИ в Москве;
- Web Components — будущее Web, статья Андрея Саломатина;
- Изоморфный JavaScript — будущее веб-приложений, перевод статьи Спайка Брема The future of web apps is — ready? — isomorphic JavaScript.
Шаблонизаторы
См. также Сравнение синтаксиса шаблонизаторов на примере простой задачи.