Digital Leadership & Soft Skills

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

Опубликовано by Pavel Nakonechnyy on в Web development.

Перевод статьи by richharris

10 маленьких (и не очень) причин, почему вам не стоит использовать web component (по крайней мере пока) в своем проекте.

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

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

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

И это нормально для вещей, которые являются интерактивными, например кастомные элементы форм (<cool-datepicker>), но это ненормально для вашего navbar'а. Или рассмотрите простой элемент <twitter-share>, который инкапсулирует всю логику для кнопки поделиться. Если бы я делал его в Svelte, то он бы генерировал на сервере HTML вроде такого:

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

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

Со включенным JavaScript оно подвержено "прогрессивному улучшению". Вместо создания новой вкладки, оно откроет небольшое попап окно. Но и без JS, оно всё еще работает.

С другой стороны, html для web компонента выглядел бы как-то так:

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

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

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

CSS в, брр, JavaScript

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

FOUC (Flash of unstyled content) — кратковременное появление html-содержимого с дефолтными стилями браузера до загрузки CSS страницы. — примечание переводчика

И это идет вразрез с советом по оптимизации, который звучит как "меньше JavaScript, пожалуйста". Технология CSS-in-JS подверглась критике в том числе из-за того, что подразумевает отсутствие css в .css файлах, от чего мы никуда не ушли.

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

Подпишитесь на нашу группу ВК

Болезнь платформ

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

Каждый раз, когда мы добавляем функцию в платформу, мы увеличиваем эту сложность — создавая новую плоскость для багов, и делая всё менее вероятным появление конкурента Chromium.

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

Полифилы (Polyfills)

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

Композиция

Компоненту полезно иметь возможность контролировать когда его контент отрисовывается. Допустим, что мы хотим использовать <html-include>, чтобы показать некоторую документацию из сети, только когда она станет видимой.

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

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

Это вызвано тем, что скрытый контент отрисовывается предварительно в custom elements. И тут выясняется, что в большинстве случаев вы хотите, чтобы скрытый контент отрисовывался лениво (lazy). Svelte v2 использует eager отрисовку, чтобы соответствовать Web стандартом, и эта модель стала крупным источником фрустрации — мы не могли сделать эквивалент React Router, например. В Svelte v3 мы отказались от модели композиции custom element и не пожалели об этом.

Увы, это только фундаментальные особенности DOM, что приводит нас к…

Путаница между props и attributes

Props и attributes — почти одинаковые вещи, верно?

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 вызывает методы свойств. В любом случае, вид ужасный.

Фреймворки, например, имеют простой и однозначный способ ввести данные в компонент.

Спорный дизайн

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

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

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

DOM — это плохо

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

Пару месяцев назад я написал статью "Write less code", чтобы продемонстрировать, как 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 одновременно, то это вызовет две перерисовки. Фреймворки обычно не страдают от этой проблемы.

Еще у нас есть канал telegram

Глобальное пространство имен

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

Это всё решенные проблемы

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

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

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

505