Я пишу это в основном для себя в будущем, чтобы у меня было куда сослаться, когда кто-нибудь спросит меня, почему я скептичен в отношении веб-компонентов и почему 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)
vintage
21.06.2019 13:30+1В платформу надо добавлять лишь то, что невозможно реализовать на уровне библиотек. Fibers, например.
Riim
22.06.2019 00:48Как с помощью библиотек реализовать элемент с изолированным css и не торчащими наружу кишками (ShadowDOM)?
vintage
22.06.2019 07:02+1Как на самосвале добраться из пункта А в пункт Б, не используя самосвал? Наверное никак.
Неймспейсы и маглинг имён решают проблему конфликтов не хуже изоляции. Насчёт кишок я не понял о чём вы.
Riim
22.06.2019 11:38-2Насчёт кишок я не понял о чём вы
я про внутренний dom компонента, зачем он мне торчащий наружу.
Неймспейсы и маглинг имён решают проблему конфликтов не хуже изоляции.
хуже, глобальные стили протекают внутрь компонента.
Представь ситуацию: крупная компания, количество фронтенд-команд перевалило за десяток, каждая сама выбирает удобный ей фреймворк и базовый набор стилей. Можно, конечно, определить корпоративный стандарт, но это существенно усложнит поиск новых разработчиков. Кроме того проекты достаточно долгоживущие, фреймворки отмирают быстрее. При этом компания хочет свой корпоративный стиль, свои компоненты, отличающиеся от стандартных, часто не только цветом. Что делать? Разрабатывать и поддерживать библиотеки компонентов под каждый используемый фреймворк? Дороговато выходит.
Или другая ситуация: ты работаешь в компании A и разрабытываешь библиотеку компонент на фреймворке X. А потом либо меняется компания A и в компании B используется фреймворк Y, либо фреймворк X отмирает и все хотят Y. Куча работы вылетает в трубу, а ведь класный набор компонентов получился, хотелось бы дальше применять.
Веб-компоненты тут идеальное решение, ShadowDOM спрячет лишний внутренний dom, который теперь не будет мешать фреймворкам, глобальные стили не будут заставлять компонент расползаться, но, в то же время, есть хитрые css-селекторы позволяющие при необходимости что-то поменять внутри. Другими словами, получающиеся компоненты полностью автономны, так же как и уже встроенные в браузер input, select, video и тд. Из коробки веб-компоненты не очень удобны, но большинство существующих проблем решается легковесной обёрткой, опять же никак не мешающей существующим фреймворкам.vintage
22.06.2019 16:17я про внутренний dom компонента, зачем он мне торчащий наружу.
Где? Речь про вкладку Elements в девтулзах? Когда всё приложение построено на shadow-dow — вам всё равно придётся открывать кучу веток этих самых кишок. Только ещё и путаться без конца будете между этими деревьями.
базовый набор стилей
Не надо так.
усложнит поиск новых разработчиков
Не усложнит.
каждая сама выбирает удобный ей фреймворк
компания хочет свой корпоративный стильПочему бы ей не захотеть не только единый стиль, но и единое поведение, и единую реализацию, и единый фреймворк?
Разрабатывать и поддерживать библиотеки компонентов под каждый используемый фреймворк?
Не использоваться фреймворки, которые сложно друг с другом интегрировать.
класный набор компонентов получился, хотелось бы дальше применять
Так применяйте, какие проблемы?
есть хитрые css-селекторы позволяющие при необходимости что-то поменять внутри
Это какие? Когда я последний раз смотрел — их все задепрекейтили.
Из коробки веб-компоненты не очень удобны, но большинство существующих проблем решается легковесной обёрткой
Ну конечно, и ленивый реактивный рендеринг к ним легко прикрутить?
Riim
22.06.2019 18:03+1вам всё равно придётся открывать кучу веток этих самых кишок
не прийдётся. Не обязательно и может даже не нужно обычные въюхи в ShadowDOM запихивать, он обязательно используется только для компонент вроде селекта, табов и подобного. Но кишки мешают больше не мне в devtools, а как раз при использовании таких компонентов в других фреймворках самим этим фреймворкам.
Не использоваться фреймворки, которые сложно друг с другом интегрировать.
ага, вот и я как раз про это: фреймворки на основе веб-компонентов максимально безболезненно интегрируются друг с другом. Причём это вполне естественно и не выглядит как совмещение единорога с китом. Сейчас интеграция заключается лишь в том, чтобы подружить полифилы, когда они станут не нужны, никакой интеграции вообще не нужно будет.
Почему бы ей не захотеть не только единый стиль, но и единое поведение, и единую реализацию, и единый фреймворк?
веб-компоненты с большой вероятностью проживут намного дольше, чем любой из существующих сегодня фреймворков, так зачем привязывать себя к какому-то из них, ждать когда он отомрёт и по новой переписывать каждую рюшечку? Если ui-библиотека сделана на веб-компонентах, то пусть разработчики использующие её пишут на чём им удобно.
Ну конечно, и ленивый реактивный рендеринг к ним легко прикрутить?
если ты про то, что реализовано в mol, то меня это не впечатлило, я против реализации чего-то подобного на уровне фреймворка. По крайней мере твои аргументы меня совсем не убедили. Подобное нужно реализовывать на уровне конкретного компонента, например, строки датагрида.
Не усложнит.
давай с аргументацией, окей? Почему ты думаешь, что найти разработчика на какой-то конкретный фреймворк так же легко как на любой из популярных на выбор кандидата? Например, насколько легко находить разработчиков на mol?
Это какие? Когда я последний раз смотрел — их все задепрекейтили.
ну видимо когда-то предложат что-то на замену или достаточно того, что осталось, я сейчас больше про общую идею, а не про текущую её реализацию, которая пока да, не идеальна. Сам я из веб-компонентов использую только CustomElements и HTMLTemplates. ShadowDOM полноценно не полифилится, а существующие недополифилы сильно жрут производительность. Поэтому я не особо в курсе что там с селекторами. Использую привычный БЭМ.
о чём вы
применяйтея себя лет на 20 старше чувствую)). Зачем вообще на хабре все выкают? В офисах и на конференциях все на ты, а здесь ощущение, как будто сплошные доктора наук собрались).
vintage
22.06.2019 20:31не нужно обычные въюхи в ShadowDOM запихивать
Похоже вы проспали бум компонентной декомпозиции, позволяющей уменьшить копипасту и сложность этих самых "простых" вьюх.
не выглядит как совмещение единорога с китом
Использование html для композиции компонент и костыли с аттрибутами/свойствами/слотами — это уже совмещение единорога с китом.
ждать когда он отомрёт
Ничего с ним не случится, если выбирать/создавать фреймворк исходя из потребностей, а не по принципу "что там сейчас в моде?". Если гнаться за модой, то сначала выбираются одни кривые технологии, а через пару лет они меняются на другие, не менее кривые.
Подобное нужно реализовывать на уровне конкретного компонента
Зачем что-то делать, если можно это не делать, а оставшееся время потратить на что-то более полезное?
найти разработчика на какой-то конкретный фреймворк
Не надо "искать на фреймворк". Толковый разработчик быстро освоит любой фреймворк. Бестолковый же и на своём любимом такого накреативит, что будете плакать кровавыми слезами.
Например, насколько легко находить разработчиков на mol?
Элементарно. Подойдёт любой знакомый с тайпскриптом. Пара недель и он уже контрибьютит во фреймворк. Прецеденты были.
Riim
22.06.2019 21:44+1В общем, я свою точку зрения высказал, принимать что-то или нет — дело твоё. Дальше препираться смысла не вижу, по моему опыту это не приводит ни к чему кроме потери времени. Удачи!
justboris Автор
22.06.2019 15:00Раз уж на то пошло, а какую уникальную функциональность добавляют Fibers, которую не заменит async/await?
vintage
22.06.2019 15:58Возможность приостанавливать исполнение в любых местах, а не только в асинхронных функциях. Обработчики событий, колбэки стандартных методов, вот это всё. Опциональная асинхронность. Преждевременная остановка исполнения.
justboris Автор
22.06.2019 17:31Не вижу здесь ничего такого, что невозможно реализовать в userland. Так что Fibers не вписываются под те же критерии, что вы применяете для ShadowDOM в треде выше.
vintage
22.06.2019 19:59Ок, как извне остановить асинхронную функцию? Как в обработчике, например, события click сделать асинхронный вызов после чего решить отменять ли дефолтное поведение?
justboris Автор
22.06.2019 20:36как извне остановить асинхронную функцию
AbortController. Его можно не только с fetch использовать, но и для своего асинхронного кода
Как в обработчике, например, события click сделать асинхронный вызов после чего решить отменять ли дефолтное поведение
А что будет видеть пользователь пока мы выполняем наш запрос? Интерфейс будет заблокирован, ожидая ответа на событие.
vintage
23.06.2019 06:25AbortController.
Этот костыль с ручным приводом нельзя использовать с любой асинхронной функцией — только специально подготовленной.
Интерфейс будет заблокирован, ожидая ответа на событие.
В том-то и дело, что нет.
justboris Автор
23.06.2019 12:51только специально подготовленной.
Если разрешить прерываться в произвольном месте, то будут происходить утечки, потому что код завершился нештатно. В любом случае функцию нужно подготовить к внезапному прерыванию.
Интерфейс будет заблокирован, ожидая ответа на событие.
В том-то и дело, что нет.Мы навесили обработчик submit на форму, синхронно его не прервали, запрос ушел на сервер. В чем смысл его потом отменять, поздно же?
vintage
23.06.2019 16:27код завершился
С файберами он не "завершается", а приостанавливается.
запрос ушел на сервер
Не ушёл, так как событие ещё не доплыло до корня документа.
abramov231
21.06.2019 16:23Без JS? В 2019 году? Вам не жалко своих пользователей, у которых не будет валидации форм, навигации без перезагрузки страницы и других давно привычных вещей?
justboris Автор
21.06.2019 16:26+5не будет валидации форм
Будет, есть же встроенная валидация через html-атрибуты
навигации без перезагрузки страницы
Если страница мало весит и быстро загружается с сервера, то в чем проблема? Зачем чинить то что не сломано?
Focushift
21.06.2019 21:44+1У моего банка клиент сделан на генерируемых сервером страницах, поэтому когда я в 5й раз открываю страницу со списком шаблонов платежей, я должен ждать несколько секунд пока сервер сгенерирует мне эту несчастную страницу, так что лучше уж JS и рендеринг страниц на стороне клиента с кешированием списков данных.
sumanai
21.06.2019 23:19+3Это проблема криво написанного бекенда. Уверяю, с таким подходом у них и рендеринг на стороне пользователя будет тормозить и жрать свой гигабайт оперативной памяти.
Focushift
22.06.2019 10:56-2Фронт и бэк пишут разные люди.
Да, с текущими реактами и «модными» технологиями в нем, фронт будет тоже тормозить.
Но! Я уже не буду 5 секунд ждать пока бэк сгенерирует несчастную страницу со списком.mayorovp
22.06.2019 11:51+2Ну да, теперь вы будете ждать 10 секунд пока эту страницу вам сгенерирует фронт...
sumanai
21.06.2019 17:59+4навигации без перезагрузки страницы
Порой голый HTML с толикой CSS и JS с нуля рендерит страницу быстрее, чем отработают все эти новомодные реакты.
andreymal
23.06.2019 15:38+1Что мешает сделать валидацию и навигацию без перезагрузки страницы опциональной плюшкой сбоку, которая при включенном JS будет работать, а при отключенном JS «изящно деградировать» в «классический» сайт?
namikiri
11. То, о чём все забывают. Производительность.
Каждый веб-компонент, будучи создаваем с помощью JS, затрачивает много больше процессорного времени, чем построение обычного, стандартного элемента DOM.
babylon
Лично меня напрягают тормоза лейаута после ресайза. Впрочем на флеше было ненамного быстрее но там была песочница....
Riim
если у вас лейаут считается в js-e, то это будет тормозить на любом фреймворке.