Хабрахабр

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

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

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

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

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

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

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

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

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

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

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

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

2. CSS в, эээ… JS

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

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

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

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

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

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

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

4. Полифилы

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

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

Что приводит нас к... К сожалению, это была одна из фундаментальных характеристик 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 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 – плохой.

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

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

Кнопка «Наверх»
Закрыть