JS: Прототипы

Теория: Проект HTML Builder

В этом уроке начинаем проект курса: HTML Builder. Идея простая: мы описываем HTML в виде структуры данных (DSL), а библиотека превращает эту структуру в готовую HTML-строку.

HTML builder structure

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

Порядок работы

В уроке идем в таком порядке:

  1. придумываем DSL
  2. фиксируем ожидаемое поведение тестом
  3. пишем реализацию

Это подход через TDD: сначала формулируем контракт, потом строим код под этот контракт.

Тест задает API

На этом этапе API библиотеки минимальный: одна функция принимает DSL и возвращает HTML.

import assert from 'assert';
import buildHtml from './solution';

const data = /*...*/;
const actual = buildHtml(data);
const expected = `<html><head>
  <title>hello, hexlet!</title></head><body>
  <h1 class="header">html builder example</h1>
  <div><span>span text2</span>
  <span>span text3</span></div></body></html>`;

assert.equal(actual, expected);

Один такой тест уже задает направление всей реализации: какой вход ожидается и какой результат считается правильным.

Как смотреть на HTML в задаче

Если HTML воспринимать только как строку, его неудобно анализировать и менять программно. Если воспринимать как дерево, картина становится проще.

У каждого узла есть:

  • имя тега
  • атрибуты
  • тело (текст)
  • дочерние узлы

В этом уроке фокус на парных тегах. С одиночными тегами есть отдельные нюансы, их разбираем позже.

Базовое представление узла

Обобщенная форма тега в DSL:

// <p class="text-center">text</p>
['p', { 'class': 'text-center' }, 'text'];

// <div><p>text1</p><p>text2</p></div>
['p', {}, '', [<Tag>, <Tag>]];

[<tagName>, <attributes>, <body>, <children>]

Здесь важный момент: children содержит такие же теги. Это рекурсивная структура, а значит она естественно ложится на дерево HTML.

Почему одной формы недостаточно

Формат [tagName, attributes, body, children] удобен для обработки. Но писать такой DSL вручную утомительно: часто у тега нет атрибутов, нет детей или нет текста.

Поэтому поддерживаем короткие формы:

[<tagName>, <attributes>, <body>, <children>]

[<tagName>]

[<tagName>, <attributes>]

[<tagName>, <body>]
[<tagName>, <attributes>, <body>]

[<tagName>, <children>]
[<tagName>, <attributes>, <children>]

Именно тут появляется практическая задача: понять по аргументу, что это - attributes, body или children.

Как различать типы аргументов

Ключевой фрагмент:

['h1', {/*...*/}]; // attributes
['p', '...']; // body
['div', [/*...*/]]; // children

[] instanceof Array; // true
{} instanceof Object; // true
[] instanceof Object; // true

'hello' instanceof String; // false
(new String('hello')) instanceof String; // true
typeof 'hello' === 'string'; // true

Здесь важно не ошибиться с проверками: массив тоже объект, а примитивная строка не проходит instanceof String.

Итоговая структура данных

const data = ['html', [
  ['head', [
    ['title', 'hello, hexlet!'],
  ]],
  ['body', { 'class': 'container' }, [
    ['h1', { 'class': 'header' }, 'html builder'],
    ['div', [
      ['span', 'span text2'],
      ['span', 'span text3'],
    ]],
  ]],
]];

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

Итоги

Мы проектируем не просто формат хранения, а интерфейс между человеком и программой.

DSL должен одновременно:

  • быть удобным для записи
  • быть предсказуемым для парсинга

Баланс между этими требованиями подводит нас к следующему шагу курса - выделению AST и более строгой внутренней модели.

Дальше

Завершено

0 / 10