Как подружить веб-компоненты и JS-фреймворки

653248c5d343e6d75cb3658bbf914a1e.jpg

Всем привет, я Роман Троицкий. Очень люблю веб-разработку; участвовал в проектах, попавших на Awwwards, Tagline и GoldenSite; помогаю организовывать митап Moscow CSS; участвовал в записи и разработке курса по фронтенду для Skillbox. На примере своего проекта я расскажу о сложившейся с Web Components ситуации, опишу их достоинства и недостатки. 

Зачем нужны веб-компоненты?

У крупных технокомпаний, как правило, есть множество ИТ-продуктов. Разрабатывают их, обычно, не одни и те же люди, целые группы команд. При этом фронтенд во всех этих продуктах обычно пишут на одних и тех же технологиях: берут готовый шаблон, что-нибудь обновляют — и готово. А что делать, если все эти продукты написаны на разных фреймворках? Конечно, можно сделать десяток UI-наборов на все случаи жизни, но будет крайне дорого их писать и поддерживать. А ещё придётся потратить колоссальное количество времени — конкуренты обгонят, сотрудники выгорят. Переписывать на один и тот же стек нецелесообразно по тем же причинам. 

Какие есть варианты?  

Взять готовый headless UI-набор с логикой компонентов, поверх которой можно добавить какой-то адаптер к фреймворку и корпоративный дизайн. Самый известный представитель такого подхода — это Tan Stack, известный также как React Query и Vue Query. 

Использовать мета-фреймворки для создания UI-наборов. Скажем, Mitosis позволяет собрать на JSX-компоненты, сделать что-то вроде AST и сгенерировать наборы компонентов с обвязками под нужные нам фреймворки. К примеру, есть у нас простой компонент с состоянием:

53ceb4d82690f23ff441d6954975a946.png

Сначала он превратится в JSON, тоже с состоянием:

198d3472b85cf994c101a0c19c4e35fb.png

Разметка превратится в массив nodes. Сначала идёт родительский div, в нём вложенный input. Дальше мы видим биндинги, onChange, привязку слушателей, значение состояния. И из этого кода средствами Mitosis мы можем десериализовать обратно в компонент, например, для Angular. 

46f286ea8aa5dd00b3e5ce0367787be2.png

Но ещё более интересными и перспективными показались веб-компоненты. 

Web Components

Эта технология вышла на рынок почти 15 лет назад, однако широкого распространения до сих пор не получила. Кто-то внедрял куда-то или читал новости спецификаций, выступал на конференции с докладом, затем опять тишина, и спустя пару лет снова один-два спикера заикаются, снова тишина. Но пару лет назад я решился использовать эту технологию в эксплуатации. Это было не просто смело, это было очень смело. Но всё оказалось не так страшно. Коротко расскажу, что это такое.

Веб-компоненты — это набор Web-API для создания собственных HTML-тегов, со своей логикой, дизайном, отображением и так далее. Например:

393138b215cb878381b6442f6a5d1676.png

Для работы этого элемента нужно создать класс:  

45a8a1e7eff30beaabf2b1b0c5314747.png

Сохраним внутренности шаблона в переменную. В шаблоне опишем разметку нашего компонента, и дальше с ним можем работать с помощью DOM API — клонировать, применять, добавлять стили и так далее. 

267fb9ab0b57df8c814b39a03d78f4f1.png

Чтобы компонент был максимально независимым от стилей, мы можем его инкапсулировать с помощью метода attachShadow. Это можно сделать с помощью атрибутов тега template, в котором мы пишем разметку. Инкапсулировать можно и какую-нибудь логику. Модифицировать стили в таком случае мы можем только через sys-переменные, потому что Shadow DOM позволяет нам эти стили абстрагировать от содержимого родителей, страниц и так далее. 

{mode: `open`} покажет, что мы можем через JavaScript со страницы достучаться к коду веб-компонента. Фичу можно закрыть передачей {mode: `closed`}. В целом, эта изоляция — защита от дурака, а не полноценное средство безопасности, её можно очень легко нарушить.

Теперь зарегистрируем наш компонент в Windows Custom Elements. 

35660e96806cf48afab88d7f90698be8.png

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

300b5b5d9cd856e05912e029da1689e3.png

А если его не зарегистрировать, то мы увидим весь контент обычным текстом, как в div. При нажатии на кнопку выпадет меню:

13152d607b49b8588b033f97c141b8eb.png

Если зайти в инструменты разработчика, то мы увидим вот что:

c129c3f465f15b049ff5091e60f250b3.png

Здесь есть похожий на iframe тег shadow-root с открытым режимом, который мы передавали в классе. В теле компонента можно посмотреть, откуда пришли дочерние элементы. Или можно по тегам slot выяснить, где отобразился компонент. 

Давайте на минуту вернёмся к нашей волшебной инкапсуляции. Возьмём простой WebInput:

463e69d90c9d806187da886c5981bea6.png

У него другой шаблон и идентификатор. Как и предыдущий компонент, мы его собираем и упаковываем с помощью Shadow DOM, регистрируем, в браузере всё правильно отображается. Как вы думаете, если мы положим такой компонент в форму, будет ли введённое пользователем значение доступно в этой форме? Оказывается, благодаря инкапсуляции, не будет. Придётся либо передавать значение выше через события, либо указать formAssociated = true, чтобы показать, что этот веб-компонент элемент формы. Также придётся примениять set и get, чтобы можно было использовать компонент прямо в форме, применить submit или другие методы работы с нативными формами без дополнительной обработки.

f2a4863912e03f2f9da1cfd11cf1facf.png

Первый API — это Custom Elements, то есть возможность создавать свои теги, описывать их логику и стили. Следующий блок спецификации — это Shadow DOM, который нам даёт инкапсуляцию стилей и JavaScript внутри компонента. Третий важный API — это HTML-теги