[Перевод] Практический пример использования render-функций Vue: создание типографской сетки для дизайн-системы

В материале, перевод которого мы сегодня публикуем, речь пойдёт о том, как создать типографскую сетку для дизайн-системы с использованием render-функций Vue. Вот демонстрационная версия проекта, который мы будем здесь рассматривать. Здесь можно найти его код. Автор этого материала говорит, что использовал render-функции из-за того, что они позволяют гораздо точнее контролировать процесс создания HTML-кода, чем обычные шаблоны Vue. Однако, к своему удивлению, он не смог найти практических примеров их применения. Ему попадались лишь учебные руководства. Он надеется на то, что этот материал изменит ситуацию в лучшую сторону благодаря тому, что здесь приводится практический пример использования render-функций Vue.

0fp7feua0qplioin-gmhhhdzhj4.png


Render-функции Vue


Render-функции всегда казались мне чем-то таким, что немного несвойственно Vue. Всё в этом фреймворке подчёркивает стремление к простоте и к разделению обязанностей различных сущностей. А вот render-функции представляют собой странную смесь из HTML и JavaScript, которую часто сложно бывает читать.

Например, вот HTML-разметка:

  

Some cool text


Для её формирования нужна следующая функция:

render(createElement) {
  return createElement("div", { class: "container" }, [
    createElement("p", { class: "my-awesome-class" }, "Some cool text")
  ])
}


Подозреваю, что такие конструкции заставят многих сразу же отвернуться от render-функций. Ведь простота использования — это именно то, что привлекает разработчиков в Vue. Жаль, если за неприглядной внешностью render-функций многие не увидят их истинных достоинств. Всё дело в том, что render-функции и функциональные компоненты — это интересные и мощные инструменты. Я, для того, чтобы продемонстрировать их возможности и их истинную ценность, расскажу о том, как они помогли мне решить реальную задачу.

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

Определение критериев для дизайн-системы


У нас имеется дизайн-система, основанная на VuePress. Нам понадобилось включить в неё новую страницу, демонстрирующую различные типографские возможности оформления текстов. Вот как выглядел макет, который дал мне дизайнер.

87582598175ae5ed7c7c59464d2511d5.png


Макет страницы

А вот пример соответствующего этой странице CSS-кода:

h1, h2, h3, h4, h5, h6 {
  font-family: "balboa", sans-serif;
  font-weight: 300;
  margin: 0;
}

h4 {
  font-size: calc(1rem - 2px);
}

.body-text {
  font-family: "proxima-nova", sans-serif;
}

.body-text--lg {
  font-size: calc(1rem + 4px);
}

.body-text--md {
  font-size: 1rem;
}

.body-text--bold {
  font-weight: 700;
}

.body-text--semibold {
  font-weight: 600;
}


Заголовки форматируются на основе имён тегов. Для форматирования других элементов используются имена классов. Кроме того, тут предусмотрены отдельные классы для насыщенности и размеров шрифтов.

Я, прежде чем приступать к написанию кода, сформулировал некоторые правила:

  • Так как основная цель этой страницы — визуализация данных — данные должны храниться в отдельном файле.
  • Для форматирования заголовков должны использоваться семантические теги заголовков (то есть —

    ,

    и так далее), их форматирование не должно быть основано на классе.

  • В теле страницы должны использоваться теги абзацев (

    ) с именами классов (например —

    ).

  • Материалы, состоящие из различных элементов, должны быть сгруппированы путём оборачивая их в корневой тег

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

      Thing 1   Thing 2

  • Материалы, при выводе которых особых требований не предъявляется, должны быть обёрнуты в тег

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

      Thing 1   Thing 2

  • Для оформления каждой стилизуемой ячейки имена классов должны записываться лишь один раз.


Варианты решения задачи


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

▍Ручное написание HTML-кода


Мне нравится писать HTML-код вручную, но только тогда, когда это позволяет адекватным образом решить имеющуюся задачу. Однако в моём случае ручное написание кода означало бы ввод различных повторяющихся фрагментов кода, в которых присутствуют некоторые вариации. Мне это не понравилось. Кроме того, это означало бы, что данные нельзя будет хранить в отдельном файле. В итоге от такого подхода я отказался.

Если бы я создавал страницу, о которой идёт речь, именно так, то получилось бы у меня примерно следующее:

  

Heading 1

  

h1

  

Balboa Light, 30px

  

    Product title (once on a page)     Illustration headline   


▍Использование традиционных шаблонов Vue


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

fae19793792f458183103178698df791.png


Пример использования шаблонов Vue

В первой колонке тут имеется следующее:

  • Тег

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

  • Тег

    , группирующий несколько дочерних элементов с текстом. Каждому из этих элементов назначен класс (но самому тегу

    особого класса не назначено).

  • Тег

    , не имеющий вложенных элементов , которому назначен класс.


Для реализации всего этого понадобилось бы много экземпляров директив v-if и v-if-else. А это, я знаю, привело бы к тому, что код очень скоро стал бы весьма запутанным. Кроме того, мне не нравится использование в разметке всей этой условной логики.

▍Render-функции


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

Модель данных


Как я уже говорил, мне хотелось хранить типографские данные в отдельном JSON-файле. Это позволило бы, при необходимости, вносить в них изменения, не прикасаясь к разметке. Вот эти данные.

Каждый JSON-объект в файле представляет собой описание отдельной строки:

{
  "text": "Heading 1",
  "element": "h1", // Корневой элемент.
  "properties": "Balboa Light, 30px", // Третий столбец текста.
  "usage": ["Product title (once on a page)", "Illustration headline"] // Четвёртый столбец текста. Каждый элемент - это дочерний узел.
}


Вот HTML-код, который получается после обработки этого объекта:

  

Heading 1

  

h1

  

Balboa Light, 30px

  

    Product title (once on a page)     Illustration headline   


Теперь рассмотрим более сложный пример. Массивы представляют группы дочерних элементов. Свойства объектов classes, которые сами являются объектами, могут хранить описания классов. Свойство base объекта classes содержит описание классов, общих для всех узлов в ячейке. Каждый класс, присутствующий в свойстве variants, применяется к отдельному элементу в группе.

{
  "text": "Body Text - Large",
  "element": "p",
  "classes": {
    "base": "body-text body-text--lg", // Применяется к каждому дочернему узлу.
    "variants": ["body-text--bold", "body-text--regular"] //Этот массив обходят в цикле, один класс применяется к одному из примеров. Каждый элемент в массиве представляет собой отдельный узел. 
  },
  "properties": "Proxima Nova Bold and Regular, 20px",
  "usage": ["Large button title", "Form label", "Large modal text"]
}


Этот объект превращается в следующий HTML-код:

     

    Body Text - Large     Body Text - Large   

     

    body-text body-text--lg body-text--bold     body-text body-text--lg body-text--regular   

     

Proxima Nova Bold and Regular, 20px

     

    Large button title     Form label     Large modal text   


Базовая структура проекта


У нас имеется родительский компонент TypographyTable.vue, который содержит разметку для формирования таблицы. Также у нас есть дочерний компонент, TypographyRow.vue, который ответственен за создание строки таблицы и содержит нашу render-функцию.

При формировании строк таблицы выполняется обход массива с данными. Объекты, описывающие строки таблицы, передаются компоненту TypographyRow в качестве свойств.



Тут хотелось бы отметить одну приятную мелочь: типографские данные в экземпляре Vue могут быть представлены в виде свойства. Обращаться к ним можно с помощью конструкции $options.typographyData так как они не меняются и не должны быть реактивными (благодарю Антона Косых).

Создание функционального компонента


Компонент TypographyRow, который обрабатывает данные, представляет собой функциональный компонент. Функциональные компоненты — это сущности, не имеющие состояний и экземпляров. Это означает, что у них нет this, и то, что у них нет доступа к методам жизненного цикла компонентов Vue.

Вот «скелет» подобного компонента, с которого мы начнём работу над нашим компонентом:

// Нет