Pull to refresh

Comments 14

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

Реактивность может быть реализована с помощью внешних библиотек, например, mobx.

Код под спойлером
const { observable, autorun, action } = mobx;

class ReactiveCounter extends HTMLElement {
    constructor() {
        super();
        // Создаём наблюдаемое состояние
        this.state = observable({
            count: 0,
        });

        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
      <div>
          <button id="decr">−</button>
          <span class="value" id="display">0</span>
          <button id="incr">+</button>
      </div>`;

        this.displaySpan = this.shadowRoot.getElementById('display');
        this.incrBtn = this.shadowRoot.getElementById('incr');
        this.decrBtn = this.shadowRoot.getElementById('decr');

        this.increment = action(() => {
            this.state.count++;
        });

        this.decrement = action(() => {
            this.state.count--;
        });

        this.incrBtn.addEventListener('click', this.increment);
        this.decrBtn.addEventListener('click', this.decrement);

        // Настраиваем реакцию: при изменении state.count обновляем текст
        this.disposeAutorun = autorun(() => {
            this.displaySpan.textContent = this.state.count;
        });
    }

    // При удалении компонента из DOM отписываемся от реакций
    disconnectedCallback() {
        if (this.disposeAutorun) {
            this.disposeAutorun();
        }
        this.incrBtn.removeEventListener('click', this.increment);
        this.decrBtn.removeEventListener('click', this.decrement);
    }
}

// Регистрируем кастомный элемент
customElements.define('reactive-counter', ReactiveCounter);

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

mobx неплох, это единственная альтернатива $mol'у в плане реактивности
но $mol далеко не только про реактивность

Не знаю почему вас удивил низкоуровневый код на js. Это база.

не удивил
привык к другой абстракции просто

Скрытый текст
namespace $.$$ {
	export class $bog_counter extends $.$bog_counter {

		@ $mol_mem
		count(next?: number) {
			return next ?? 0
		}

		count_str() {
			const val = this.count()
			if (val === 3) $mol_fail(new Error('Three is not allowed!'))
			return String(val)
		}

		incr() {
			this.count(this.count() + 1)
		}

		decr() {
			this.count(this.count() - 1)
		}

	}

	$mol_view_component($bog_counter)
}
$bog_counter $mol_view
	sub /
		<= Decr $mol_button_minor
			title \−
			click? <=> decr? null
		<= Value $mol_paragraph
			title <= count_str \0
		<= Incr $mol_button_minor
			title \+
			click? <=> incr? null

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

mobx тихо схавал ошибку, в результате пользователь видит
2, нажимает еще раз - видит снова 2 - нажимает еще раз - видит 4
что выглядит как баг

при этом ошибку если бы мы хотели исправить пришлось бы оборачиравать в try catch

Спасибо за публикацию! Полезно.

Добавлю.

Например, в такой наивной реализации свойства могут быть только строковыми.

Строковые, потому что вы setAttribute используете, что необязательно. А так, у классов веб-компонент возможности такие же, как и у обычных js-классов. Вот пример когда свойство - объект. Специально добавил connectedCallback, чтобы отобразить его в виде текстовой ноды.

      class MyFoo extends HTMLElement {
        connectedCallback() {
          this.textContent = this.bar.foo;
        }
        bar = { foo: 1337 };
      }

      globalThis.customElements.define("my-foo", MyFoo);
      const element = new MyFoo();
      console.log(element.bar.foo);
      // 1337


значит хорошие сапоги, надо брать(c)

Ну те придумали как всегда сферическую проблему и донкихотите?)

Зачем JSON хранить в атрибуте? Вас кто-то заставляет?

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

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

Веб-компоненты - это одно из самых ожидаемых и удачных нововведений (особенно с возможностью изоляции через shadowdom) в браузерах, и да, если пытаться не понимать их, и тыкать каменным топором, как вы привыкли - будет больно.

Я рассмотрел далеко не все проблемы веб-компонент

Вы рассмотрели лишь свое непонимание и старые подходы к новой технологии - не надо пинять на технологию, если руки кривые.

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

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

можем к примеру сделать две реализации и сравнить, сейчас это несложно с нейронками, как вам такой формат ?

Реальные аргументы про что? Про то, что в статье абсолютно надуманная дичь типа впихивания JSON в атрибуты? Для этого какие-то аргументы нужны, серьезно?

Что значит поддерживается руками? Это можно сделать автоматизировано без каких-либо проблем, в том числе и при сборке.

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

Внезапно использование внутри одного скопа одинаковых наименований приводит к конфликту наименований - как так?! Никогда такого ведь не было, и вот опять!

А если серьезно - а как должно было-бы быть? Браузер должен разруливать нейминг за программиста? )

Нейминг давно разрулен. MS это еще во времена СОМ сделала - все имена -это GUID. :)

Вы описываете ряд проблем Web Components как фундаментальные. Но многие из них уже закрыты стандартами платформы - с документацией на MDN и поддержкой браузеров.

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

Здесь смешаны атрибуты (HTML-разметка, всегда строки) и свойства (JS-рантайм, любые типы). Web Component - инстанс JS-класса. Сложные объекты передаются по ссылке через свойства:

document.querySelector('my-widget').data = { user: 'Habr', roles: [1, 2] };

Как пишет open-wc: “A great benefit of properties is that they can accept any javascript value, including complex objects and arrays”.

у нас же подражание HTML, а значит ничего, кроме строк и DOM элементов передать нельзя. Сам компонент должен взять откуда-то снаружи стайлшит и пропихнуть его в свой Shadow DOM через adoptedStyleSheets. Откуда и как — ну придумайте как-нибудь, чо как маленькие.

В компонент передается любой ссылочный тип, не только строки. Стандарт Constructable Stylesheets появился в Chrome в 2019-м, с марта 2023-го работает везде (Safari 16.4+). CSS парсится один раз, инстансам раздается ссылка на CSSStyleSheet:

const sheet = new CSSStyleSheet();
sheet.replaceSync('.btn { color: red }');
shadowRoot.adoptedStyleSheets = [sheet];

Документация: adoptedStyleSheets (MDN), Constructable Stylesheets (web.dev).

Любой перенос — это удаление и вставка, поэтому disconnectedCallback и затем connectedCallback… Будут каскадные вызовы этих колбэков на всём поддереве компонент… запуск сотен-тысяч этих задач на ровном месте — штука не бесплатная.

JS-инстанс при отрыве от DOM не уничтожается, стейт сохраняется. Повторную инициализацию можно предотвратить guard-флагом:

connectedCallback() {
  if (this.initialized) return;
  this.initialized = true;
}

А платформа уже пошла дальше: Element.moveBefore() и connectedMoveCallback() решают эту задачу нативно - перенос узла без вызова disconnectedCallback/connectedCallback. MDN прямо пишет: “You could add an empty connectedMoveCallback() to stop the other two callbacks running”. Поддержка: Chrome 133+, Firefox 144+ (Safari пока нет). Документация: Element.moveBefore() (MDN).

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

Scoped Custom Element Registries уже реализованы и документированы на MDN. Конструктор new CustomElementRegistry() позволяет создавать изолированные реестры, привязанные к конкретному Shadow Root:

const myRegistry = new CustomElementRegistry();
myRegistry.define('my-button', MyButton);
host.attachShadow({ mode: 'open', customElementRegistry: myRegistry });

Разные shadow-деревья могут использовать одинаковые имена без конфликтов. Причем первым эту фичу зашипил именно Safari (v26), Scoped Registries включены в программу Interop 2026 (Apple, Google, Microsoft, Mozilla), так что Firefox - вопрос времени. Поддержка: Chrome 146+, Safari 26+ (Firefox пока нет, есть полифил). Документация: Scoped registries (MDN).

если вставляешь сторонний компонент в свою страницу, то он должен выглядеть не в стиле твоего приложения, а в каком-то своём уникальном, превращая всё приложение в лоскутное одеяло

Платформа добавила CustomStateSet и CSS-псевдокласс :state(). Компонент экспортирует внутреннее состояние наружу, потребитель стилизует через обычный CSS:

labeled-checkbox:state(checked) { border: solid; }

Плюс ::part() для таргетирования конкретных частей Shadow DOM снаружи. Оба API поддерживаются всеми браузерами: ::part() с 2020 года, :state() - Chrome 90+, Firefox 126+, Safari 17.4+. Документация: CustomStateSet (MDN), ::part() (MDN).

Реальные ограничения у Web Components есть (работа с формами, например), но основные пункты перечисленные в вашей статье платформа уже закрыла.

Продолжу разбор остальных утверждений из статьи.

В качестве примера бойлерплейта приводится сырой HTMLElement, из чего делается вывод о переусложненности всего стандарта. Но это все равно что судить о разработке интерфейсов на чистом JS по document.createElement и ручной вставке нод в DOM - так уже давно никто не пишет.

Текущие инструменты вроде Lit решают проблему рутинного кода, добавляя реактивность и работу с шаблонами в очень тонкой обертке (порядка 6KB gzip). А новые декораторы из спецификации TC39 (закреплены в Stage 3, базовая поддержка в движках стартовала в 2024 году) позволяют убрать лишний код вообще на уровне самого языка:

class MyWidget extends LitElement {
  @property() name = '';
  render() { return html`<p>Hello, ${this.name}</p>`; }
}

Бойлерплейт raw API - не аргумент против технологии, иначе raw WebGL “доказывает”, что 3D в браузере невозможен.

создание веб-компонента на 3 порядка медленнее обычного JS-объекта… 124 байта против 16

Методологически странно сравнивать DOM-элемент с plain JS-объектом. С тем же успехом можно сказать, что document.createElement('div') “на порядки медленнее” пустого {} - это не баг <div>, а природа работы с DOM.

Корректное сравнение - Web Component против компонента React или Vue. Абстракции фреймворков (виртуальное дерево, замыкания хуков) вместе с реальным DOM-элементом занимают сопоставимый объем памяти, просто затраты спрятаны под капотом. В эталонном бенчмарке js-framework-benchmark реализация на Lit стабильно идет рядом с React и Vue по скорости рендеринга.

Что касается статической типизации, о которой “можно не мечтать” (аргумент про нетипизированные строки) - это верно только для HTML-атрибутов. Свойства Web Component - это обычные поля ES-класса. Они типизируются ровно так же, как любой другой TypeScript-код, и работа с компонентами из JS идет именно через свойства:

class MyWidget extends HTMLElement {
  data: UserProfile | null = null;
  set count(v: number) { /* ... */ }
}

Инструменты вроде Stencil вообще компилируют .tsx в нативные Web Components с полными типами.

Наконец, называть “мертворожденным” и “не снискавшим популярности” стандарт, на котором работает почти весь фронтенд GitHub (на базе Catalyst), интерфейсы YouTube, Spectrum Web Components от Adobe, Salesforce (Lightning Web Components) и даже дашборды SpaceX Crew Dragon - довольно смело. Если это критерии “мертворожденности”, то непонятно, какой тогда должен быть порог успешности технологии.

Вы описываете ряд проблем Web Components как фундаментальные. Но многие из них уже закрыты стандартами платформы - с документацией на MDN и поддержкой браузеров. Пройдемся по конкретным пунктам.

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

Здесь смешаны атрибуты (HTML-разметка, всегда строки) и свойства (JS-рантайм, любые типы). Web Component - инстанс JS-класса. Сложные объекты передаются по ссылке через свойства:

document.querySelector('my-widget').data = { user: 'Habr', roles: [1, 2] };

Как пишет open-wc: “A great benefit of properties is that they can accept any javascript value, including complex objects and arrays”.

у нас же подражание HTML, а значит ничего, кроме строк и DOM элементов передать нельзя. Сам компонент должен взять откуда-то снаружи стайлшит и пропихнуть его в свой Shadow DOM через adoptedStyleSheets. Откуда и как — ну придумайте как-нибудь, чо как маленькие.

В компонент передается любой ссылочный тип, не только строки. Стандарт Constructable Stylesheets появился в Chrome в 2019-м, с марта 2023-го работает везде (Safari 16.4+). CSS парсится один раз, инстансам раздается ссылка на CSSStyleSheet:

const sheet = new CSSStyleSheet();
sheet.replaceSync('.btn { color: red }');
shadowRoot.adoptedStyleSheets = [sheet];

Документация: adoptedStyleSheets (MDN), Constructable Stylesheets (web.dev).

Любой перенос — это удаление и вставка, поэтому disconnectedCallback и затем connectedCallback… Будут каскадные вызовы этих колбэков на всём поддереве компонент… запуск сотен-тысяч этих задач на ровном месте — штука не бесплатная.

JS-инстанс при отрыве от DOM не уничтожается, стейт сохраняется. Повторную инициализацию можно предотвратить guard-флагом:

connectedCallback() {
  if (this.initialized) return;
  this.initialized = true;
}

А платформа уже пошла дальше: Element.moveBefore() и connectedMoveCallback() решают эту задачу нативно - перенос узла без вызова disconnectedCallback/connectedCallback. MDN прямо пишет: “You could add an empty connectedMoveCallback() to stop the other two callbacks running”. Поддержка: Chrome 133+, Firefox 144+ (Safari пока нет). Документация: Element.moveBefore() (MDN).

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

Scoped Custom Element Registries уже реализованы и документированы на MDN. Конструктор new CustomElementRegistry() позволяет создавать изолированные реестры, привязанные к конкретному Shadow Root:

const myRegistry = new CustomElementRegistry();
myRegistry.define('my-button', MyButton);
host.attachShadow({ mode: 'open', customElementRegistry: myRegistry });

Разные shadow-деревья могут использовать одинаковые имена без конфликтов. Причем первым эту фичу зашипил именно Safari (v26) - исторически самый консервативный из браузеров. Scoped Registries включены в программу Interop 2026 (Apple, Google, Microsoft, Mozilla), так что Firefox - вопрос времени. Поддержка: Chrome 146+, Safari 26+ (Firefox пока нет, есть полифил). Документация: Scoped registries (MDN).

если вставляешь сторонний компонент в свою страницу, то он должен выглядеть не в стиле твоего приложения, а в каком-то своём уникальном, превращая всё приложение в лоскутное одеяло

Платформа добавила CustomStateSet и CSS-псевдокласс :state(). Компонент экспортирует внутреннее состояние наружу, потребитель стилизует через обычный CSS:

labeled-checkbox:state(checked) { border: solid; }

Плюс ::part() для таргетирования конкретных частей Shadow DOM снаружи. Оба API поддерживаются всеми браузерами: ::part() с 2020 года (96%+ пользователей), :state() - Chrome 90+, Firefox 126+, Safari 17.4+. Документация: CustomStateSet (MDN), ::part() (MDN).

Реальные ограничения у Web Components есть (работа с формами, например), но перечисленные в вашей статье пункты платформа уже закрыла.

Sign up to leave a comment.

Articles