Когда разговор заходит о веб-компонентах, часто говорят: «Ты что хочешь без фреймворков? Там же все готовое». На самом деле есть фреймворки созданные на основе реализаций стандартов входящих в группу веб-компонентов. Есть даже относительно неплохие, такие как X-Tag. Но сегодня мы все равно будем разбираться насколько простым, элегантным и мощным стало браузерное API для решения повседневных задач разработки в том числе организации взаимодействия компонентов между собой и с другими объектами из контекста выполнения браузера, а использовать фреймворки вместе с веб-компонентами всегда можно, даже те, которые разрабатывались поперек стандартов в том числе через механизмы, которые мы сегодня разберем, по крайней мере так утверждается на сайте.

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

У каждого элемента есть атрибуты значения которых мы можем менять. И если перечислить имена в хуке observedAttributes, то при их изменении будет автоматически вызываться attributeChangedCallback() в котором мы можем определить поведение компонента при изменении атрибута. Используя магию геттеров нетрудно сделать обратный биндинг схожим образом.

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

Сразу стоит оговорить одно ограничение, по крайней мере в текущей реализации заключающееся в том, что значением атрибута может быть только примитивное значение, задаваемое литералом и приводимое к строке, но вообще можно таким образом передавать и “обджекты” используя одинарные кавычки снаружи и двойные для определения значений и полей обжекта.

<my-component my='{"foo": "bar"}'></my-component>

для использования этого значения в коде можно реализовать автомагический геттер который будет вызывать у него JSON.parse().

Но пока что нам хватит числового значения счетчика.

Добавим нашему элементу новый атрибут count, укажем его как observed, обработчик клика заставим инкрементировать этот счетчик, а на хук изменения добавим обновление отображаемого значения по прямой ссылке логику которого реализуем в отдельном переисползуемом методе updateLabel().

export class MyWebComp extends HTMLElement {

   constructor() {
       super();
   }

   connectedCallback() {
       let html = document.importNode(myWebCompTemplate.content, true);
       this.attachShadow({mode: 'open'});
       this.shadowRoot.appendChild(html);
       this.updateLabel();
   }

   updateLabel() {
       this.shadowRoot.querySelector('#helloLabel').textContent = 'Hello ' +
           this.getAttribute('greet-name') + ' ' + this.getAttribute('count');
   }

   static get observedAttributes() {
       return ['count'];
   }

   attributeChangedCallback(name, oldValue, newValue) {
       if (name === 'count') {
           this.updateLabel();
       }
   }

   showMessage(event) {
       this.setAttribute('count', this.getAttribute('count') + 1);
   }
}



Каждый элемент получил независимый автоматически обновляемый счетчик.

Домашнее задание: реализуйте приведение значения счетчика к числу и использование через авто-геттер;)

Возможны конечно и более продвинутые варианты. Например, если не брать в расчет простые колбеки и евенты, с помощью нативного Proxy и тех же геттеров и сеттеров можно реализовать функционал обратного биндинга.

Добавим файл my-counter.js с классом такого вида

export class MyCounter extends EventTarget {
  
   constructor() {
       super();
       this.count = 0;
      
   }
  
   increment() {
       this.count++;
       this.dispatchEvent(new CustomEvent('countChanged', {
           detail: { count: this.count }
       }));
   }
}

Мы унаследовали класс от EventTarget, чтобы другие классы могли подписываться на события выбрасываемые объектами этого класса и определили свойство count которое будет хранить значение счетчика.

Теперь добавим инстанс этого класса как статическое свойство для компонента.

<script type="module">
   import { MyWebComp } from "./my-webcomp.js";
   import { MyCounter } from "./my-counter.js";

   let counter = new MyCounter();

   Object.defineProperty(MyWebComp.prototype, 'counter', { value: counter });

   customElements.define('my-webcomp', MyWebComp);
</script>

А в коде компонента подпишем на изменение значения метод обновления лейбла updateLabel(), в который добавим отображение значения из глобального расшаренного счетчика. А в обработчик нажатия прямой вызов инкрементирующего метода.

export class MyWebComp extends HTMLElement {

   constructor() {
       super();
   }

   connectedCallback() {
       let html = document.importNode(myWebCompTemplate.content, true);
       this.attachShadow({mode: 'open'});
       this.shadowRoot.appendChild(html);
       this.updateLabel();
       this.counter.addEventListener('countChanged', this.updateLabel.bind(this));
   }

   updateLabel() {
       this.shadowRoot.querySelector('#helloLabel').textContent = 'Hello ' +
           this.getAttribute('greet-name') + ' ' + this.getAttribute('count') + ' ' + this.counter.count;
   }

   static get observedAttributes() {
       return ['count'];
   }

   attributeChangedCallback(name, oldValue, newValue) {
       if (name === 'count') {
           if (this.shadowRoot) {
               this.updateLabel();
           }
       }
   }

   showMessage(event) {
       this.setAttribute('count', parseInt(this.getAttribute('count')) + 1);
       this.counter.increment();
   }
}

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



Таким образом, мы получили прямое связывание в прямом биндинге и за счет применения эвентов слабое в обновлении этого значение. Ничто конечно не мешает и инкремент реализовать через эвенты, сбиндив метод increment() к лисенеру одноименного эвента:

export class MyCounter extends EventTarget {

   constructor() {
       super();
       this.count = 0;
       this.addEventListener('increment', this.increment.bind(this));
   }

   increment() {
       this.count++;
       this.dispatchEvent(new CustomEvent('countChanged', {
           detail: { count: this.count }
       }));
   }

}

и заменив вызов метода на выброс евента:

export class MyWebComp extends HTMLElement {


   ...

   showMessage(event) {
       this.setAttribute('count', parseInt(this.getAttribute('count')) + 1);
       this.counter.dispatchEvent(new CustomEvent('increment'));
   }
}

Что это меняет? теперь если в ходе развития метод increment() будет убран или изменен, корректность нашего кода нарушится, но ошибок интерпретатора возникать не будет, т.е. сохранится работоспособность. Такая характеристика называется слабой связностью.

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

Мы можем по старинке вешать обработчики и вызывать события на глобальном объекте document.

export class MyWebComp extends HTMLElement {


   constructor() {
       super();
   }

   connectedCallback() {
       let html = document.importNode(myWebCompTemplate.content, true);
       this.attachShadow({mode: 'open'});
       this.shadowRoot.appendChild(html);
       this.updateLabel();
       this.counter.addEventListener('countChanged', this.updateLabel.bind(this));
       document.addEventListener('countChanged', this.updateLabel.bind(this));
   }

   disconnectedCallback() {
       document.removeEventListener(new CustomEvent('increment'));
       document.removeEventListener(new CustomEvent('countChanged'));
   }
   ...
   showMessage(event) {
       this.setAttribute('count', parseInt(this.getAttribute('count')) + 1);
       this.counter.dispatchEvent(new CustomEvent('increment'));
       document.dispatchEvent(new CustomEvent('increment'));
   }
}

export class MyCounter extends EventTarget {

   constructor() {
       super();
       this.count = 0;
       this.addEventListener('increment', this.increment.bind(this));
       document.addEventListener('increment', this.increment.bind(this));
   }

   increment() {
       this.count++;
       this.dispatchEvent(new CustomEvent('countChanged', {
           detail: { count: this.count }
       }));
       document.dispatchEvent(new CustomEvent('countChanged', {
           detail: { count: this.count }
       }));
   }

}

Поведение никак не отличается, но теперь нам надо подумать о том, чтобы убрать обработчик события с глобального объекта если сам по себе элемент удален из дерева. Благо веб-компоненты предоставляют нам не только хуки конструкции, но и деструкции позволяя избегать утечек памяти.

Также есть еще один немаловажный момент: эвенты элементов дерева могут взаимодействовать между собой посредствам механизма называющегося “баблинг”.

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

В любой точке этой цепочки вызовов эвент может быть перехвачен и обработан.

Это конечно не совсем один и тот же эвент, а его производные и контексты хоть и будут содержать ссылки друг на друга как например в event.path, но не будут полностью совпадать.

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

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

Годами веб-разработчики боролись с “пропагацией” (распространением или всплытием) евентов и стопили и превентили их как могли, а для их комфортного использования как и в случае с айдишниками не хватало только изоляции теневого дерева.

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

Чтобы понять как это все работает обернем наши элементы webcomp в контейнер вот так:

<my-webcont count=0>
   <my-webcomp id="myWebComp" greet-name="John" count=0 onclick="this.showMessage(event)"></my-webcomp>
   <my-webcomp id="myWebComp2" greet-name="Josh" count=0 onclick="this.showMessage(event)"></my-webcomp>
</my-webcont>

У контейнера будет вот такой код:

export class MyWebCont extends HTMLElement {


   constructor() {
       super();
   }

   connectedCallback() {
       this.addEventListener('increment', this.updateCount.bind(this));
   }

   updateCount(event) {
       this.setAttribute('count', parseInt(this.getAttribute('count')) + 1);
   }

   static get observedAttributes() {
       return ['count'];
   }

   attributeChangedCallback(name, oldValue, newValue) {
       if (name === 'count') {
           this.querySelectorAll('my-webcomp').forEach((el) => {
              el.setAttribute('count', newValue);
           });
       }
   }

}

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

Код дочерних элементов изменится незначительно, собственно говоря все что нам нужно это чтобы событие increment выбрасывал сам элемент а не его агрегаты, и делал он это с атрибутом bubbles: true.

export class MyWebComp extends HTMLElement {

 ...

   showMessage(event) {
       this.setAttribute('count', parseInt(this.getAttribute('count')) + 1);
       this.counter.dispatchEvent(new CustomEvent('increment'));
       this.dispatchEvent(new CustomEvent('increment', { bubbles: true }));
       document.dispatchEvent(new CustomEvent('increment'));
   }
}



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

Готовый код для референса вы можете найти все в том же репозитории, в бранче events.

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


  1. leybniz
    24.07.2019 14:25

    Для чего так утруждать руки набором кода? Есть же lit element. Всё это уже прожевано там.


    1. syncro Автор
      24.07.2019 14:31

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


      1. epishman
        24.07.2019 18:02

        Это пунктик гугла, во Flutter они вообще верстку выпилили, оставив голые классы.


        1. syncro Автор
          25.07.2019 04:27
          +1

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


          1. epishman
            25.07.2019 09:12

            А html импорты разве реализованы, вроде там проблема с синхронными конструкторами была, тогда вариантов нет — скрипт дает больше гибкости.


            1. syncro Автор
              25.07.2019 09:21
              +1

              отдельные браузерные вендоры от импортов отказались «на отрез», поэтому в стандарты W3C они не пошли, а команда полимера на тот момент уже вовсю готовила 3ю версию фреймворка серьезно базировавшуюся на механизме импортов и все дальнейшее развитие тоже пришлось отменить в пользу LitElement


              1. epishman
                25.07.2019 10:51

                Спасибо!


              1. i360u
                26.07.2019 09:06

                В 3-м Полимере основным нововведением как раз был переход от html-импортов к ES-модулям. Вполне закономерно и грамотно. Но Lit — оказался быстрее… Я лично не сравнивал, но судя по блогу разрабов — это был основной аргумент перехода. Сейчас Polymer — это скорее экосистема, чем библиотека. И LitElement входит в нее. Но из-за всех этих метаний, веб-разработчики отвернулись от этого минизоопарка, хотя и стоит отдать должное сообществу Полимера — они сделали очень многое для внедрения новых стандартов.


      1. i360u
        25.07.2019 08:42
        +2

        Принцип разделения ответственности не определяет критерии разделения. То есть вы сами вольны делить свой код в согласии с вашими приоритетами и в в случае с LitElement никаких особых проблем с этим нет. Как нет их и с возможностью поддержки скинов и другой кастомизации: вам доступны те-же самые пользовательские свойства CSS и кастомные атрибуты. Но мне самому LitElement не нравится, более всего потому, что вместо того, чтобы быть альтернативой реакту, он пошел той-же скользкой дорожкой на поводу у фанатов функциональщины. И из за этого, к примеру, он медленнее, чем мог бы быть.


        1. syncro Автор
          25.07.2019 08:47
          +1

          у меня был кейс angular material/cdk, когда мне надо было сделать другой пейджер и свой фильтр, легко переопределял классы и использовал свои шаблоны, менял на совершенно иной принцип работы и заодно локализовал, не уверен, что такое было бы возможно с фреймворком предполагающим прямую линковку и хардкод верстки


          1. i360u
            25.07.2019 09:53

            Но в Lit шаблон — это же просто js-строка, которую вы можете генерить на лету, как вам угодно, или брать целиком из различных источников. Совсем не обязательно что-то хардкодить.


            1. syncro Автор
              25.07.2019 10:07

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


              1. i360u
                26.07.2019 08:57

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


                1. syncro Автор
                  26.07.2019 09:23

                  если у вас шаблоны отдельно, то их надо загружать, загружать их надо будет по сети, особенно если вы имеете дело с SPA и некоторой модульной архитектурой на приличном размере, а по сети значить асинхронный запрос с отложенным результатом, т.е. это надо будет делать либо до инициализации/рендера самих компонентов, либо как-то хитрить. Видимо так же нетривиально, как и в случае если вам пейджер с контролами < — -> надо будет перескинить на пейджер с пейджанацией 1 2 3 используя только css. Насчет наследования я не уверен, тут штука в том, что компоненты могут находится в тесном взаимодействии друг с другом которое будет полагаться на конкретные класс, а не их даже наследники. Например у вас есть такой лейаут:

                  <grid>
                    <filter></fitler>
                    <table></table>
                    <pager></pager>
                  </grid>
                  


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


                  1. i360u
                    26.07.2019 09:29

                    Не совсем понимаю зачем загружать шаблоны отдельно… Почему сразу нельзя создать несколько шаблонов и передавать в render нужный? Почему нельзя переопределить класс-конструктор?


                    1. syncro Автор
                      26.07.2019 09:58

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


                      1. i360u
                        26.07.2019 11:03
                        +1

                        Если вы код контролируете — проблемы нет вообще. Если не контролируете — тогда не понятно как именно вы интегрируетесь. Почему нельзя сделать так:

                            class MyEl extends HTMLElement {
                        
                              render(tpl) {
                                this.$.innerHTML = tpl;
                              }
                        
                              constructor() {
                                super();
                                this.$ = this.attachShadow({
                                  mode: 'closed',
                                });
                                this.render('Original Element');
                              }
                            }
                            
                            class MyExtendedEl extends MyEl {
                              constructor() {
                                super();
                                this.render('Extended Element');
                              }
                            }
                        
                            if (someFlag) {
                              MyEl = MyExtendedEl;
                            }
                        
                            window.customElements.define('my-el', MyEl);
                        

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


                        1. syncro Автор
                          26.07.2019 12:01

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


        1. justboris
          25.07.2019 11:08

          Про производительность интересно. Что могли в lit-element сделать по-другому, чтобы стать быстрее?


          1. i360u
            26.07.2019 08:49

            Для ускорения первичной отрисовки, могли исключить этап обработки строки шаблона в js-рантайме для привязки данных перед парсингом в DOM. Вариант с клонированием шаблона из template-контейнера и заполнением его данными, но уже через DOM API работает быстрее почти в 2 раза, судя мо моим тестам. Даже если template-контейнер (элемент ) создавать динамически из той-же js-строки и хранить в памяти. Для ускорения рендера изменений — могли бы не делать этого асинхронно. Синхронный рендер работает быстрее на 20% примерно (браузер умный, он не перерисовывает документ сразу если изменение элементов идет в цикле). Как то так.


            1. justboris
              26.07.2019 10:57
              +1

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


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


              Синхронное обновление свойств – это проклятие веб-компонентов в целом. Свойства обновляются поштучно, и изнутри компонента вы не можете предугадать, закончили вам передавать свойства или нет. Я уже писал об этом подробнее. И в статье Рича Харриса, почему Svelte не использует веб-компоненты, об этом тоже есть. В общем, lit-element тут не одинок, все веб-компоненты этим страдают.


              1. i360u
                26.07.2019 11:17
                +2

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


              1. syncro Автор
                26.07.2019 12:11
                +1

                >> то начинаются другие проблемы

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

                >> С ленивой загрузкой компонентов тоже непонятно как быть

                с ленивой загрузкой в веб-компонентах ситуация лучше всех, т.к. для браузера динамически дорендеренный <my-custom-element> преобразуется в ваш кастомный компонент без особенных препятствий и с выполнением полагающихся хуков

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

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


                1. justboris
                  26.07.2019 16:59

                  грузить шаблоны можно и фронтенд кодом

                  Собственно, lit-html это сейчас и делает. Если я правильно понял i360u, то он предлагает следующий уровень оптимизации — вынести это из JS совсем


  1. epishman
    24.07.2019 14:26
    +1

    Сама идея, что у приложения есть данные (State) и представление (DOM) — создает дополнительные сложности, которые вынужден был решать Реакт. Но если мы примем другой подход — данные приложения, состояние приложения — это и есть DOM, и нет ничего кроме DOM — тогда все становится сильно проще — не нужен двусторонний биндинг, не нужен искусственный глобальный стейт и т.д. Одна проблемка — в DOM лежат строки, но и JS динамический язык, так что штепсель соответствует розетке, а если хочется извлекать из DOM типизированные данные и в удобном виде — пишем геттеры и все. Все это можно сделать даже без кастом-элементов, на старых стандартах.


    1. syncro Автор
      24.07.2019 14:38

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


      1. epishman
        24.07.2019 14:58
        +1

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


    1. justboris
      24.07.2019 23:08

      А как это будет работать на практике? Возьмем, к примеру, вот этот самый блок комментариев на Хабре. Каким образом вы бы собрали из DOM его состояние, чтобы реализовать редактирование комментария, например?


      1. epishman
        24.07.2019 23:53

        Я больше это к формам применял, а тут… текст достану из innerHTML, автора отсюда. <span class=''user-info__nickname''>epishman
        Естественно, когда все данные приходится доставать из DOM, верстку делаешь с учетом этого требования, ID где надо прописать, иногда скрытые спаны приходится вставлять, пишется одна JS-функция на все это.


        1. justboris
          25.07.2019 11:06
          +1

          Да, есть такая библиотека, Stimulus, работает именно как вы и описываете. Я даже опубликовал на Хабре перевод их анонса.


          Однако, в массы такой подход не идет почему-то.


          1. epishman
            25.07.2019 12:02

            Спасибо за ссылку!
            PS
            Массы всегда следуют широкой дорогой, легче ведь 10 понятных кодеров нанять, чем с двумя непонятными дизайнить, тем более если деньги есть :)


    1. i360u
      25.07.2019 08:29
      +1

      Идея о том, что DOM может быть основным хранилищем данных, поскольку он в любом случае их хранит, имеет под собой веские основания. Тем более, что помимо данных, пришедших, к примеру с сервера, есть еще положение скролла, элементы в фокусе и так далее, то есть все то, чего нет обычно в реализациях внешних хранилищ, и DOM, вроде как, имеет больше прав называться «стейтом». Однако есть одно большое «но»: DOM — это древовидная структура, а ваши реальные данные — чаще являются графом. И если любое дерево может быть представлено в виде графа, то граф в виде дерева — уже нет. На практике это означает, что если у вас есть, к примеру, некая зависимость в интерфейсе от того, залогинен пользователь или нет — вы не можете рассчитывать на достоверность этого флага в одной из нод вашего DOM, пока вы не выделите для хранения этого значения единый «источник правды». Насколько я помню, именно об это обожглись, в свое время, разработчики Facebook, перед тем как начали пропагандировать flux-архитектуру.

      Все это можно сделать даже без кастом-элементов, на старых стандартах.

      — и в итоге вы получите толстый оверхед в js как в React.


      1. epishman
        25.07.2019 08:38

        Я реакта не знаю, но насколько понимаю, там байндинги нужны на любые данные, даже на статику, в моем случае — только на то, что изменяется, или используется при изменении чего-то другого, остальные 80% статики формируются один раз, и забыли.
        PS
        Кстати, спасибо за наводку, изучу историю этого дела…


      1. epishman
        25.07.2019 08:47

        Про безопасность честно не думал, для внутри-корпоративного ПО это решалось другими средствами, а тут даже не знаю, возможно, что-то и придется хранить в jS.