Веб-компоненты: долгая игра

Перевод статьи Web Components: The Long Game с сайта infrequently.org для CSS-live.ru, автор — Алекс Рассел

На прошлой неделе Майкл Роджерс выступил с докладом про веб-компоненты, который меня удивил, но его запись в блоге по следам этого доклада — толковый и своевременный материал для чтения.

Начиная проект, в котором возникли и много лет дорабатывались веб-компоненты, Дмитрий Глазков, Алекс Комороске и я ставили перед собой несколько главных целей:

  • Улучшить переносимость компонентов
  • Уменьшить количество инфраструктурного кода, требуемого для скачивания и во время работы
  • Дать браузеру возможность оптимизировать компоненты

И всё это делалось под девизом нашего проекта: «пиши то, что имеешь в виду».

Мы стояли (и стоим) на том, что разработчики не должны писать слово function, когда имеют в виду class или module, и не должны набирать <div class="tree-control"> или издеваться над существующими HTML-элементами, навязывая им очевидно неподходящий смысл. JavaScript-программистам нужна возможность создавать экземпляры компонентов естественным образом (new TreeControl(...)), и это не должно быть единственным вариантом, неявно заставляющим разработчиков отказываться от HTML в пользу JS или наоборот. Компонентное будущее не должно быть закрыто для тех, кто строит интерфейсы в HTML. Это значит, что компоненты должны участвовать во встроенной системе десериализации: HTML-парсере.

Веб-разработчикам нужна возможность обходиться без этапов сборки или сложных систем, по-своему реализующих парсинг в рантайме. И для того, чтобы просто вставить <tree-control> в разметку, не нужно, чтобы какой-либо конкретный фреймворк «имитировал» интеграцию с парсером в своей особой, «фирменной» манере управления задержками и жизненным циклом (источника множества несовместимостей).

Когда «иначе» не значит «лучше»

Когда в 2010-м мы начинали проект «Parkour», у участников команды был опыт создания порядка дюжины JavaScript-фреймворков или систем компонентов, и на этих системах работал фронтенд компаний с оборотом в миллиарды долларов, каждый день ими пользовались тысячи инженеров.

Ни в одной из них нельзя было толком использовать компоненты или код где-то еще.

Любой из этих инструментов при масштабном использовании сам собой давал эффект снежного кома. Код фреймворка сам по себе обходится недешево, а компоненты из других фреймворков тянут за собой весь служебный код, необходимый для работы компонентной модели в каждой из систем. Может, это терпимо для особенно «вкусного» компонента (как насчет грида с данными?), но чаще совместимость стопорилась об необходимость городить обертки для компонентов. Из-за выбора абстракции, которую нужно переносить из одной системы в другую, неявно возникает ситуация, когда командам приходится выбирать «свой» фреймворк, а потом заставлять компоненты от других систем работать в этих условиях.

Не нужно глубоко разбираться в истории JS-фреймворков, чтобы заметить, насколько разнообразны успешные инструменты по целому ряду важных параметров: самый продуктивный и эффективный способ создавать экземпляры компонентов, как и когда происходит конфигурирование, как обновляются данные и конфигурация, жизненный цикл компонента, контроль над управляемыми DOM-нодами (и доступ к ним), интеграция с разметкой и многое другое. Шаблонизаторы подключаются относительно легко, но у фреймворков такая особенность, что они задают условия для всего, что происходит в компонентах. Когда фреймворки делают разный выбор (а так оно и происходит), совместимость оказывается первой жертвой.

Мы-то привыкли, мы можем спорить о таких выборах до бесконечности. Но компании, которым нужны надежные инвестиции, давно поняли, что результат немного предсказуем: команды выбирают «лучший» инструмент, вовсю вкладываются в создание (или использование) компонентности, лишь чтобы обнаружить, что очередное приложение или другая команда сделали другой выбор, порождая запутанную проблему с совместимостью. Одно лишь обновление фреймворка с версии N на версию N+1 нередко порождает такие проблемы. И на тщательную работу по обеспечению доступности, единства стилей, разумной производительности и многого другого часто попросту не остается ресурсов.

На фундаментальном уровне это происходит потому, что когда компонентная модель — это JavaScript, каждый может выбрать что угодно. Может показаться, что, строя модель для всего на чистом JS (не DOM), можно достичь некой «более низкоуровневой» совместимости, но это иллюзия. Не знаю, какая функция верно описывает этот рецепт несовместимости, но интуитивно кажется, что сложность достижения совместимости — O(N^2) или еще хуже. С каждым важным решением в рамках фреймворка достичь совместимости с другим фреймворком становится сложнее, причем сложность растет по экспоненте. И это всё еще умножается на количество «вроде бы совместимых» фреймворков.

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

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

Как бы компаниям не хотелось использовать компоненты многократно, JavaScript-фреймворки, в своем нынешнем виде, никогда не возьмутся за совместимость. Это стали называть «мешаниной фреймворков», а не «смешиванием компонентов», потому что для внедрения чего-то нового его приходится наслаивать поверх старого.

Конец проблемы совместимости?

Мысль о том, что бесконечная мешанина фреймворков тянется с тех самых пор, как мы начали писать большие приложения, неплохо отрезвляет. Для меня она тянется уже больше 15 лет. Доказательства налицо, вердикт ясен: не может быть никакой долговечности компонентов с совместимостью, пока абстракцией у нас служит JS.

Глубинная причина этого в том, что все современные JavaScript-фреймворки для пользовательского интерфейса работают с двумя деревьями:

  • Логическим деревом высокоуровневых компонентов («виджетов»), из которых разработчики выстраивают свои приложения
  • Внутренним деревом управляемой DOM для каждого виджета.

Задача фреймворков — предоставить абстракцию для логического дерева, систему для создания и управления внутренностями виджетов, и — что важнее всего — системы, не дающие внутренностям виджетов «просочиться» в логическое дерево. До недавнего времени такая инкапсуляция по сути сводилась к созданию нового дерева, параллельного тому, что отображается в DOM.

До прихода Shadow DOM никак не удавалось не выносить грязное бельё компонентов (их управляемую DOM) на всеобщее обозрение в дереве документа. Авторам компонентов нужно работать с кусочками DOM, которыми они заведуют и управляют, тогда как пользователи компонентов обычно не желают видеть, трогать и тем более вмешиваться в тонкости реализации компонентов, из которых они собирают приложение.

Кастомные элементы и Shadow DOM устраняют потребность в отдельном дереве и системе его обхода. Вместе они позволяют разработчику выносить свои компоненты наружу как полноправных участников действа в рамках существующего соглашения (HTML и итоговой DOM), скрывая при этом детали реализации от случайного обхода и вмешательства. Встроенные элементы (вроде <video> или <select>) всегда владели этой хитростью, но до сих пор она не была доступна нам, простым маглам.

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

А еще ключевой момент в том, что веб-компоненты — это веб-стандарт. Пятилетний спор о том, как должны вызываться методы жизненного цикла, что они должны делать и как это всё должно состыковываться, наконец закончился. То, что уже сейчас работает и в Chrome, и в Safari, и в Opera, и в Samsung Internet, и в UC Browser, вряд ли запросто изменится (хоть к лучшему, хоть к худшему). Это соглашение, на которое полагается значительная часть веба; оно никуда не денется. Браузерам, где веб-компонентов еще нет, всё-таки придётся их реализовать.

Если вы — технический руководитель или менеджер веб-команды, самое время подумать, когда и как переходить на веб-компоненты, и в чем для вас выгода от фреймворков, когда большую часть их функций уже перетянула на себя платформа. Команды, которые смотрят в будущее (напр. Ionic), уже сейчас переходят, и результаты просто невероятны.

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

Не только совместимость

В докладе, с которым я выступал несколько недель назад на конференции Polymer Summit, я подробно раскрыл то, чем мотивировалась часть нашей работы в проекте Parkour с точки зрения производительности:

Одно из важнейших преимуществ того, что наша компонентная модель делегируется самой платформе — то, что тогда многие задачи становится дешевле. Мало того, что можно выкинуть код для создания двух разных деревьев и управления ими, браузеры будут улучшать производительность для построенных таким образом приложений, соревнуясь в этом друг с другом. За последний год в Chromium уже добились значительного ускорения для кастомных элементов и Shadow DOM, над этим работают и дальше. Ограничение видимости CSS на уровне платформы благодаря Shadow DOM дает значительный выигрыш в плане памяти и вычислений, а с новой архитектурой всей системы у кастомных элементов появятся преимущества там, где решения на привычном JS уже почти достигли своего предела.

Помимо всего этого, главный тезис Майкла оказался весьма созвучным:

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

Работая над веб-компонентами, мы, кроме всего прочего, рассчитывали, что благодаря им сможет вернуться тот стиль разработки, когда для просмотра результатов изменения достаточно было нажать Ctrl-r. С какого-то уровня сложности и величины проекта нам всем становятся нужны инструменты, чтобы справиться с размером кода, структурой приложения и прочим. Но каждый раз с любовью и нежностью «вылизывать» конфигурации то babel, то webpack, то npm, кажется сейчас каким-то… наказанием. Всё это не должно быть необходимым для разработки одного компонента (или немногих), и строить интерфейсы не должно быть так трудно. Сложность инструментов должна снова стать пропорциональна сложности решаемой ими проблемы. Без общей компонентной модели этого никогда не добиться.

Я в восторге, что мы наконец этого дождались.

P.S. Это тоже может быть интересно:

9 Комментарии
  1. undifined

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

    Сегодня мы говорим — родитель, вот тебе стиль flex, расставь своих детей вот так.
    Но shadowdom, который все ждут за его инкапсуляцию стилей, не позволяет так делать.
    Ведь если бы он позволял родителям устанавливать стили детям, то не было бы и инкапсуляции. Вместо это придется в детях прописывать все стили. То есть, будет так — чилд, если ты в таком-то родители, то примени к себе вот эти стили, если в таком-то, то такие. Это просто вынос. В angular 4 такая инкапсуляция включена по умолчанию. Писать стили вообще невозможно. Удовольствие превратилось в муку. Все с кем я разговаривал на эту тему, отключают эту инкапсуляцию.

    к тому же все ui на очень много процентов состоят из переиспользуемых стилей. Загружая такие вот компоненты, css кода, который в большей степени будет одинаков, станет просто в разы больше. Поэтому радость появления сомнительная. Есть много вариантов реализации в других языках, но в вэбе опять пошли наихудшим путем.

  2. Вадим

    Мне кажется Вы рассматриваете неправильно применение стилей внутри кастомных элементах и их shadow DOM. Стили внутри этих кастомных элементов, а именно в их shadow DOM предназначены больше именно для shadow DOM структуры, чтобы снаружи нельзя было напрямую их стилизовать. А если такое и нужно, то здесь пригодятся кастомные свойства. Стили внутри кастомных элементов не должны быть переиспользуемы, т.к. они предназначены только для текущего компонента и скорее всего если у Вас получается что в одном компоненте стили дублируют стили другого компонента, то выбран неправильный подход. На крайний случай можно создавать кастомные элементы из других кастомных, наследуя часть их «внутренностей», например стили, ну или же ждать @apply или использовать через postCSS

    1. undifined

      Я не понимаю о чем Вы.. Представьте компонент показывающий статью, что-то типа card с картинкой и описанием, количеством лайков и т.п. И вот Вы его скачали, но к тому же Вы скачали адаптивный контейнер, который с помощью флекс расставляет как Вам нравится контент. Но работать-то это не будет, так как компонент отображающий контент не позволит своему родителю применить к нему стили. Вэб крут только за счет css, это его самая сильная сторона. css это каскадные стили, а в будущем с его shadowdom каскад превратится в что-то обратное.

      Про @apply не слышал, если это позволит устанавливать стили от родителя к детям, то это круто, но будет наверное только лет через 10, а я живу здесь и сейчас.

      1. Вадим

        Если Вы хотите стилизовать shadow DOM компонента в зависимости от родителя то эти стили прописываются в стилях компонента, я к сожалению не помню какой для этого новый css-селектор нужен, но такой есть, в Shadow DOM level 0 и Shadow DOM level 1 они отличаются. Выглядеть будет типа :host-context(.flex) :host div {}. Т.е. если сам компонент находится внутри блока .flex то внутренние div в shadow DOM будут стилизованы по данному правилу.
        Или же просто добавляете класс модификатор для самого компонента и стилизуете от класса :host(.mod) div {} и если у компонента класс mod, то применяются данные стили.

        @apply напоминает кастомные свойства, но позволяет определить целый набор свойств который наследуется по всему дереву, даже внутрь shadow DOM, по аналогии с кастомными свойствами, и этот набор стилей можно применить в любом дочернем элементе. Т.е. в контейнере для компонента можете задать набор правил и использовать этот набор внутри стилей кастомного элемента, что-то типа миксинов из препроцессоров. К сожалению пока поддержка только за флагами.

        Ну и вообще вам никто не запрещает внутри кастомных элементов не использовать shadow DOM, можете просто создавать содержимое напрямую без обертки в виде shadow DOM.
        shadow DOM нужен когда необходимо оградить какие-то «внутренности» верстки компонента от воздействия извне напрямую.

        1. Вадим

          И в этом как раз смыл, что все стили компонента должны быть в нем. Т.к. если тебе не нужен этот компонент ты просто не подключаешь его, а иначе тебе еще искать по другим стилям где удалить связанное с ним.
          Хотите изолированность — используйте shadow DOM, не хотите — делайте без shadow DOM. В каждом способе свои плюсы и минусы, но я пока не вижу серьезных проблем с использованием shadow DOM

          1. undifined

            Ну Вы и говорите о том, что я в первом сообщении написал. То есть, стили нужно указывать шиворот на выворот. От ребенка к родителю. :hot(flex), а если кто-то не захочет флексом выравнивать, тогда что? Тогда автор компонента должен как ненормальный под все случая жизни писать стили. Я много-много писал с этим shadowdom и могу сказать, что по человечески так работать просто нереально.

            1. Вадим

              Зачем писать на все случаи жизни, если компонент пишется под проект, просто заложили в нем нужные варианты и все, если что-то добавилось — дописали. С одной стороны же разницы вообще нет в каких стилях Вы добавите, в общих или изолированных, просто откроете стили и допишите что нужно. Делать что-то универсальное вообще не реально, хоть с shadow DOM хоть без него.
              Если же Вы рассматриваете случай когда используете сторонние веб-компоненты, как jQuery плагины, то кто Вам мешает так же дописать в компонент свои стили. Вы же наверное те же самые jQuery плагины стилизуете сами еще под UI проекта.

              Еще где-то читал что рассматривалась возможность стилизации внутренних элементов shadow DOM за пределами стилей компонента по аналогии со стилизацией скролла, input[type=range] и других элементов в хроме через *::-webkit-*. Т.е. внутри компонента в стилях какой-то элемент Вы определяете, например как chilren и за shadow DOM он будет доступен как custom-element::chilren и тут стилизуйте как хотите. Но точно не помню, это уже принятая спецификация или предложения.

              1. undifined

                Ну если взять Ваши слова за правду и сделать из них лозунг, с которым шагать по жизни, то так можно и яичницу обратной стороны сковородки жарить, главное чтобы она растечься не успела. А если так не нравится, то всегда можно самому молотком вбить обратную сторону сковородки, чтобы она как передняя стала. Только вот спрашивается — нафиг так делать, если уже есть лучше?

                Я говорю, я очень долго писал стили, причем всегда компоненты сам с нуля пишу, и не разу не испытывал того, что привнес этот shadowdom.

  3. Leo

    Вэб? Вроде нет такого слова в русском языке. Идея компонентов, в том, чтобы они работали с минимум зависимостей от родительского компонента, в лучшем случае standalone. Предполагается что контейнер и все его итемы разместятся внутри одного компонента, ну flexbox собственно исчезающий зверь (привет, Box Alignment). Родительские компоненты на своих детей должны влиять через явные проброшенные в них параметры. В общем, так живет QML и никто еще не умер.

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

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