Это не туториал и не вполне обзор — скорее заметки по горячим следам после собирания библиотеки компонентов. Начиналось всё с обычной обыденной истории: есть легаси-код, к легаси нужно прикрутить пипмочек и финтифлюшек, переписывать ничего нельзя, некогда, и вообще не трогайте тут ничего руками; большие и страшные пакеты тоже на всякий случай не трогайте, да и вообще, почему бы вам просто не взять самый прекрасный фреймворк Vanilla JS и не начать на нём писать, как завещали деды?
Но поскольку объем работ предвиделся очень заметный, то творческих порывов от идеи писать всё в голом JS никто почему-то не испытал. Пошли смотреть на инструменты и выбирать из них такие, которые бы не напугали потребителей наших компонентов до дрожи в коленках.
Хоть я и уже проспойлерил весь этот раздел, кое-что сказать тут нужно. Во-первых, веб-компоненты сразу же стали абсолютно безальтернативными: когда результатом творчества — предполагается библиотека компонентов, и нельзя тащить большого монстра а-ля Angular, который разрулит модульность полностью внутри себя, то остаётся то, что вообще не требует тащить библиотечного кода, и работает «прям так» в современных браузерах. Сами по себе веб-компоненты — это всё тот же удивительный фреймворк Vanilla JS, только с немного решенными вопросами модульности. Руками писать остаётся всё еще слишком много — нужна обертка с шаблонизацией, да еще и желательно, чтоб на каждый компонент было минимум бойлерплейта.
Таковых обёрток, да еще и достаточно зрелых для использования в серьезном проде — не так уж много: Hybrids, LitElement, Stencil. И они все делают примерно одно и то же примерно одним и тем же способом, разница — в малозначимых рюшечках. Hybrids пытается быть модным и безклассовым, Stencil — модным и реактоподобным, LitElement — вроде бы даже особо и не пытается. И по результатам выбирания из малозначимых мелочей на выходе остался LitElement — ООПшники поворотили нос от Hybrids, а нелюбители реакта — от Stencil, в котором JSX и вообще чувствуется повторение не самых бесспорных идей реакта.
Говорить о том, что можно — не так уж интересно, это всё есть в документации, поэтому далее я буду говорить в основном о том, что нельзя: об этом в документации обычно не пишут.
С точки зрения шаблонизации LitElement пользуется lit-html, которая отлично минималистична:
Это не html, но запомнить тут нужно ровно три конструкции в один символ — ".", "?", и "@". Всё остальное — таки html. Отностительно JSX с его className и прочим — это немного приятнее и законно отделено от кода JS/TS. Но, впрочем, отсюда и вытекает первое «нельзя» — биндить что попало куда попало не выйдет, биндить можно только в значения атрибутов и свойств, и в текстовое содержимое. Разумеется, в tagged template literals нет магии, и ценой некоторого количества извращений можно собрать строку в рантайме и в рантайме же к ней приделать биндинги lit-html, но это фактически влезание в рендер руками, и с тем же успехом можно собирать строки и отправлять их в innerHTML. Нормальным же образом это всё делается через композицию шаблонов и разбиение сложных компонентов на более простые составляющие. Бойлерплейта довольно-таки мало — обвязка шаблонов фактически отсутствует, поскольку это просто переменная, а для создания компонента потребуется пять «лишних» строк.
Единственная (но весомая) проблема компонентов — в том, что для полноценного «замешивания» компонентов с рендером детей где-то внутри шаблона родителя нужен Shadow DOM. Который, конечно, включён в LitElement по умолчанию, и вообще вроде бы как неплох. Но теневой DOM в настоящее время имеет одну большую проблему со стилями, аналогично css modules: изолировать стили оно прекрасно изолирует, но попутно это рубит на корню всю каскадность. Влезть в изолированные стили снаружи попросту нельзя. Вообще (почти) никак.
Это сильно мешает, например, возможности накатывания на компоненты разных тем. Всё, что можно делать с теневым DOM — это или упаковать все варианты стилизации внутрь компонента, или пытаться сделать тему полностью зависимой от переменных css — это то самое «почти», с которым таки можно подёргать изолированные стили. Но с которым придётся предусмотреть переменные буквально на любой разумный чих стилизации, и, с большой вероятностью, всё равно потом добавлять еще.
К счастью, Shadow DOM в LitElement можно просто отключить в компоненте. К несчастью, это также отключит и возможность адресно отрендерить детей элемента в нужных местах шаблона через <slot>. К счастью, немного извратившись, можно получить и первое и второе: для этого всего лишь нужно завести теневой корень на каждый необходимый слот, и не держать в нём ничего, кроме, собственно, <slot>. Таким образом и стилизация компонента будет открытой, и слоты будут наличествовать. Я хотел привести кратенький пример, но к сожалению код по манипулированию слотами кратеньким не выходит в любом случае — очень интересующиеся могут почитать вот этот вот issue. Я вдохновлялся идеями именно оттуда.
Ну и еще стоит упомянуть, что в обозримом будущем скорее всего заколосится браузерная поддержка и полифиллы ::part и ::theme, и вот с ними теневой DOM наконец-то станет тем решением, которого все ждут уже много лет — чтоб и изолировано, и расширяемо/изменяемо. Но пока этого всего еще нет.
На этом пункте писать про «что нельзя» уже не выходит, потому что дальше можно всё — библиотеки lit существуют в виде ES modules, и поэтому без каких-либо проблем подхватываются чем угодно и как угодно (хоть голым браузером, который умеет в модули сам). Для IE и нынешнего Edge нужно подключать полифилл веб-компонентов, для модулей, если есть желание поднять их прямо из браузера, нужно что-то избавляющее от боли браузерных импортов, например es-module-shims. Ну или же бандлер.
Подцепленные в приложение веб-компоненты просто и банально доступны к применению, можно начинать использовать их в html и дёргать их методы и свойства в коде. Посмотреть, насколько хорошо это всё может быть прицеплено к другой либе или фреймворку — можно вот здесь (реакт отличился, но в среднем всё очень хорошо). Мы цеплялись в AngularJS, и всё было банально: ng-prop позволяет передать что-нибудь в компонент, а ng-on — слушать события.
Если нужно наваять компонентный UI и прикрутить его к чему-то, во что ты совершенно не хочешь влезать (в легаси код, в немодный страшный фреймворк, и в прочие плохие места) — веб-компоненты выручают просто прекрасно. Основные «зрелые» библиотеки, с ними управляющиеся — мелкие по размеру, серьезных технических проблем модульности и компоновки — уже нет, и можно просто брать и делать. Какую именно библиотеку вы возьмете — даже и не так важно, различий между ними на данный момент очень мало; конкретно LitElement, который взяли мы — не создал нам ни одной дополнительной проблемы, и отработал ожидаемым образом во всех случаях.
Но поскольку объем работ предвиделся очень заметный, то творческих порывов от идеи писать всё в голом JS никто почему-то не испытал. Пошли смотреть на инструменты и выбирать из них такие, которые бы не напугали потребителей наших компонентов до дрожи в коленках.
Инструменты
Хоть я и уже проспойлерил весь этот раздел, кое-что сказать тут нужно. Во-первых, веб-компоненты сразу же стали абсолютно безальтернативными: когда результатом творчества — предполагается библиотека компонентов, и нельзя тащить большого монстра а-ля Angular, который разрулит модульность полностью внутри себя, то остаётся то, что вообще не требует тащить библиотечного кода, и работает «прям так» в современных браузерах. Сами по себе веб-компоненты — это всё тот же удивительный фреймворк Vanilla JS, только с немного решенными вопросами модульности. Руками писать остаётся всё еще слишком много — нужна обертка с шаблонизацией, да еще и желательно, чтоб на каждый компонент было минимум бойлерплейта.
Таковых обёрток, да еще и достаточно зрелых для использования в серьезном проде — не так уж много: Hybrids, LitElement, Stencil. И они все делают примерно одно и то же примерно одним и тем же способом, разница — в малозначимых рюшечках. Hybrids пытается быть модным и безклассовым, Stencil — модным и реактоподобным, LitElement — вроде бы даже особо и не пытается. И по результатам выбирания из малозначимых мелочей на выходе остался LitElement — ООПшники поворотили нос от Hybrids, а нелюбители реакта — от Stencil, в котором JSX и вообще чувствуется повторение не самых бесспорных идей реакта.
В бой?
Говорить о том, что можно — не так уж интересно, это всё есть в документации, поэтому далее я буду говорить в основном о том, что нельзя: об этом в документации обычно не пишут.
Шаблоны
С точки зрения шаблонизации LitElement пользуется lit-html, которая отлично минималистична:
const template = html`
<div
attr=${a}
.property=${b}
?booleanAttr=${c}
@click=${handleClick}
></div>
`;
Это не html, но запомнить тут нужно ровно три конструкции в один символ — ".", "?", и "@". Всё остальное — таки html. Отностительно JSX с его className и прочим — это немного приятнее и законно отделено от кода JS/TS. Но, впрочем, отсюда и вытекает первое «нельзя» — биндить что попало куда попало не выйдет, биндить можно только в значения атрибутов и свойств, и в текстовое содержимое. Разумеется, в tagged template literals нет магии, и ценой некоторого количества извращений можно собрать строку в рантайме и в рантайме же к ней приделать биндинги lit-html, но это фактически влезание в рендер руками, и с тем же успехом можно собирать строки и отправлять их в innerHTML. Нормальным же образом это всё делается через композицию шаблонов и разбиение сложных компонентов на более простые составляющие. Бойлерплейта довольно-таки мало — обвязка шаблонов фактически отсутствует, поскольку это просто переменная, а для создания компонента потребуется пять «лишних» строк.
Компоненты
Единственная (но весомая) проблема компонентов — в том, что для полноценного «замешивания» компонентов с рендером детей где-то внутри шаблона родителя нужен Shadow DOM. Который, конечно, включён в LitElement по умолчанию, и вообще вроде бы как неплох. Но теневой DOM в настоящее время имеет одну большую проблему со стилями, аналогично css modules: изолировать стили оно прекрасно изолирует, но попутно это рубит на корню всю каскадность. Влезть в изолированные стили снаружи попросту нельзя. Вообще (почти) никак.
Это сильно мешает, например, возможности накатывания на компоненты разных тем. Всё, что можно делать с теневым DOM — это или упаковать все варианты стилизации внутрь компонента, или пытаться сделать тему полностью зависимой от переменных css — это то самое «почти», с которым таки можно подёргать изолированные стили. Но с которым придётся предусмотреть переменные буквально на любой разумный чих стилизации, и, с большой вероятностью, всё равно потом добавлять еще.
К счастью, Shadow DOM в LitElement можно просто отключить в компоненте. К несчастью, это также отключит и возможность адресно отрендерить детей элемента в нужных местах шаблона через <slot>. К счастью, немного извратившись, можно получить и первое и второе: для этого всего лишь нужно завести теневой корень на каждый необходимый слот, и не держать в нём ничего, кроме, собственно, <slot>. Таким образом и стилизация компонента будет открытой, и слоты будут наличествовать. Я хотел привести кратенький пример, но к сожалению код по манипулированию слотами кратеньким не выходит в любом случае — очень интересующиеся могут почитать вот этот вот issue. Я вдохновлялся идеями именно оттуда.
Ну и еще стоит упомянуть, что в обозримом будущем скорее всего заколосится браузерная поддержка и полифиллы ::part и ::theme, и вот с ними теневой DOM наконец-то станет тем решением, которого все ждут уже много лет — чтоб и изолировано, и расширяемо/изменяемо. Но пока этого всего еще нет.
Деплой
На этом пункте писать про «что нельзя» уже не выходит, потому что дальше можно всё — библиотеки lit существуют в виде ES modules, и поэтому без каких-либо проблем подхватываются чем угодно и как угодно (хоть голым браузером, который умеет в модули сам). Для IE и нынешнего Edge нужно подключать полифилл веб-компонентов, для модулей, если есть желание поднять их прямо из браузера, нужно что-то избавляющее от боли браузерных импортов, например es-module-shims. Ну или же бандлер.
Подцепленные в приложение веб-компоненты просто и банально доступны к применению, можно начинать использовать их в html и дёргать их методы и свойства в коде. Посмотреть, насколько хорошо это всё может быть прицеплено к другой либе или фреймворку — можно вот здесь (реакт отличился, но в среднем всё очень хорошо). Мы цеплялись в AngularJS, и всё было банально: ng-prop позволяет передать что-нибудь в компонент, а ng-on — слушать события.
Итог
Если нужно наваять компонентный UI и прикрутить его к чему-то, во что ты совершенно не хочешь влезать (в легаси код, в немодный страшный фреймворк, и в прочие плохие места) — веб-компоненты выручают просто прекрасно. Основные «зрелые» библиотеки, с ними управляющиеся — мелкие по размеру, серьезных технических проблем модульности и компоновки — уже нет, и можно просто брать и делать. Какую именно библиотеку вы возьмете — даже и не так важно, различий между ними на данный момент очень мало; конкретно LitElement, который взяли мы — не создал нам ни одной дополнительной проблемы, и отработал ожидаемым образом во всех случаях.