Как подружить веб-компоненты и JS-фреймворки
Всем привет, я Роман Троицкий. Очень люблю веб-разработку; участвовал в проектах, попавших на Awwwards, Tagline и GoldenSite; помогаю организовывать митап Moscow CSS; участвовал в записи и разработке курса по фронтенду для Skillbox. На примере своего проекта я расскажу о сложившейся с Web Components ситуации, опишу их достоинства и недостатки.
Зачем нужны веб-компоненты?
У крупных технокомпаний, как правило, есть множество ИТ-продуктов. Разрабатывают их, обычно, не одни и те же люди, целые группы команд. При этом фронтенд во всех этих продуктах обычно пишут на одних и тех же технологиях: берут готовый шаблон, что-нибудь обновляют — и готово. А что делать, если все эти продукты написаны на разных фреймворках? Конечно, можно сделать десяток UI-наборов на все случаи жизни, но будет крайне дорого их писать и поддерживать. А ещё придётся потратить колоссальное количество времени — конкуренты обгонят, сотрудники выгорят. Переписывать на один и тот же стек нецелесообразно по тем же причинам.
Какие есть варианты?
Взять готовый headless UI-набор с логикой компонентов, поверх которой можно добавить какой-то адаптер к фреймворку и корпоративный дизайн. Самый известный представитель такого подхода — это Tan Stack, известный также как React Query и Vue Query.
Использовать мета-фреймворки для создания UI-наборов. Скажем, Mitosis позволяет собрать на JSX-компоненты, сделать что-то вроде AST и сгенерировать наборы компонентов с обвязками под нужные нам фреймворки. К примеру, есть у нас простой компонент с состоянием:
Сначала он превратится в JSON, тоже с состоянием:
Разметка превратится в массив nodes. Сначала идёт родительский div
, в нём вложенный input
. Дальше мы видим биндинги, onChange
, привязку слушателей, значение состояния. И из этого кода средствами Mitosis мы можем десериализовать обратно в компонент, например, для Angular.
Но ещё более интересными и перспективными показались веб-компоненты.
Web Components
Эта технология вышла на рынок почти 15 лет назад, однако широкого распространения до сих пор не получила. Кто-то внедрял куда-то или читал новости спецификаций, выступал на конференции с докладом, затем опять тишина, и спустя пару лет снова один-два спикера заикаются, снова тишина. Но пару лет назад я решился использовать эту технологию в эксплуатации. Это было не просто смело, это было очень смело. Но всё оказалось не так страшно. Коротко расскажу, что это такое.
Веб-компоненты — это набор Web-API для создания собственных HTML-тегов, со своей логикой, дизайном, отображением и так далее. Например:
Для работы этого элемента нужно создать класс:
Сохраним внутренности шаблона в переменную. В шаблоне опишем разметку нашего компонента, и дальше с ним можем работать с помощью DOM API — клонировать, применять, добавлять стили и так далее.
Чтобы компонент был максимально независимым от стилей, мы можем его инкапсулировать с помощью метода attachShadow
. Это можно сделать с помощью атрибутов тега template
, в котором мы пишем разметку. Инкапсулировать можно и какую-нибудь логику. Модифицировать стили в таком случае мы можем только через sys-переменные, потому что Shadow DOM позволяет нам эти стили абстрагировать от содержимого родителей, страниц и так далее.
{mode: `open`}
покажет, что мы можем через JavaScript со страницы достучаться к коду веб-компонента. Фичу можно закрыть передачей {mode: `closed`}
. В целом, эта изоляция — защита от дурака, а не полноценное средство безопасности, её можно очень легко нарушить.
Теперь зарегистрируем наш компонент в Windows Custom Elements.
Название тега передаётся первым параметром и должно состоять минимум из двух слов через дефис. Вторым параметром мы передаём сам класс. После этого компонент отображается в браузере.
А если его не зарегистрировать, то мы увидим весь контент обычным текстом, как в div
. При нажатии на кнопку выпадет меню:
Если зайти в инструменты разработчика, то мы увидим вот что:
Здесь есть похожий на iframe
тег shadow-root
с открытым режимом, который мы передавали в классе. В теле компонента можно посмотреть, откуда пришли дочерние элементы. Или можно по тегам slot
выяснить, где отобразился компонент.
Давайте на минуту вернёмся к нашей волшебной инкапсуляции. Возьмём простой WebInput
:
У него другой шаблон и идентификатор. Как и предыдущий компонент, мы его собираем и упаковываем с помощью Shadow DOM, регистрируем, в браузере всё правильно отображается. Как вы думаете, если мы положим такой компонент в форму, будет ли введённое пользователем значение доступно в этой форме? Оказывается, благодаря инкапсуляции, не будет. Придётся либо передавать значение выше через события, либо указать formAssociated = true
, чтобы показать, что этот веб-компонент элемент формы. Также придётся примениять set
и get
, чтобы можно было использовать компонент прямо в форме, применить submit
или другие методы работы с нативными формами без дополнительной обработки.
Первый API — это Custom Elements, то есть возможность создавать свои теги, описывать их логику и стили. Следующий блок спецификации — это Shadow DOM, который нам даёт инкапсуляцию стилей и JavaScript внутри компонента. Третий важный API — это HTML-теги и
, которые мы использовали для отрисовки разметки.
Промежуточные итоги
Какие достоинства и недостатки у веб-компонентов?
Достоинства:
Нативность. Можно использовать нативные веб-компоненты как свои HTML-теги, которым не нужен фреймворк для отрисовки, разметки или транспиляции в HTML текущего стандарта. То есть мы как бы расширяем текущий стандарт своими тегами с нужной нам функциональностью и отображением.
Инкапсуляция. Внутри компонентов мы можем неплохо скрыть внутренние API и стили, как бы отделяя логику компонента от приложения, тем самым снижая риски возможных коллизий.
Переиспользование. Мы можем переиспользовать веб-компоненты.
Агностицизм. Мы можем переиспользовать их в любом фреймворке. Собранный компонент — это немного JavaScript-кода и HTML-тег с какими-то атрибутами.
Меньше зависимостей. Нужно гораздо меньше кода для работы в браузере.
Производительность. Меньше объём, меньше кода тянет за собой, и поэтому требует значительно меньше ресурсов
Недостатки:
Всплытие и прослушка событий. Возникают вопросы с событиями, с проброской данных. Приходится или обвешиваться сообщениями и прослушками, либо хранить данные в Windows. Или можно сделать приложение так, чтобы веб-компоненты были независимыми.
Свойства — строки. Все эти проблемы связаны ещё и с тем, что свойства могут быть только строками, поэтому приходится сериализовать и десериализовать данные для работы с ними внутри компонента, передавать HTML-атрибуты.
Связывание данных. Из-за этого многие задачи трудно реализовать, например, кросс-валидацию.
Очень много работы с DOM API. В целом, ничего страшного, просто писать приходится гораздо больше, чем если бы мы делали компоненты на Vue.js, например.
Очень сильно хромает доступность на уровне браузерных элементов.
Проблемы с поисковой оптимизацией, потому что мы отображаем веб-компоненты с помощью JavaScript.
И, как следствие всего этого, популярность этой технологии в мире достаточно низкая, потому что при небольших преимуществах мы получаем пачку вышеописанных недостатков.
Почему я тогда рассказываю о веб-компонентах?
Во-первых, есть ещё один веб-API, который появился в Chrome в ноябре 2023 — Declarative Shadow DOM. Он позволяет определить shadow DOM для компонента прямо в HTML без JavaScript. Это упрощает создание веб-компонентов и уменьшает количество JavaScript-кода —, а значит, сильно вырастает производительность. Этот API пока сыроват, однако его поддержку запланировали внедрять во все браузеры. Посмотрим, что из этого выйдет.
А во-вторых, на самом деле технология интересная. Она не для гиков и докладов на конференциях, её используют в production такие компании, как Google, Adobe, SpaceX. Например, интерфейс управления ракетами в SpaceX написан на веб-технологиях с применением веб-компонентов. Среди российских компаний я не нашёл упоминаний об использовании этой технологии. На HeadHunter не встречаются требования уметь работать с веб-компонентами, нет примеров с ними.
Библиотека Stencil
Есть очень много библиотек, которые так или иначе нивелируют описанные выше недостатки технологии, упрощают работу с ней. Например, Lit, StencilJS. Последняя мне нравится тем, что позволяет без проблем отрисовывать веб-компоненты на сервере. С помощью этой библиотеки можно написать полноценное приложение. Кроме того, Stencil позволяет сделать универсальный UI-набор для веб-компонентов или с готовыми обвязками под большую тройку фреймворков. Вдобавок, есть отличный консольный интерфейс, куча встроенных команд. Покажу, как легко создавать веб-компоненты с помощью Stencil:
Из коробки доступно много декораторов. Библиотека написана на TypeScript, всё по стандартам. С помощью декоратора Component дадим название тегу, укажем путь до всех стилей, и выберем режим Shadow DOM.
В классе компонента можно указать типы свойства, значения по умолчанию, виды классового события, которое будет генерировать наш компонент — всё это тоже с помощью декораторов, которые предоставляет Stencil. Если мы хотим отрисовать компонент при изменении его свойства, то можем прокинуть mutable: true
в нужный декоратор. Нужно реактивное состояние? Пожалуйста. Состояние должно быть комплексным, с объектом, массивом и так далее? Без вопросов.
И в конце обязательный метод render
, в котором мы можем написать всю разметку в корректном JSX:
Писать такие компоненты получается быстро, они очень просто выглядят, всё декларативно. Stencil ещё позволяет снабдить наш UI-набор обёртками для фреймворков. Сначала сделаем два NPM-пакета, покажу на примере Vue и React:
Импортируем из них функции в конфигурацию Stencil, и в outputTargets
просто вызываем их, передаём маленький конфиг, в который хотим сложить наши компоненты с обвязкой, названием точки входа для построения графа зависимостей.
Всё на модулях, есть reshaking. Можно использовать это как есть из готовой сборки, а можно упаковать в отдельные NPM-пакеты, версионировать, публиковать в разные зоны.
Давайте посмотрим, как это будет выглядеть в приложении. Я взял для примера Vue.js. По документации Stencil делаем плагин в папке со сборкой компонентов, импортируем и используем:
Компоненты будут видны в приложении, свойства проверяются, события генерируются, всё это с автозавершением.
Ещё есть документация:
Будет сгенерирован один огромный JSON-файл со всей метаинформацией по всем компонентам. Настройка type: `docs-readme`
создаёт readme-файлы в каждой директории с компонентом. То есть верхняя часть — генерация Stencil, а если мы будем модифицировать компонент, то в сборке будет модифицироваться readme-файл. Stencil генерирует эту документацию благодаря декораторам и TypeScript’у.
Можем добавить JSDoc для компонентов. Он пойдёт либо в метаполя, либо в описание полей для документации. JSDoc — это движок, который собирает вот такие комментарии и позволяет генерировать с их помощью какую-то документацию:
Поскольку в нашем случае JSDoc пойдёт именно в storybook, я считаю, что есть смысл потратить на него время и упростить работу с UI-набором. Есть только небольшие ограничения:
Приватные методы класса не попадают в документацию, но мы можем их добавить вручную в readme-файл в директории с компонентом.
После этого достаточно завести storybook, добавить дополнение, оно примет сгенерированные Stencil файлы документации и создаст стенд с документацией:
Можем что-то поменять, но из коробки всё будет готово на 90%.
Производительность
Ещё один аргумент в пользу веб-компонентов — их производительность. Есть регулярно обновляемый набор тестов с открытым исходным кодом от Krausest, вот с каким результатом они выполняются:
Конечно, это лабораторные условия, в реальности всё зависит от квалификации того, кто пишет, но выглядит очень позитивно. А ведь здесь ещё нет Declarative Shadow DOM, который позволит часть компонентов использовать вообще без JavaScript.
Заключение
Почему веб-компоненты, появившиеся ещё до React и других компонентных фреймворков, не смогли захватить рынок? Выскажу своё мнение.
Несколько лет назад я использовал веб-компоненты в эксплуатации. Самыми сложными элементами были графики на Canvas, которые мы с помощью Chart.js делали в веб-компонентах, и комплексные таблицы. Там было немало сложностей. Графики были динамические, приходилось всё отслеживать, влияли зависимости. Чтобы облегчить себе работу, я воспользовался StencilJS. Она сильно помогла. Потом удалось подружить веб-компоненты с Vue.js-приложением. В общем, большинство проблем были связаны с моими руками и сложными бизнес-требованиями.
Какие я сделал выводы?
Простые вещи — кнопки, поля ввода, заголовки и так далее — делать на веб-компонентах легко и приятно, они хорошо работают. Если компоненты посложнее, но замкнуты в себе, например, как таблица, то с ними тоже ещё можно жить. Например, я свойствами пробрасывал URL, по которому таблица сама запрашивала данные, которые нужно было отрисовать. Это было гораздо проще других способов проброса данных, их нужно централизованно зачем-то хранить. Но если бы найденный мной способ не подошёл, было бы намного неприятнее. Были вопросы только с доступностью и с SEO, но в том проекте это не особо требовалось.
Сейчас веб-компоненты используются в некоторых проектах Сбера. Есть ряд виджетов, которые ранее рендерили в iFrame и отдавали партнёрам. Сейчас для упрощения встраивания эти виджеты перевели на веб-компоненты, и партнёры могут их там немного модифицировать, устанавливать через NPM-пакеты и так далее. Про внутренние проекты с веб-компонентами рассказать не могу из-за NDA, но такие тоже есть.
На текущий момент я всё-таки считаю, что веб-компоненты — это сомнительно, но окей. Они не самые удобные, есть и достоинства, и недостатки, их можно использовать в популярных фреймворках. И многое зависит от дальнейших действий Interop: обещают много хорошего, новый API, отказ от JavaScript. Поэтому в перспективе веб-компоненты могут стать очень даже интересной технологией.