Просто хочу строить свой DOM из своих кирпичей.
С преферансом и поэтессами...
И, если уж на то пошло, может быть что‑то типа: «раз пошла такая пъянка...»

Думаю некторые понимают, что так можно, но — повторение мать учения, и, то есть, никто не мешает и не мешал делать не так, как все привыкли, не брать чей‑то готовый код, и не оставаться в рамках ограничений, наложенных кем‑то на что‑то «просто потому что».

Что мне это даст:

  • мне больше не нужны подписки, могу просто знать, что какое‑то свойство изменилось

  • могу устраивать коммуникации с нодами на своё усмотрение

  • стандартную логику вообще не трогаю и не мешаю ей

Как? Возьму Proxy и буду оборачивать в него всё, что необходимо.

Итого, сначала создадим «базу» для нашего элемента:

const { HTMLElement } = window;

class MyHTMLElement extends HTMLElement {}

customElements.define('my-custom-element', MyHTMLElement);
const myElement = document.createElement('my-custom-element');

console.log('myElement instanceof MyHTMLElement',
    myElement instanceof MyHTMLElement);   // true
console.log('myElement instanceof HTMLElement',
    myElement instanceof HTMLElement);      // true

Да, тут штука в том, что нужно использовать customElements.define и без него никак нельзя, так как оно, в целом, как бы запрещено, без этого мы получим ошибку:

TypeError: Failed to construct 'HTMLElement': Illegal construct

Но в целом нас это никак особо не напрягает, поэтмоу продолжим.
Теперь можно добавить Proxy для отслеживания обращений к свойствам.

const { HTMLElement } = window;

class MyHTMLElement extends HTMLElement {}

const protoProps = {};
Object.setPrototypeOf(protoProps, HTMLElement.prototype);

const proxy = new Proxy(protoProps, {
    get(target, prop, receiver) {
        const result = Reflect.get(target, prop, receiver);
        console.log('get:', prop, result);
        return result;
    },
    set(target, prop, value, receiver) {
        const result = Reflect.set(target, prop, value, receiver);
        console.log('set:', prop, result, value);
        return result;
    },
});


Object.setPrototypeOf(MyHTMLElement.prototype, proxy);

customElements.define('my-custom-element', MyHTMLElement);
const myElement = document.createElement('my-custom-element');

console.log('myElement instanceof MyHTMLElement',
    myElement instanceof MyHTMLElement);
console.log('myElement instanceof HTMLElement',
    myElement instanceof HTMLElement);

Теперь убедимся, что всё работает как задумано, создадим простой HTML и добавим необходимую обвязку.

index.html :

<html>

<head>
    <style>
        #some {
            padding: auto;
            text-align: center;
            border: 1px solid red;
            min-height: 100px;
            font-size: 7vh;
        }
    </style>
</head>

<body bgcolor="white">
    <div id="some"></div>
    <script src="MyHTMLElement.js"></script>
</body>

</html>

MyHTMLElement.js :


const { HTMLElement } = window;

class MyHTMLElement extends HTMLElement {}

const protoProps = {};
Object.setPrototypeOf(protoProps, HTMLElement.prototype);

const proxy = new Proxy(protoProps, {
    get(target, prop, receiver) {
        const result = Reflect.get(target, prop, receiver);
        console.log('get:', prop, result);
        return result;
    },
    set(target, prop, value, receiver) {
        const result = Reflect.set(target, prop, value, receiver);
        console.log('set:', prop, result, value);
        return result;
    },
});


Object.setPrototypeOf(MyHTMLElement.prototype, proxy);

customElements.define('my-custom-element', MyHTMLElement);
const myElement = document.createElement('my-custom-element');

console.log('myElement instanceof MyHTMLElement',
    myElement instanceof MyHTMLElement);
console.log('myElement instanceof HTMLElement',
    myElement instanceof HTMLElement);

console.log('render begins');
myElement.innerText = 123;
const renderBox = document.getElementById('some');
renderBox.appendChild(myElement);
console.log('render finish');

Теперь в консоли "видно всё" :

Конечно, теперь нам никто не запрещает играться с ним так, как нам вздумается, например добавить в нехо собственных свойств и наладить собственный прямой канал общения для внешних коммуникаций:


const { HTMLElement } = window;

class MyHTMLElement extends HTMLElement {

    communicate(value) {
        this.innerHTML = `${this.protoAddition} + ${this.addition} + ${value}`;
    }

    addition = 'addition';
  
}

const protoProps = {
    protoAddition: 'protoAddition',
};
Object.setPrototypeOf(protoProps, HTMLElement.prototype);

const proxy = new Proxy(protoProps, {
    get(target, prop, receiver) {
        const result = Reflect.get(target, prop, receiver);
        console.log('get:', prop, result);
        return result;
    },
    set(target, prop, value, receiver) {
        const result = Reflect.set(target, prop, value, receiver);
        console.log('set:', prop, result, value);
        return result;
    },
});


Object.setPrototypeOf(MyHTMLElement.prototype, proxy);

customElements.define('my-custom-element', MyHTMLElement);
const myElement = document.createElement('my-custom-element');

console.log('myElement instanceof MyHTMLElement',
    myElement instanceof MyHTMLElement);
console.log('myElement instanceof HTMLElement',
    myElement instanceof HTMLElement);

console.log('render begins');
myElement.innerText = 123;          // set  
const renderBox = document.getElementById('some');
renderBox.appendChild(myElement);
console.log('render finish');

myElement.communicate('message');

console.log(myElement.innerText);   // get

Если нужна ссылка на Gist, то Держите .

Надеюсь, что теперь код различных Front-End библиотек будет менее пугающим и загадочным, нет никакой магии, всё банально и не очень сложно.

Конечно, есть и другие API со схожим поведением, Listeners, Observables и т.п. Есть преимущества, есть недостатки. Но если хочется чего-то "простого как топор", то вот.

Спасибо за внимание :-)

Комментарии (16)


  1. zababurin
    22.08.2025 09:33

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


    1. wentout Автор
      22.08.2025 09:33

      Да, с React Component тоже самое можно провернуть )


      1. zababurin
        22.08.2025 09:33

        Ну в реакте это лишнее. Там как правило это стейт менеджер какой нибудь. Да и в целом виртуальное дерево это заменяет.

        Реакт это уже фреймворк(знаю что это библиотека) а здесь ты сам создаешь то что требуется. Можно и реакт свой условно собрать. Я хуки(не помню как это называется useEffect useState) на компонентах делал как то.


        1. wentout Автор
          22.08.2025 09:33

          Да, там достаточно просто от компонента унаследоваться, и вобоще не иметь дело с остальной их обвязкой с тими props и т.п., если хочется просто как шаблонизатор его использовать чтобы "на коленке" велосипед собрать )


          1. zababurin
            22.08.2025 09:33

            Я бы сказал, можно структуру проекта не учитывать вычеркнув её из проекта. Я ни одного такого велосипеда не видел.


            1. wentout Автор
              22.08.2025 09:33

              Проект, в котором только свои собственные тёпленькие компоненты + немного спагетти-макаронной логики для обвязки. Пишется быстро, разбираться потом невозможно. Но нужно ли )


              1. zababurin
                22.08.2025 09:33

                Проект, в котором только свои собственные тёпленькие компоненты + немного спагетти-макаронной логики для обвязки. Пишется быстро, разбираться потом невозможно. Но нужно ли )

                Здесь нет ни одного правильного слова.

                1. Макаронной логики для обвязки не надо.- в этом и фишка компонентов.
                2. Пишется быстро, - Придумать сложно.
                3.  разбираться потом невозможно - 1 файл 400-500 строчек кода элементарного кода + это стандарт html который вы знать должны (вы React и остальные 100 500 фреймворков можете не знать ).. А вы точно программист ? Сказать что это сложно это сказать я не знаю html это для меня сложно.
                https://developer.mozilla.org/ru/docs/Web/API/Web_components/Using_custom_elements
                4. Но нужно ли. - Гарантия что я могу собрать проект за 2 недели определенной сложности мне нравится. Вам не знаю.


                1. wentout Автор
                  22.08.2025 09:33

                  Предыдущй мой коментарий -- это сарказм )

                  В API Custom Elements ни слова про Proxy.
                  API для отслеживания свойств в DOM совсем другое, и к JS имеет такое же отношение, как customElements.define , мне всё же хотелось в примере быть ближе именно к JS.

                  А вообще я Back-End пишу на Node.js, в браузер смотрю для чтения статей, решил, вот написать что-то, что может быть кому-то интересно, т.к. прикладные навыки с Front-End остались в эпохе jQuery.


                  1. zababurin
                    22.08.2025 09:33

                    А я про   Апи ничего и не писал... Я писал. При чем здесь оно ?... Я сервер написал пару месяцев назад, все руки не доходят его подключить. Слава богу о нем никто и не вспоминает пока ))


                    1. wentout Автор
                      22.08.2025 09:33

                      эмм... наверное потому, что без customElements.define не получится сделать, т.к. нужно зарегистрировать кастомный HTML элемент, иначе придётся модифицировать базовые, а это не то, чтобы вообще правильно, не знаю, может попробую когда-нибудь поэксперементировать, вообще, скорее всего, можно и их модифицировать, там нет рекурсии, конечное количество глубины, просто, вот это точно громадный impact по производительности будет, но если там нет запретов, то можно порвать и эти цепочки

                      Современный Front-End где нужно знать 100500 библиотек и фреймворков только чтобы страничку отрендерить -- это сложно, переусложнено. Нужно было как-то на реакт что-то сделать очень простое. С .props было лень разбираться, со стейтами и т.п., прям лень. Добавил общую зависимость, подписался на неё и вызывал .render по мере необходимости. Коряво, но элементарно, уложился в пару часов. Но вообще да, всё браузерное API выучить, видимо, уже практически не реально, разве что если писать это самое API со стороны писателей API или стандартов, то есть когда это работа. Видимо поэтому все библиотеки и плодятся, упрощая работу в какой-то своей привычной авторам "атмосфере".
                      А так Custom Elements -- это тоже своего рода API -- то есть Application Programming Interface )


                      1. zababurin
                        22.08.2025 09:33


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


      1. ooko
        22.08.2025 09:33

        В реакт убили компонентный поход. Теперь там за все отвечает родитель, а на практике глобал)


  1. nihil-pro
    22.08.2025 09:33

    Только зачем Proxy, если у HTMLElement уже есть методы setAttribute, removeAttributе которые можно переопределять + есть attributeChangedCallbak + MutationObserver чтобы отслеживать вложенные элементы? HTMLElement это очень большой объект, и его проксирование это очень дорогая операция.


    1. wentout Автор
      22.08.2025 09:33

      Да, просто если хочется видеть сами вызовы .setAttribute и .removeAttributе и т.п., то можно и так, думаю. Про размер объекта -- не уверен, что это как-то влияет, объект --это же всего лишь указатель, поэтому какая разница что там "под" прокси. Количество свойств, безусловно влияет если мы хотим их "перебирать", но они же все в этом самом объекте, то есть хешированы и доступ к ним "атомарный". Другое дело, что в самом деле само проксирование -- дорогая операция + поиск свойств вглубину же будет, и чем на более низжем уровне они лежат, тем дольше. А так -- да, конечно, спасибо за дополнение!


      1. nihil-pro
        22.08.2025 09:33

        Да, просто если хочется видеть сами вызовы .setAttribute и .removeAttributе и т.п., то можно и так

        А что еще вы хотите видеть, а самое главное зачем?

        Класс HTMLElement наследует класс Element. И в одном и в другом примерно два-три свойства, все остальное пара getter/setter (за исключением readonly свойств, там только getter), следовательно у вас уже есть механизм перехвата чтения/записи без необходимости проксировать

        class MyHTMLElement extends HTMLElement {
          get innerText() {
            // кто-то прочитал innerText
            return super.innerText
          }
        
          set innerText(value) {
            // кто-то хочет зменить innerText
            return super.innerText = value;
          }
        }

        Конечно браузеры не могут полностью защитится от таких попыток влезть туда, куда не стоит, но в этом случае они довольно строго предупреждают:

        class MyHTMLElement extends HTMLElement {
          constructor() {
            super();
            return new Proxy(this, {});
          }
        }
        TypeError: custom element constructors must call super() first and must not return a different object
        
        TypeError: Failed to execute 'createElement' on 'Document': The result must implement HTMLElement interface

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


        1. wentout Автор
          22.08.2025 09:33

          Думаю не представляет никакого интереса. Уверен они точно знают, что так можно и в этом нет ничего такого уж необычного. Выйти "за границы" не получится, оно всё равно в рамках JavaScript останется, а "базовую логику" я не трогал, просто инкапсулировал. Полиморфизм добавлен "сверху", без мутации, так что смысла в споре не вижу.

          Зачем ? Практического смысла не много, да и вряд-ли кто-то после прочтения статьи прям "бросится" писать код на основе этой идеи. В вашем примеме радикальных отличий от моего примера только те, что Proxy у вас не используется. Так что спорить смысла тоже не вижу )

          А, ну и да, вы же неправильно сделали, поэтому и получили ошибку, вот так нужно:

          class MyHTMLElement extends HTMLElement {
            constructor() {
              super();
              const root = Object.getPrototypeOf(this);
              const props = {
                // тут можно дополнительных свойств накидать
                // которые нужно проксировать
              };
              Object.setPrototypeOf(props, root);
          
              // в этом месте в this можно накидать свойств
              // которые не будут обрабатываться proxy
              
              const proxy = new Proxy(props, { ... });
              Object.setPrototypeOf(this, proxy);
          
              // а с этого места всё новое в this будет идти через proxy    
            }
          }

          Если так сделаете то вашей ошибки не будет )