- PVSM.RU - https://www.pvsm.ru -

Веб-компоненты в реальном мире (часть 2)

Прошло больше года с моей публикации "Веб-компоненты в реальном мире" [1] и у меня накопились новые наблюдения, что ещё не так с этой технологией. Возможно, эти моменты позволят кому-то избежать тупикового пути для своих проектов.

rusty carPhoto by Brandon Molitwenik [2] on Unsplash [3]

Сломанный HTML

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

<form>
  <label>First name: <input type="text"></label>
  <label>Last name: <input type="text"></label>
  <button>Send!</button>
</form>

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

А теперь попробуем заменить обычную кнопку на веб-компонент:

<form>
  <label>First name: <input type="text"></label>
  <label>Last name: <input type="text"></label>
  <my-button>Send!</my-button>
</form>

Веб-компонент my-button внутри себя содержит всё ту же кнопку, визуально никаких отличий нет. А вот отправка формы по нажатию Enter сломалась! Вот демо [4], можете убедиться в этом сами.

В чем причина такого поведения? Это недоработка спецификации веб-компонентов, вот тикет [5]. В библиотеках разработчики обходят эту проблему с помощью вот такого костыля [6], например. Сам код выглядит не очень страшно, но давайте на секунду задумаемся: мы пишем кастомный Javascript, чтобы починить поведение, которое сломал веб-стандарт. На всякий случай напомню, что спецификации веб-компонентов уже 8 лет и изо всех утюгов трубят, что она уже production-ready.

Но это ещё не всё, что умудрились сломать в веб-компонентах. В HTML есть такая фича, автоматический фокус поля ввода при нажатии на соседний label. Очень удобно, не обязательно целиться в маленький квадрат, можно нажать на текст рядом. Но не в случае веб-компонентов! Вот пример:

<label>First name: <input type="text"></label>
<label>Last name: <my-input></label>

На демо [7] видно, что обычный тэг input можно выделить нажатием на "First name", а вот нажатие на "Last name" веб-компонент выделить не может. Проблема! На эту тему есть открытый тикет [8] с последним комментарием 2 года назад, так что скорого разрешения тут ждать не стоит. У разработчиков пока есть только один способ – объединить label и input в один компонент. А как быть, если дизайн этого не позволяет? Тут два варианта, либо уговаривать дизайнеров придумать что-то совместимое с веб-компонентами, либо отказаться от веб-компонентов в своем проекте (по крайней мере, от ShadowDOM).

CSP

В своё время нашумел "Рассказ о том, как я ворую номера кредиток и пароли у посетителей ваших сайтов" [9]. В качестве одной из мер защиты там упоминается CSP [10] – возможность указать белый список доменов, на которые разрешено делать запросы с вашей страницы. Одним из побочных эффектов внедрения CSP является невозможность использовать <style></style> тэги, только внешние файлы через <link rel="stylesheet"> (конечно, можно разрешить style-тэги обратно, через директиву 'unsafe-inline', но как видно из её названия, это будет ослабление вашей защиты).

При чем здесь веб-компоненты? Дело в том, что содержимое ShadowDOM полностью изолированно от внешних стилей, загруженных на страницу, поэтому для стилизации внутри ShadowDOM обычно используются style-тэги, что противоречит CSP. Два самых популярных веб-компонент фреймворка имеют с этим проблемы: Stencil (тикет [11]) и LitElement (тикет [12]).

Свет в конце туннеля есть – планируется новое Constructable Stylesheets API [13], которое позволит создавать стили для ShadowDOM в безопасной форме без необходимости в unsafe-inline. А пока разработчикам придется делать выбор – либо CSP, либо веб-компоненты.

Lifecycle-хаос

В хорошей архитектуре компоненты должны выполнять роль кирпичиков, из которых собирается большой проект. Например, мы можем получить такую комбинацию (по аналогии с material-web-components [14]):

<my-menu>
    <my-menu-item />
    <my-menu-item />
</my-menu>

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

class MyMenu extends HTMLElement {
    connectedCallback() {
        console.log('my menu')
    }
}

class MyMenuItem extends HTMLElement {
    connectedCallback() {
        console.log('my menu item')
    }
}

// регистрация
customElements.define('my-menu', MyMenu)
customElements.define('my-menu-item', MyMenuItem)

Запускаем демо [15], смотрим в консоль и видим:

"my menu"
"my menu item"
"my menu item"

Можно предположить что connectedCallback вызывается на родительском элементе, потом на дочерних. Звучит логично, почему нет. А что, если мы сделаем маленькое изменение и откроем второе демо [16]:

"my menu item"
"my menu item"
"my menu"

Как это получилось? Почему my-menu теперь опаздывает? В HTML изменений нет, но мы переставили эти две строки местами

// было
customElements.define('my-menu', MyMenu)
customElements.define('my-menu-item', MyMenuItem)

// стало
customElements.define('my-menu-item', MyMenuItem)
customElements.define('my-menu', MyMenu)

Оказывается, порядок регистрации элементов влияет на порядок вызова connectedCallback [17]. В практическом смысле это означает то, что мы не можем знать порядок вызова методов, и наш код должен быть готов обработать оба варианта. С вариантом "нас вызвали слишком рано" все просто, добавляем window.setTimeout делаем нашу инициализацию попозже. В случае "нас вызвали слишком поздно" ситуация хуже, мы уже не сможем отменить начатые операции. Поэтому на веб-компонентах не получится сделать нормально работающий компонент спойлера

Пример спойлера

Спасибо что заглянули, вот вам котик:
Веб-компоненты в реальном мире (часть 2) - 2

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

Выводы

В веб-компонентах повсюду раскиданы грабли, грамотно присыпанные маркетингом от Гугла. В стандарте еще много неразрешенных вопросов, которые могут оказаться непреодолимым препятствием для ваших проектов. Было бы полезно знать о потенциальных граблях заранее, чтобы принять более взвешенное решение, использовать ли веб-компоненты и фреймворки на их основе, или остаться с простым старым подходом на HTML/JS/CSS. Надеюсь, эта статья была полезной, спасибо за внимание!

Автор: Boris Serdiuk

Источник [18]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/356009

Ссылки в тексте:

[1] "Веб-компоненты в реальном мире": https://habr.com/en/post/443032/

[2] Brandon Molitwenik: https://unsplash.com/@brandomol

[3] Unsplash: https://unsplash.com/

[4] демо: https://output.jsbin.com/musajuc/

[5] тикет: https://github.com/w3c/webcomponents/issues/814

[6] такого костыля: https://github.com/ionic-team/ionic-framework/blob/753fd2f910e718438e3ac918e2f129501d8e9791/core/src/components/button/button.tsx#L163-L173

[7] демо: https://output.jsbin.com/reyemul/

[8] тикет: https://github.com/whatwg/html/issues/3219

[9] "Рассказ о том, как я ворую номера кредиток и пароли у посетителей ваших сайтов": https://habr.com/en/company/ruvds/blog/346442/

[10] CSP: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

[11] тикет: https://github.com/ionic-team/stencil/issues/496

[12] тикет: https://github.com/Polymer/lit-element/issues/478

[13] Constructable Stylesheets API: https://github.com/WICG/construct-stylesheets

[14] material-web-components: https://github.com/material-components/material-components-web-components/blob/master/packages/menu/README.md

[15] демо: https://output.jsbin.com/wulitoh/

[16] второе демо: https://output.jsbin.com/hiqebis/

[17] порядок регистрации элементов влияет на порядок вызова connectedCallback: https://github.com/webcomponents/polyfills/issues/103

[18] Источник: https://habr.com/ru/post/515332/?utm_source=habrahabr&utm_medium=rss&utm_campaign=515332