Я уверен, что среди уважаемой аудитории найдутся те, кто меня поймет. Дело в том, что во всем изобилии популярных библиотек и фреймворков для веб-фронтэнда, лично мне, не нравятся, практически, все альтернативы. В каждом из вариантов я нахожу для себя существенные минусы, которые не дают мне спокойно ими пользоваться. Начинается всегда все радужно: интересный концепт, здравые, на первый взгляд, рассуждения в пунктах «за»… Но затем, все, снова и снова, начинает упираться в избыточные зависимости, лишние сущности и попытки решить проблемы, которые разработчики сами же и создали. Нам предлагают выучить новый синтаксис, принять новые идеи, узнать кучу умных слов, установить множество «необходимых» пакетов. Ок, я люблю все новое, и люблю умные слова. Но меня сильно обескураживает, когда то, что можно сделать просто, люди начинают фрактально усложнять. Вы наверняка уже читали исповеди тех, кто также как и я, отчаялся искать во всем этом здравый смысл и решил уйти в другую крайность — все писать «на ваниле». Со мной это случилось, когда я разочаровался в проекте Polymer, создаваемом при активном участии разработчиков из Google. Сначала мне все очень нравилось, девизом этого движения была фраза «Use the platform!», что для меня означало: «не стоит делать в коде того, что браузер сам сделает эффективнее». Команда Polymer сделала очень многое для внедрения новых стандартов и возможностей на уровне платформы, и за это им — огромное спасибо. Но когда эти цели были достигнуты, они сами принялись нарушать свои-же принципы. И вот их новая библиотека (LitElement) уже отказывается работать напрямую в браузере без установки специального окружения, потому как ребята не следуют стандартам… Я продолжаю наблюдать за развитием LitElement, и даже вижу явные признаки возвращения этих заблудших на путь истинный, но сам я этим уже пользоваться не буду, потому, что теперь у меня есть кое-что получше.

Что?


Holiday.js — библиотека для тех, кому нужно все необходимое и не нужно ничего лишнего. Holiday — потому, что работа с ней для меня — это праздник. А также потому, что писал я ее, преимущественно, по праздникам или будучи в отпуске. Тем не менее, это вполне серьезный проект, с помощью которого разрабатывается целый ряд сложных приложений для того самого «кровавого энтерпрайза», которым любят пугать новичков велосипедостроения. Все свои персональные проекты я также пишу на Holiday.js.

Основано все на группе стандартов Custom Elements и Shadow DOM, как вы уже могли догадаться, и если вы уже знакомы с ними, вам будет очень легко во всем разобраться.

Немного о себе
Мне 40 лет. В данный момент я являюсь ведущим веб-разработчиком и R&D инженером в одной голландской компании, специализирующейся на интеграции решении от IBM. Наши клиенты — крупные компании: нефть, газ, танкеры, железные дороги — вот это все. Суть моей работы — расширение стандартных возможностей для больших и неповоротливых корпоративных систем и улучшение пользовательского опыта, при работе с ними. С одной стороны у меня дяденьки в галстуках, с другой — свежий ветер свободы современной веб разработки. Это интересное сочетание, оно позволяет на многие вещи взглянуть под новым углом. И мне это нравится. Ранее я примерял на себя роль стартапера, ux-инженера, арт-директора небольшой дизайн-студии, и еще много кого, поэтому мой общий «экспириенс» похож на лоскутное одеяло. Но это не значит, что я отношусь ко всему поверхностно, просто я во многих видах деятельности нахожу схожие инженерные принципы, которые и интересуют меня в первую очередь.

В чем смысл?


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

import {HdElement} from '../../holiday/core/hd-element.js';

class MyComponent extends HdElement {
  constructor() {
    super();
    this.state = {
      imageURL: 'images/photo.jpg',
      firstName: 'John',
      secondName: 'Snow',
    };
  }
}

MyComponent.template = /*html*/ `
<style>
  :host {
    display: block;
    padding: 10px;
    background - color: #fff;
    color: #000;
  }
</style>
<img bind="src: imageURL" />
<div bind="textContent: firstName"></div>
<div bind="textContent: secondName"></div>
`;

MyComponent.is = 'my-component';

Здесь мы видим, что у компонента есть некое состояние и шаблон со странными атрибутами элементов "bind". Вот эти атрибуты — и есть основная магия: они позволяют распарсить шаблон и сохранить копию результата в памяти для дальнейшего использования (клонирования) БЕЗ дополнительной обработки строки шаблона в JS рантайме, что дает высокую производительность при первичной отрисовке, когда создаются экземпляры каждого компонента. В отличие от многих современных библиотек, в которых участок DOM, за который отвечает компонент, определяется функцией от состояния, в Holiday.js шаблон — это полуфабрикат, который определяет начальную структуру и дает возможность привязать данные уже позже, с помощью обычного DOM API, и это работает очень быстро. К вопросу скорости мы еще вернемся.

Перечислю и другие преимущества:

  • Максимальная приближенность к нативным API, минимум дополнительных абстракций
  • Нативный синтаксис шаблонов: вам не нужно учить новые HTML и CSS
  • Для начала работы, вам не нужно ничего дополнительно устанавливать, в вашем проекте вообще может не быть папки node_modules
  • Легкость: вы не будете заставлять пользователей долго ждать загрузки
  • В комплект входит глобальный стейт-менеджмент, роутинг, UI-библиотека: вы не почувствуете себя в пустыне

Основная документация проекта: https://holiday-js.web.app/

Жизненный цикл


Давайте рассмотрим этапы жизненного цикла компонента:

import {HdElement} from '../../holiday/core/hd-element.js';

class LifecycleExample extends HdElement {

  constructor() {
    super();

    // Определение начального состояния:
    this.state = {
      name: '',
      bigName: '',
    };

    /*
В некоторых случаях, компонент может быт добавлен в DOM как "неизвестный элемент по умолчанию", и конструктор компонента может быть вызван позже. Чтобы не потерять данные, сеттеры лучше определять через метод "defineAccessor":
    */
    this.defineAccessor('name', (name) => {
      // Some accessor code:
      this.setStateProperty('name', name);
    });

    /*
На этапе работы конструктора, компонент еще не добавлен в DOM и вы можете модифицировать его содержимое без опасности замедлить работу за счет перерисовок в браузере.
    */
  }

  connectedCallback() {
    super.connectedCallback();
    /*
Нативный коллбэк.
Элемент добавлен в DOM.
На этом этапе вы можете получить доступ к потомкам элемента или к значению его атрибутов.
    */
  }

  stateUpdated(path) {
    /*
Данный метод может быть использован для вычисляемых свойств:
    */
    if (path === 'name') {
      this.setStateProperty('bigName', this.state.name.toUpperCase());
    }
  }

  attributeChangedCallback(attributeName, oldValue, newValue) {
    /*
Нативный коллбэк.
Вызов происходит при изменении значений атрибутов.
    */
    super.attributeChangedCallback(attributeName, oldValue, newValue);
  }

  disconnectedCallback() {
    /*
Нативный коллбэк.
Элемент удален из DOM. На данном этапе можно удалить подписки на события или данные для очистки памяти
    */
  }

}

LifecycleExample.template = /*html*/ `
  <div bind="textContent: name"></div>
  <div bind="textContent: bigName"></div>
`;

/*
Список HTML-атрибутов, на которые компонент будет реагировать:
*/
LifecycleExample.logicAttributes = [
  // Значения этих атрибутов, будут автоматически присвоены свойствам компонента с аналогичными именами
  'name',
];

/*
Регистрация компонента в реестре CustomElements:
*/
LifecycleExample.is = 'lifecycle-example';

Шаблоны


Поскольку шаблоны в Holiday.js — это просто стандартные шаблонные литералы, вы имеете полную свободу в том, как размещать их в проекте и как формировать их содержимое. Можно их выносить в отдельные модули, можно определять вместе с кодом компонента, а можно генерировать налету из данных. В шаблонах можно использовать выражения и результаты выполнения функций. Все это дает большие возможности и свободу самовыражения. Например, вы можете импортировать в шаблон общие стили, использовать переменные и вычисление значений в CSS без использования препроцессоров и с большим удобством, собирать шаблоны из готовых кусков.

Важной частью шаблонов веб-компонентов являются "слоты". Слоты позволяют обозначить специальные места в «теневом» DOM вашего компонента, в которые будут выводится его прямые потомки в общем DOM-дереве. Пример:

<header>
  <slot name="header"></slot>
</header>

<div class="toolbar">
  <slot name="toolbar"></slot>
</div>

<div class="content">
 <slot></slot>
</div>

<footer>
  <slot name="footer"></slot>
</footer>

Размещение в слотах:

<my-component>
  <div slot="header">Header content</div>
  <toolbar-component slot="toolbar"></toolbar-component>
  <div>Content of the default slot...</div>
  <div slot="footer">Footer content...</div>
</my-component>

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

Как уже упоминалось выше, для динамической привязки данных и обработчиков, используется атрибут «bind»:

<div bind="textContent: content; onclick: on.clicked"></div>

Для привязки значений атрибутов используется символ "@" в начале наименования свойства:

<div bind="@style: style">Content...</div>

Больше подробностей вы сможете найти в соответствующем разделе документации: https://holiday-js.web.app/?templates

Управление состоянием


Внутри компонента:

class MyComponent extends HdElement {

  constructor() {
    super();

    // Определение структуры и начальных значений для объекта состояния:
    this.state = {
      content: 'Initial Content',
      style: 'color: #f00',
      on: {
        clicked: () => {
          console.log('Clicked!');
        },
      },
    };

    // Обновление данных происходит с помощью метода "setStateProperty":
    window.setTimeout(() => {
      // Обновление одного свойства:
      this.setStateProperty('content', 'Updated Content');

      // Обновление нескольких свойств:
      this.setStateProperty({
        'content': 'Updated Content',
        'style': 'color: #00f',
      });
    }, 1000);
  }

}

Для управления состоянием на уровне всего приложения, используется модуль HdState.
Пример использования:

import {HdState} from '../../holiday/core/hd-state.js';

// Подписка на значение:
HdState.subscribe('ui.loading', (val) => {
  console.log(val);
});

// Публикация нового значения:
HdState.publish('ui.loading', true);

// Либо:
HdState.publish({
  'ui.loading': true,
  'ui.sidePanelActive': true,
});

Для работы с глобальным состоянием, сперва необходимо определить его общую схему:

HdState.applyScheme({
  ui: {
    loading: {
      /*
При записи значений осуществляется контроль типов, поэтому для каждого поля необходимо указать тип:
      */
      type: Boolean,
      // И значение по умолчанию:
      value: false,
    },
    sidePanelActive: {
      type: Boolean,
      value: false,
    },
  },
  user: {
    authorized: {
      type: Boolean,
      value: false,
    },
    name: {
      type: String,
      value: null,
      // Значение поля может быть сохранено и восстановлено после перезагрузки страницы:
      cache: true,
    },
  },
});

Схема также может быть удобно разделена на отдельные логические блоки.

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

let subscription = HdState.subscribe('ui.loading', (val) => {
  console.log(val);
});

window.setTimeout(() => {
  // Unsubscribe:
  subscription.remove();
}, 10000);

Документация по работе со стейтом: https://holiday-js.web.app/?state

Инструменты разработки


Самый минимальный набор для начала работы — текстовый редактор и браузер. Да, этого достаточно, чтобы попробовать. Стоит оговориться, что в такой конфигурации, браузером должен быть именно Firefox. Он умеет загружать ES-модули без использования локального сервера.

Для более полноценной работы, вам понадобится git и любой приглянувшийся вам веб-сервер (для работы с ES-модулями в других браузерах).

Для подсветки HTML-синтаксиса и автодополнений в шаблонных литералах, я использую расширение для VS Code, изначально предназначенное для работы с lit-html: https://github.com/pushqrdx/vscode-inline-html

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

Производительность


Бенчмарки для фреймфорков писать непросто. Для репрезентативного сравнения, вы должны убедится, что сравниваете сравнимые вещи и, насколько это возможно, минимизируете влияние сайд-эффектов. А значит, должны иметь четкое представление от том, что происходит «под капотом» в каждом случае. Сперва, нужно определиться с тем, какие именно параметры мы будем измерять. Для нашего случая, нам важны:

  1. Время до начала исполнения основной программы (загрузка)
  2. Время первичной отрисовки компонентов
  3. Время обновления данных компонентов и их перерисовки

К сожалению, у нас нет надежного способа узнать момент, когда браузер выполнил программу и полностью закончил рендер. На уровне браузера существует множество собственных оптимизаций, которые для нас являются чем-то типа «черного ящика» и могут отличаться в разных движках. Что мы можем использовать: Performance API, requestAnimationFrame и нестандартный метод requestIdleCallback, который работает только в Chrome и Firefox. На первое время хватит.

С чем будем сравнивать Holiday.js? Я начал с двух библиотек: React (куда же без него) и LitElement (похожие технологии в основе). Код бенчмарков вы можете найти в этих репозиториях:

https://github.com/foxeyes/holiday-benchmark
https://github.com/foxeyes/lit-benchmark
https://github.com/foxeyes/react-benchmark

К сожалению, с автоматизацией тестов я пока не закончил, а это тоже непросто, потому как автоматизация не должна сама по себе создавать сайд-эффектов. В данный момент я продолжаю работу над этим вопросом и готовлю соответствующий инструмент с использованием Puppeteer (очень полезная штука, рекомендую). А пока, вы можете попробовать тесты в ручном режиме (Chrome/Firefox) и проверить результаты в консоли браузера. Вы увидите, что Holiday.js уверенно лидирует во всех случаях.

Вы можете заметить, что в случае с React, при выводе компонентов создается дополнительный div-контейнер, который служит для того, чтобы уровнять количество создаваемых DOM-элементов. Я решил что, так будет справедливо, потому что веб-компонент — это всегда DOM-элемент и некий контейнер, и соответствие структур DOM-дерева в разных тестах очень важно. К тому-же избавиться от лишней вложенности возможно и в Holiday.js и вывести данные так-же, как это происходит в React. Однако, если данный вопрос кажется вам спорным, обратите внимание, в первую очередь, на скорость обновлений.

Поддержка браузерами


Нет, Internet Explorer не поддерживается. Этого возможно было бы добиться с определенными усилиями (для 11-й версии), но отказ от поддержки — принципиальная позиция. Во первых, я считаю, что IE давно должен умереть, и в наших с вами силах этому поспособствовать. Во вторых, люди, которые выступают за поддержку IE, часто допускают серьезную логическую ошибку: доля IE в общем трафике, по актуальным данным caniuse ~ 1.6% для всех встречающихся версий. А самый популярный браузер, идущий с большим отрывом впереди всех остальных — это мобильный Chrome (~35%). Так вот, поддерживая IE, вы сознательно отказываетесь от использования большого количества технологий, которые просто необходимы для реализации полноценной адаптивности и мобильности в ваших приложениях, то есть, ради незначительного меньшинства, усложняете жизнь значительному большинству своих пользователей. Или существенно удорожаете разработку и поддержку своих продуктов. В третьих, Microsoft, в последнее время, активно принуждает пользователей отказаться от Windows 7, так что доля IE трафика в ближайшее время будет все быстрее снижаться. Признаю, что существуют случаи, когда поддержку IE никак не обойти, но такие случаи гораздо более редки, чем мы привыкли думать.

15 января этого года, Microsoft выпустили релизную версию нового Edge, на движке Chromium. C этого момента можно считать, что Holiday.js нативно поддерживается во всех актуальных версиях популярных браузеров. Для более старых Edge — потребуется полифилл. Возможность работы с полифиллами реализована в Holiday.js, но сам скрипт нужно подключать отдельно.

Минимализм


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

Планы


Если вы дочитали до этого места, значит мои усилия уже потрачены не зря. Даже если вы не захотите использовать Holiday.js, вы можете, как минимум, почерпнуть что-то интересное для себя в работе с веб-компонентами как таковыми. Я буду очень рад, если данный материал положит начало формированию сообщества. Буду очень рад конструктивной дискуссии, критике, багрепортам, пуллреквестам и, конечно, звездочкам на GitHub. Конечно, я не смог затронуть все аспекты, и с удовольствием отвечу на ваши вопросы. Прошу принять во внимание, что до сего момента, я работал над этой библиотекой один, а делать все в одиночку, включая документацию, тесты и сопутствующие инструменты, параллельно с основной работой и пет-проектами — сложно.

Тем не менее, в ближайших планах:

  • Больше сравнений с другими популярными библиотеками и фреймворками
  • Минималистичный и гибкий инструмент для тестирования «PingPong» на основе Puppeteer
  • Генератор базового проекта с примерами (Starter Kit)
  • Дальнейшее развитие документации

Скрещиваю пальцы, жму «Опубликовать»…