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

Почему я не использую веб-компоненты

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

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

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

Это может быть старомодным убеждением, но я считаю, что веб-сайты должны работать без JavaScript насколько это возможно. Веб-компоненты без JS не работают. Это нормально для вещей, которые по своей природе интерактивные, такие как кастомные элементы форм (<cool-datepicker>), но это ненормально для навигации сайта, например. Или представьте себе компонент <twitter-share>, который инкапсулирует в себе логику построения URL для отправки в Twitter [3]. Я мог бы реализовать его на Svelte [4], что отрендерит на сервере мне вот такой 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 [5] а также Constructable Stylesheets [6], чтобы справиться с этой проблемой. Еще у нас будет возможность стилизовать внутренности Shadow DOM через ::theme и ::part. Но и здесь не обошлось без проблем.

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

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

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

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

4. Полифилы

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

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

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

<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 – плохой. Но все еще тяжело преувеличить, насколько это неудобный способ делать интерактивные приложения.

Несколько месяцев назад я написал статью "Пишите меньше кода" [12], призванную проиллюстрировать как 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 в компонентно-ориентированном стиле – уже решена несколько лет как. И мы все еще добавляем фичи в веб-платформу только для того чтобы догнать то, что мы уже имеем в библиотеках и фреймворках.

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

Автор: Борис Сердюк

Источник [13]


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

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

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

[1] Svelte: https://svelte.dev/

[2] Custom Elements Everywhere: https://custom-elements-everywhere.com/

[3] отправки в Twitter: https://developer.twitter.com/en/docs/twitter-for-websites/tweet-button/guides/web-intent.html

[4] реализовать его на Svelte: https://svelte.dev/repl/98aa20d4cb3d40dabfef7d8dae183b85?version=3.5.2

[5] CSS Modules: https://github.com/w3c/webcomponents/issues/759

[6] Constructable Stylesheets: https://developers.google.com/web/updates/2019/02/constructable-stylesheets

[7] June 19, 2019: https://twitter.com/Rich_Harris/status/1141404066704232448?ref_src=twsrc%5Etfw

[8] https://crbug.com: https://crbug.com

[9] спецификации: https://wicg.github.io/construct-stylesheets/

[10] имеет сомнения: https://github.com/mozilla/standards-positions/issues/103#issuecomment-494181931

[11] компонент <html-include>: https://github.com/justinfagnani/html-include-element

[12] "Пишите меньше кода": https://habr.com/en/post/451366/

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