Как и зачем создавать собственный шаблонизатор для DOM

Фронтенд-разработчик в Miro рассказал, что такое шаблонизация, и показал один из возможных подходов к динамическому формированию разметки.

9К открытий10К показов

Посмотрим в код

Прежде чем начинать изучать шаблонизацию, давайте вспомним два базовых подхода к созданию разметки.

Перед нами два отрывка кода, которые отвечают за одинаковую разметку для списка чатов: декларативный и императивный. Так это выглядит в HTML:

			<div class="chat__wrapper">
  <div class="chat__button">
    <button class="button">
      <span>Добавить чат</span>
    </button>
  </div>

  <ul class="chat__list">
    <li class="chat__item">Название чата 1</li>
    <li class="chat__item">Название чата 2</li>
    <li class="chat__item">Название чата 3</li>
  </ul>
</div>
		

Теперь создаём такое же дерево, но через DOM API в JavaScript:

			const CHAT_NAMES = [
  'Название чата 1',
  'Название чата 2',
  'Название чата 3',
];
function renderChat() {
  const ul = document.createElement('ul');
  ul.className = 'chat__list';

  CHAT_NAMES.forEach(item => {
    const li = document.createElement('li');
    li.textContent = item;
    li.className = 'chat__item';
    ul.appendChild(li);
  });

  return ul;
}

function renderButton() {
  const button = document.createElement('button');
  button.className = 'button';

  const text = document.createElement('span');
  text.textContent = 'Добавить чат';
  button.appendChild(text);
		
  const wrapper = document.createElement('div');
  wrapper.className = 'chat__button';
		
  wrapper.appendChild(button);
  return wrapper;
}

const wrapper = document.createElement('div');
wrapper.className = 'chat__wrapper';

wrapper.appendChild(renderButton());
wrapper.appendChild(renderChat());

document.body.appendChild(wrapper);
		

HTML выглядит понятнее, чем множество однотипных вызовов DOM API. Однако есть и минус: HTML статичен — его прописывают один раз и пользуются, а если нужно описать десятки или сотни однотипных элементов на странице, придётся написать всю разметку.

Есть огромный соблазн отдать эту рутинную работу коду. Или, например, когда десятки или сотни элементов приходят с сервера в процессе работы приложения, нужно создавать dom-элементы динамически, наполняя их данными, как в примере кода выше.

Где-то на стыке этих двух подходов работы с DOM появляется шаблонизация. С одной стороны, она позволяет описать разметку декларативно, а с другой стороны — описать динамические данные прямо в этой разметке.

Представьте, если бы вы могли создать DOM-дерево с динамическими данными, не используя createElement и описание свойств, а например, вот так:

			<div class="{{ wrapperClassName }}"> 
  <div class="chat__button"> 
    <button class="button"> 
      <span>{{ buttonText }}</span> 
    </button> 
  </div> 

  <ul class="{{ chatListClassName }}"> 
    {{ chatListItems }} 
  </ul> 
</div>
		

Такая запись читается проще, чем манипуляции с document.createElement. Здесь можно:

  • ясно увидеть, где должны быть имена классов и в каких местах они динамические;
  • представить, как выглядит DOM-дерево сразу, не прикидывая в голове цепочку append (добавлений) элементов друг в друга;
  • рендерить куски вёрстки быстро и наглядно.

К подобному синтаксису шаблона мы и будем стремиться.

Зачем это всё?

Действительно, зачем в век React, Angular и Vue заниматься такой мелочью, как шаблонизация? Все современные фреймворки умеют динамически подставлять данные в DOM и даже больше: циклы, условные операторы, компоненты, жизненный цикл.

В конце концов, если не пользоваться фреймворками — есть уже готовые шаблонизаторы, можно же взять их?

Сколько бы ни было на рынке инструментов, какие бы возможности они ни предоставляли, фактически всё сводится к 26 буквам английского алфавита, десятку символов и идеям: паттернам программирования и проектирования, принципам построения чистой архитектуры и чистого кода, шаблонам решения типовых задач.

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

Именно поэтому написана эта статья — чтобы показать один из возможных подходов к динамическому формированию разметки.

Идея и синтаксис шаблона

Шаблонизатор — это функция, которая на вход получает некоторую разметку в виде чистого текста и контекст, а на выход даёт готовый DOM-элемент или текст, готовый к использованию в innerHTML. Под контекстом подразумевается набор информации, который подставляется вместо переменных в шаблонной строке.

Чтобы шаблонизатор мог работать, нужно придумать соглашение о синтаксисе — как описать в шаблоне места, куда должны подставляться данные из контекста. Обычно используют какие-нибудь скобки, в которые помещают идентификаторы переменных.

			<div class="chat {{wrapperClassName}}">
<!-- или -->
<div class="chat [[wrapperClassName]]">
<!-- или -->
<div class="chat ((wrapperClassName))">
		

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

			<div class="chat"> 
  Всего активно(на данный момент) (chatCount) чатов 
</div>
		

Если выбрать круглые скобки в качестве маркера динамических данных и не удвоить их, непонятно, где переменная, а где просто текст в скобках.

Примем за обозначение переменных в шаблоне удвоенные фигурные скобки — такое решение достаточно распространённое. Таким образом шаблон для примера выше будет таким:

			<div class="chat"> 
  Всего активно(на данный момент) {{chatCount}} чатов 
</div>
		

Хранение шаблона

Так как шаблон — это строка, то его можно хранить просто в строке ?

			const template = ` 
<div class="chat"> 
  Всего активно(на данный момент) {{chatCount}} чатов 
</div> 
`;
		

Если пойти дальше и развивать идею, окажется, что необязательно хранить строку с шаблоном именно в файле с кодом. Хранение шаблона превращается в отдельную задачу в духе «получить откуда-то шаблон», например, из тега