Я пишу это в основном для себя в будущем, чтобы у меня было куда сослаться, когда кто-нибудь спросит меня, почему я скептичен в отношении веб-компонентов и почему Svelte не компилируется в веб-компоненты по умолчанию. (Тем не менее, он может компилироваться в веб-компоненты, а так же интегрироваться с ними, что подтверждается превосходной оценкой на Custom Elements Everywhere).


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


1. Прогрессивное улучшение


Это может быть старомодным убеждением, но я считаю, что веб-сайты должны работать без JavaScript насколько это возможно. Веб-компоненты без JS не работают. Это нормально для вещей, которые по своей природе интерактивные, такие как кастомные элементы форм (<cool-datepicker>), но это ненормально для навигации сайта, например. Или представьте себе компонент <twitter-share>, который инкапсулирует в себе логику построения URL для отправки в Twitter. Я мог бы реализовать его на Svelte, что отрендерит на сервере мне вот такой HTML:


<a target="_blank" noreferrer href="..." class="svelte-1jnfxx">
  Tweet this
</a>

Другими словами, обычный <a> во всем его доступном великолепии.


При включенном JavaScript происходит прогрессивное улучшение – вместо открытия нового таба, открывается маленькое всплывающее окно. Но и без JS, компонент все еще работает нормально.


В случае веб-компонента HTML выглядел бы как-то так:


<twitter-share text="..." url="..." via="..."/>

… что бесполезно и не пригодно для использования, если JS заблокирован, или почему-то сломался, или у пользователя старый браузер.


Кроме того, class="svelte-1jnfxx" предоставляет нам инкапсуляцию стилей без Shadow DOM. Что приводит нас к следующему пункту.


2. CSS в, эээ… JS


Если вы хотите использовать Shadow DOM для инкапсуляции стилей, то вам понадобится вставить свой CSS в тэг <style>. Единственный практичный способ это сделать, если вы хотите избежать моргания загружащегося контента (FOUC), это встроить CSS как строку в JavaScript, который определяет всю остальную логику вашего веб-компонента.


Это противоречит совету об улучшении производительности, который гласит: "поменьше JavaScript, пожалуйста". Сообщество CSS-in-JS, в частности, много критиковалось за неиспользование css-файлов для CSS, и вот, с веб-компонентами мы снова здесь.


В будущем, мы сможем использовать CSS Modules а также Constructable Stylesheets, чтобы справиться с этой проблемой. Еще у нас будет возможность стилизовать внутренности Shadow DOM через ::theme и ::part. Но и здесь не обошлось без проблем.


3. Усталость платформы



Это больная темя для меня – я рекламировал эти вещи как "Будущее" на протяжении нескольких лет, но для того чтобы не отставать от настоящего мы должны были набить платформу кучей разных фич, усугубляя разрыв между браузерами.

На момент написания, на https://crbug.com, баг-трекере Хрома, 61,000 открытых багов, которые показывают огромную сложность написания современного браузера.


Каждый раз когда мы добавляем в платформу новую фичу, мы увеличиваем сложность – создаем потенциал для новых багов и делаем все менее вероятным то, что у Хрома появится новый конкурент. Это также создает сложности для разработчиков, которых призывают учить эти новые фичи (некоторые из которых, например HTML Imports или изначальня версия стандарта Custom Elements, никак не прижились за пределами Google и теперь в процессе удаления).


4. Полифилы


То что вам нужно использовать полифилы для поддержки старых браузеров, не способствует развитию ситуации. И это совсем не помогает, что статьи на тему Constructable Stylesheets, написанные в Google (привет, Джейсон!), никак не упоминают, что эта фича доступна только в Chrome. (Все три автора спецификации работают в Google. Webkit, кажется, имеет сомнения по поводу некоторых аспектов этого стандарта).


5. Композиция


Бывает полезно контролировать, когда содержимое слота должно отрендериться. Представьте, что у вас есть компонент <html-include> для загрузки какого-то дополнительного контента, когда он виден:


<p>Toggle the section for more info:</p>
<toggled-section>
  <html-include src="./more-info.html"/>
</toggled-section>

Внезапно! Даже если мы еще не открыли toggled-section, но браузер уже запросил more-info.html, вместе со всеми изображениями и другими ресурсами, что там есть.


Это происходит потому что содержимое слотов рендерится в веб-компонентах заранее. В реальности же оказывается, что в большинстве случаев вы хотите рендерить содержимое слотов лениво. Svelte v2 принял упреждающую модель реднеринга чтобы соотвествовать веб-стандартам, но это оказалось основным источником неудобств – мы не могли создать что-то похожее на React Router, например. В Svelte v3 мы отошли от поведения веб-компонентов и ни разу не оглядывались назад.


К сожалению, это была одна из фундаментальных характеристик DOM. Что приводит нас к...


6. Путаница между свойствами и атрибутами


Свойства и атрибуты это же, в принципе, одно и тоже, правда?


const button = document.createElement('button');

button.hasAttribute('disabled'); // false
button.disabled = true;
button.hasAttribute('disabled'); // true

button.removeAttribute('disabled');
button.disabled; // false

Ну почти:


typeof button.disabled; // 'boolean'
typeof button.getAttribute('disabled'); // 'object'

button.disabled = true;
typeof button.getAttribute('disabled'); // 'string'

Бывают имена, которые не совпадают:


div = document.createElement('div');

div.setAttribute('class', 'one');
div.className; // 'one'

div.className = 'two';
div.getAttribute('class'); // 'two'

… а есть и такие, которые вообще не согласованы:


input = document.createElement('input');

input.getAttribute('value'); // null
input.value = 'one';
input.getAttribute('value'); // null

input.setAttribute('value', 'two');
input.value; // 'one'

Но мы бы смогли справиться с этими причудами, взаимодействия строкового формата (HTML) и DOM. Есть конечное число этих особенностей, они задокументированы, так что мы хотя бы можем о них узнать, при наличии времени и терпения.


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


class MyThing extends HTMLElement {
  static get observedAttributes() {
    return ['foo', 'bar', 'baz'];
  }

  get foo() {
    return this.getAttribute('foo');
  }

  set foo(value) {
    this.setAttribute('foo', value);
  }

  get bar() {
    return this.getAttribute('bar');
  }

  set bar(value) {
    this.setAttribute('bar', value);
  }

  get baz() {
    return this.hasAttribute('baz');
  }

  set baz(value) {
    if (value) {
      this.setAttribute('baz', '');
    } else {
      this.removeAttribute('baz');
    }
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'foo') {
      // ...
    }

    if (name === 'bar') {
      // ...
    }

    if (name === 'baz') {
      // ...
    }
  }
}

Можно сделать и наоборот – attributeChangedCallback вызывают геттеры и сеттеры. В любом случае, удобство работы с этим просто удручающе. В то же время во фреймворках имеется простой и однозначный способ передать данные в компонент.


7. Протекающий дизайн


Этот пункт немного расплывчатый, но мне кажется странным, что attributeChangedCallback это просто метод класса. Вы можете сделать буквально следующее:


const element = document.querySelector('my-thing');
element.attributeChangedCallback('w', 't', 'f');

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


8. Плохой DOM


Ок, мы уже установили, что DOM – плохой. Но все еще тяжело преувеличить, насколько это неудобный способ делать интерактивные приложения.


Несколько месяцев назад я написал статью "Пишите меньше кода", призванную проиллюстрировать как Svelte позволяет писать компоненты более эффективно, чем фреймворки вроде React и Vue. Там не было сравнения с ванильным DOM, а должно бы. Вкратце, у нас есть простой компонент <Adder a={1} b={2}/>:


<script>
  export let a;
  export let b;
</script>

<input type="number" bind:value={a}>
<input type="number" bind:value={b}>

<p>{a} + {b} = {a + b}</p>

Вот и все дела. А теперь напишем то же самое через веб-компонент:


class Adder extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: 'open' });

    this.shadowRoot.innerHTML = `
      <input type="number">
      <input type="number">
      <p></p>
    `;

    this.inputs = this.shadowRoot.querySelectorAll('input');
    this.p = this.shadowRoot.querySelector('p');

    this.update();

    this.inputs[0].addEventListener('input', e => {
      this.a = +e.target.value;
    });

    this.inputs[1].addEventListener('input', e => {
      this.b = +e.target.value;
    });
  }

  static get observedAttributes() {
    return ['a', 'b'];
  }

  get a() {
    return +this.getAttribute('a');
  }

  set a(value) {
    this.setAttribute('a', value);
  }

  get b() {
    return +this.getAttribute('b');
  }

  set b(value) {
    this.setAttribute('b', value);
  }

  attributeChangedCallback() {
    this.update();
  }

  update() {
    this.inputs[0].value = this.a;
    this.inputs[1].value = this.b;

    this.p.textContent = `${this.a} + ${this.b} = ${this.a + this.b}`;
  }
}

customElements.define('my-adder', Adder);

Да уж.


Заметьте, если мы синхронно изменим и a, и b, то у нас будут два отдельных обновления. Фреймворки в большинстве своем от этой проблемы не страдают.


9. Глобальные имена


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


10. Все эти проблемы уже решены


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


Поскольку наши ресурсы не бесконечны, время потраченное на одну задачу означает недостаток внимания другой задаче. Значительная энергия была потрачена на веб-компоненты, несмотря на общее безразличие разработчиков. Чего бы мы смогли достичь, потратив эту энергию на что-нибудь другое?

Комментарии (30)


  1. namikiri
    21.06.2019 07:24
    +3

    11. То, о чём все забывают. Производительность.
    Каждый веб-компонент, будучи создаваем с помощью JS, затрачивает много больше процессорного времени, чем построение обычного, стандартного элемента DOM.


    1. babylon
      21.06.2019 22:13
      -3

      Лично меня напрягают тормоза лейаута после ресайза. Впрочем на флеше было ненамного быстрее но там была песочница....


      1. Riim
        21.06.2019 22:42
        +1

        тормоза лейаута после ресайза

        если у вас лейаут считается в js-e, то это будет тормозить на любом фреймворке.


  1. vintage
    21.06.2019 13:30
    +1

    В платформу надо добавлять лишь то, что невозможно реализовать на уровне библиотек. Fibers, например.


    1. Riim
      22.06.2019 00:48

      Как с помощью библиотек реализовать элемент с изолированным css и не торчащими наружу кишками (ShadowDOM)?


      1. vintage
        22.06.2019 07:02
        +1

        Как на самосвале добраться из пункта А в пункт Б, не используя самосвал? Наверное никак.


        Неймспейсы и маглинг имён решают проблему конфликтов не хуже изоляции. Насчёт кишок я не понял о чём вы.


        1. Riim
          22.06.2019 11:38
          -2

          Насчёт кишок я не понял о чём вы

          я про внутренний dom компонента, зачем он мне торчащий наружу.


          Неймспейсы и маглинг имён решают проблему конфликтов не хуже изоляции.

          хуже, глобальные стили протекают внутрь компонента.


          Представь ситуацию: крупная компания, количество фронтенд-команд перевалило за десяток, каждая сама выбирает удобный ей фреймворк и базовый набор стилей. Можно, конечно, определить корпоративный стандарт, но это существенно усложнит поиск новых разработчиков. Кроме того проекты достаточно долгоживущие, фреймворки отмирают быстрее. При этом компания хочет свой корпоративный стиль, свои компоненты, отличающиеся от стандартных, часто не только цветом. Что делать? Разрабатывать и поддерживать библиотеки компонентов под каждый используемый фреймворк? Дороговато выходит.
          Или другая ситуация: ты работаешь в компании A и разрабытываешь библиотеку компонент на фреймворке X. А потом либо меняется компания A и в компании B используется фреймворк Y, либо фреймворк X отмирает и все хотят Y. Куча работы вылетает в трубу, а ведь класный набор компонентов получился, хотелось бы дальше применять.
          Веб-компоненты тут идеальное решение, ShadowDOM спрячет лишний внутренний dom, который теперь не будет мешать фреймворкам, глобальные стили не будут заставлять компонент расползаться, но, в то же время, есть хитрые css-селекторы позволяющие при необходимости что-то поменять внутри. Другими словами, получающиеся компоненты полностью автономны, так же как и уже встроенные в браузер input, select, video и тд. Из коробки веб-компоненты не очень удобны, но большинство существующих проблем решается легковесной обёрткой, опять же никак не мешающей существующим фреймворкам.


          1. vintage
            22.06.2019 16:17

            я про внутренний dom компонента, зачем он мне торчащий наружу.

            Где? Речь про вкладку Elements в девтулзах? Когда всё приложение построено на shadow-dow — вам всё равно придётся открывать кучу веток этих самых кишок. Только ещё и путаться без конца будете между этими деревьями.


            базовый набор стилей

            Не надо так.


            усложнит поиск новых разработчиков

            Не усложнит.


            каждая сама выбирает удобный ей фреймворк
            компания хочет свой корпоративный стиль

            Почему бы ей не захотеть не только единый стиль, но и единое поведение, и единую реализацию, и единый фреймворк?


            Разрабатывать и поддерживать библиотеки компонентов под каждый используемый фреймворк?

            Не использоваться фреймворки, которые сложно друг с другом интегрировать.


            класный набор компонентов получился, хотелось бы дальше применять

            Так применяйте, какие проблемы?


            есть хитрые css-селекторы позволяющие при необходимости что-то поменять внутри

            Это какие? Когда я последний раз смотрел — их все задепрекейтили.


            Из коробки веб-компоненты не очень удобны, но большинство существующих проблем решается легковесной обёрткой

            Ну конечно, и ленивый реактивный рендеринг к ним легко прикрутить?


            1. Riim
              22.06.2019 18:03
              +1

              вам всё равно придётся открывать кучу веток этих самых кишок

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


              Не использоваться фреймворки, которые сложно друг с другом интегрировать.

              ага, вот и я как раз про это: фреймворки на основе веб-компонентов максимально безболезненно интегрируются друг с другом. Причём это вполне естественно и не выглядит как совмещение единорога с китом. Сейчас интеграция заключается лишь в том, чтобы подружить полифилы, когда они станут не нужны, никакой интеграции вообще не нужно будет.


              Почему бы ей не захотеть не только единый стиль, но и единое поведение, и единую реализацию, и единый фреймворк?

              веб-компоненты с большой вероятностью проживут намного дольше, чем любой из существующих сегодня фреймворков, так зачем привязывать себя к какому-то из них, ждать когда он отомрёт и по новой переписывать каждую рюшечку? Если ui-библиотека сделана на веб-компонентах, то пусть разработчики использующие её пишут на чём им удобно.


              Ну конечно, и ленивый реактивный рендеринг к ним легко прикрутить?

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


              Не усложнит.

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


              Это какие? Когда я последний раз смотрел — их все задепрекейтили.

              ну видимо когда-то предложат что-то на замену или достаточно того, что осталось, я сейчас больше про общую идею, а не про текущую её реализацию, которая пока да, не идеальна. Сам я из веб-компонентов использую только CustomElements и HTMLTemplates. ShadowDOM полноценно не полифилится, а существующие недополифилы сильно жрут производительность. Поэтому я не особо в курсе что там с селекторами. Использую привычный БЭМ.


              о чём вы

              применяйте

              я себя лет на 20 старше чувствую)). Зачем вообще на хабре все выкают? В офисах и на конференциях все на ты, а здесь ощущение, как будто сплошные доктора наук собрались).


              1. vintage
                22.06.2019 20:31

                не нужно обычные въюхи в ShadowDOM запихивать

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


                не выглядит как совмещение единорога с китом

                Использование html для композиции компонент и костыли с аттрибутами/свойствами/слотами — это уже совмещение единорога с китом.


                ждать когда он отомрёт

                Ничего с ним не случится, если выбирать/создавать фреймворк исходя из потребностей, а не по принципу "что там сейчас в моде?". Если гнаться за модой, то сначала выбираются одни кривые технологии, а через пару лет они меняются на другие, не менее кривые.


                Подобное нужно реализовывать на уровне конкретного компонента

                Зачем что-то делать, если можно это не делать, а оставшееся время потратить на что-то более полезное?


                найти разработчика на какой-то конкретный фреймворк

                Не надо "искать на фреймворк". Толковый разработчик быстро освоит любой фреймворк. Бестолковый же и на своём любимом такого накреативит, что будете плакать кровавыми слезами.


                Например, насколько легко находить разработчиков на mol?

                Элементарно. Подойдёт любой знакомый с тайпскриптом. Пара недель и он уже контрибьютит во фреймворк. Прецеденты были.


                1. Riim
                  22.06.2019 21:44
                  +1

                  В общем, я свою точку зрения высказал, принимать что-то или нет — дело твоё. Дальше препираться смысла не вижу, по моему опыту это не приводит ни к чему кроме потери времени. Удачи!


    1. justboris Автор
      22.06.2019 15:00

      Раз уж на то пошло, а какую уникальную функциональность добавляют Fibers, которую не заменит async/await?


      1. vintage
        22.06.2019 15:58

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


        1. justboris Автор
          22.06.2019 17:31

          Не вижу здесь ничего такого, что невозможно реализовать в userland. Так что Fibers не вписываются под те же критерии, что вы применяете для ShadowDOM в треде выше.


          1. vintage
            22.06.2019 19:59

            Ок, как извне остановить асинхронную функцию? Как в обработчике, например, события click сделать асинхронный вызов после чего решить отменять ли дефолтное поведение?


            1. justboris Автор
              22.06.2019 20:36

              как извне остановить асинхронную функцию

              AbortController. Его можно не только с fetch использовать, но и для своего асинхронного кода


              Как в обработчике, например, события click сделать асинхронный вызов после чего решить отменять ли дефолтное поведение

              А что будет видеть пользователь пока мы выполняем наш запрос? Интерфейс будет заблокирован, ожидая ответа на событие.


              1. vintage
                23.06.2019 06:25

                AbortController.

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


                Интерфейс будет заблокирован, ожидая ответа на событие.

                В том-то и дело, что нет.


                1. justboris Автор
                  23.06.2019 12:51

                  только специально подготовленной.

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


                  Интерфейс будет заблокирован, ожидая ответа на событие.

                  В том-то и дело, что нет.

                  Мы навесили обработчик submit на форму, синхронно его не прервали, запрос ушел на сервер. В чем смысл его потом отменять, поздно же?


                  1. vintage
                    23.06.2019 16:27

                    код завершился

                    С файберами он не "завершается", а приостанавливается.


                    запрос ушел на сервер

                    Не ушёл, так как событие ещё не доплыло до корня документа.


  1. abramov231
    21.06.2019 16:23

    Без JS? В 2019 году? Вам не жалко своих пользователей, у которых не будет валидации форм, навигации без перезагрузки страницы и других давно привычных вещей?


    1. justboris Автор
      21.06.2019 16:26
      +5

      не будет валидации форм

      Будет, есть же встроенная валидация через html-атрибуты


      навигации без перезагрузки страницы

      Если страница мало весит и быстро загружается с сервера, то в чем проблема? Зачем чинить то что не сломано?


      1. YuriM1983
        21.06.2019 19:08

        Будет, есть же встроенная валидация через html-атрибуты
        А в Safari оно уже работает?


        1. justboris Автор
          21.06.2019 19:31

          Да. Сейчас сам проверил, валидация вызывается.


        1. sumanai
          21.06.2019 21:43

          Даже если не работает, то ничего не сломается, изящная деградация во все поля. В отличии от отключения JS на сайте, написанном только под его использование.


      1. Focushift
        21.06.2019 21:44
        +1

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


        1. sumanai
          21.06.2019 23:19
          +3

          Это проблема криво написанного бекенда. Уверяю, с таким подходом у них и рендеринг на стороне пользователя будет тормозить и жрать свой гигабайт оперативной памяти.


          1. Focushift
            22.06.2019 10:56
            -2

            Фронт и бэк пишут разные люди.
            Да, с текущими реактами и «модными» технологиями в нем, фронт будет тоже тормозить.
            Но! Я уже не буду 5 секунд ждать пока бэк сгенерирует несчастную страницу со списком.


            1. mayorovp
              22.06.2019 11:51
              +2

              Ну да, теперь вы будете ждать 10 секунд пока эту страницу вам сгенерирует фронт...


    1. sumanai
      21.06.2019 17:59
      +4

      навигации без перезагрузки страницы

      Порой голый HTML с толикой CSS и JS с нуля рендерит страницу быстрее, чем отработают все эти новомодные реакты.


    1. andreymal
      23.06.2019 15:38
      +1

      Что мешает сделать валидацию и навигацию без перезагрузки страницы опциональной плюшкой сбоку, которая при включенном JS будет работать, а при отключенном JS «изящно деградировать» в «классический» сайт?