Custom elements в бою

    Добрый день!

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

    В связи с этим, при наличии желания увеличить скорость разработки и обеспечить единообразие продуктов, было решено разработать общую компонентную базу. О том, как создавался ui kit, и о долгих боях с дизайнерами мы умолчим, а вот о реализации данной задачи я и хочу поговорить.
    На фронте у нас демократия или даже анархия. Люди вольны использовать те решения, с которыми им удобно работать. На данный момент в бою есть проекты на AngularJS, Angular, React, Vanilla, и есть также проекты на Vue для внутреннего использования. Вот на этом моменте наш взор и обратился на web components.

    Web Components


    Давайте кратко осмотрим концепцию web components. В основе лежит концепция custom elements, которая позволяет расширять класс HTMLElement, создавая свои собственные html тэги, со скрытой от пользователя бизнес логикой. Звучит круто, выглядит приятно. Давайте посмотрим, что мы можем сделать. Здесь и далее исходный код приведен на typescript.

    Чтобы создать custom element, нам нужно сделать следующее. Описать класс и зарегистрировать компонент.

    export class NewCustomElement extends HTMLElement {
      constructor() {
        super();
        console.log('Here I am');
      }
    }
    if (!customElements.get('new-custom-element')) {
      /* Зарегистрируем компонент, если его еще нет */
      customElements.define('new-custom-element', NewCustomElement);
    }
    

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

    Еще custom elements дают нам несколько хуков для отслеживания жизни компонента.

    export class NewCustomElement extends HTMLElement {
      constructor() {
        super();
        console.log('I am created');
      }
      
      /* Вызывается каждый раз, когда элемент вставляется в DOM, согласно лучшим практикам, такие операции, как первый рендер компонента, стоит делать именно на данном шаге */
      connectedCallback() {
        console.log('Now I am in Dom');
        this._render();
        this._addEventListeners();
      }
    
      /* Вызывается каждый раз, когда элемент удаляется из DOM, хорошее место, чтобы произвести уборку */
      disconnectedCallback() {
        console.log('I am removed now');
        this._removeEventListeners();
      }
    
      /* Так объявляется список отслеживаемых атрибутов */
      static get observedAttributes() {
        return ['date'];
      }
    
      /* Вызывается, когда изменен один из отслеживаемых атрибутов */
      attributeChangedCallback(attrName, oldVal, newVal) {
        switch (attrName) {
            case 'date': {
              /* Обрабатываем изменение атрибута, например перерендериваем соответствующую часть компонента */
              break;
            }
         }
      }
      
      /* Элемент перенесен в новый документ */
      adoptedCallback() {
        /* Не знаю, что с этим делать, поделитесь в комментариях своими предложениями */
      }
    }
    

    Также мы можем генерировать события в компонентах через метод dispatchEvent

    export class NewCustomElement extends HTMLElement {
      //////
      _date: Date = new Date();
      set date(val: Date) {
        this._date = val;
        this.dispatchEvent(new CustomEvent('dateChanged', {
            bubbles: true,
            cancelable: false,
            detail: this._date
          }));
      }
      //////
    }
    

    Будущее наступило, говорили они, пишешь код один раз и используешь его везде, говорили они


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

    Посмотрим, какие мы получили плюсы.

    • Reusable: мы получили действительно переиспользуемую библиотеку. На данный момент она работает в vanilla проекте, подключаясь как собранный Webpack bundle, и проекте angular 7, подключаясь исходниками typescript в AppModule
    • Понятное поведение: если следовать лучшим практикам, то мы получаем компоненты с понятным поведением, которые легко интегрируются в существующие фреймворки, например для angular, с помощью бананов в коробке, в нативных приложениях через атрибуты, или работу с property, отражающими атрибуты
    • Единый стиль: это некоторое повторение пункта о реюзабельности, но все же. Теперь на всех проектах используются единые строительные блоки для конструирования UI.
    • Честно, не могу больше придумать плюсов: расскажите, чем WebComponents помогли Вам.

    Далее попробую описать вещи, которые мне скорее не понравились.

    • Трудозатраты: затраты на разработку компонент несравнимо выше, нежели разработка под фреймворк.
    • Именование: компоненты регаются глобально, поэтому и имена классов, и имена тэгов приходится префиксить. Учитывая, что у нас еще есть библиотеки компонент, реализованные под фреймворки, которые именовались как <company-component-name>, то вэб-компоненты пришлось префиксить дважды <company-wc-component-name>.
    • ShadowRoot: согласно лучшим практикам, рекомендуется использовать shadowRoot. Однако, это не очень удобно, так как не остается возможности повлиять на внешний вид компонента извне. А такая необходимость часто встречается.
    • Render: без фреймворков приходится забыть о data binding и реактивности (LitElement в помощь, но это еще одна зависимость).
    • Будущее не наступило: Чтобы сохранить поддержку пользователей на старом уровне (у нас это ie11 и все, что посвежее), приходится прикручивать полифилы, es5 — целевой стандарт, что создает дополнительные проблемы.
    • Сами полифилы: Чтобы завести все это добро под IE, пришлось немало помучиться, и принять несколько некрасивых решений, так как полифилы от webcomponent ломают что-то внутри ангуляра, вызывая переполнение call stack. В итоге пришлось полифилить полифилы, получив лишние зависимости.

    Я не знаю, какой сделать из всего этого вывод. Если Microsoft-таки сделает браузер на базе chromium и прекратит поддержку IE и Edge — то да, станет проще дышать.

    Есть одна странная польза: можно давать разработку чистых web components начинающим разработчикам — пускай посмотрят как оно, писать на JS без фреймворков. Один коллега долго не мог понять, почему изменение property в компоненте не отражалось сразу в DOM. Вот они — люди, выращенные на фреймворках. И я такой же.
    Поделиться публикацией

    Похожие публикации

    Комментарии 14

      +1

      Повлиять на стиль компонента с Shadow DOM легко: стили уровня :host могут быть переопределены снаружи как и у любого другого DOM-элемента. Для пробрасывания свойств в более глубокие слои — используются CSS-переменные, и это значительно лучше и удобнее отсутствия инкапсуляции. Microsoft поддержку пилит в данный момент (официальный статус). Хотя, конечно, не известно сколько они ее будут еще пилить, возможно и не успеют до перехода на новый движок с такими темпами. Остальное тоже, так или иначе, решается, но если нужна поддержка IE — наверное не стоит связываться. Зависит от того, сколько у вас реальных юзеров на IE.

        0
        К сожалению, процент пользователей IE крайне высок.
        Повлиять на стиль компонента с Shadow DOM легко: стили уровня :host могут быть переопределены снаружи как и у любого другого DOM-элемента

        Я имел в виду стили именно не host уровня, а те, что сидят под shadowRoot компонента. вот пример того что я имею в виду в index.html я пытаюсь повлиять на стиль .test-class, но нет никакого эффекта. Именно поэтому для части компонент, например, календаря, пришлось отказаться от использования shadowRoot, так как часть стилей (цвета, например) иногда нужно переопределять. Скажем, в angular, вроде можно было использовать :ng-deep для таких целей
          0
          К сожалению, процент пользователей IE крайне высок.

          Больше чем в среднем по статистике? caniuse.com/usage-table
          в index.html я пытаюсь повлиять на стиль .test-class, но нет никакого эффекта

          Снаружи:
          :root {
          --color: #F00;
          }
          

          Внутри компонента (из Вашего примера):
            connectedCallback() {
              let shadow = this.attachShadow({ mode: 'open' });
              shadow.innerHTML = `<style>
              .test-class {
                background-color: var(--color, #EEE);
              }
              </style>
              <div class="test-class">Test text</div>`
            }
          

          И да, шаблон лучше добавлять не в connectedCallback, а в конструкторе и не парсить каждый раз HTML а клонировать (cloneNode) из заранее созданного контейнера template. И вообще, лучше создать базовый класс блэкджеком и биндингами и наследоваться от него.
            0
            Больше чем в среднем по статистике?

            О да, в десятки раз. Примерно половина.

            И вообще, лучше создать базовый класс блэкджеком и биндингами и наследоваться от него

            В статье упоминался lit-element, как вариант.

            Спасибо за :root — поэкспериментирую (проверил на stackblitz — работает). Если добавить в документацию к компонентам — вполне себе решение
              0
              Я использую два базовых метода параметризации стилей: через кастомные атрибуты и через CSS-переменные. В сочетании они дают полный контроль над всем необходимым:
              <my-button big style="--color: #F00">Press Me!</my-button>
              
                0

                Я понял ваш способ, но только что нашел один недостаток. Перегрузить можно только то что предусмотрел разработчик компонента. То есть на самом деле, если внутри shadow написано строго display: block, то я не смогу снаружи это поведение изменить.

                  –1

                  Ну в этом ведь и смысл Shadow DOM, не совсем понимаю как это можно называть недостатком…

        +1

        спасибо за статью, интересный опыт!


        полифилы от webcomponent ломают что-то внутри ангуляра, вызывая переполнение call stack

        Я так понимаю, речь про эту issue? Я тоже сталкивался, знакомо.

          0
          Да, это именно оно. В итоге полечил, но осадок остался
            0
            Кстати, можете поделиться своим решением? Вдруг то, что смогли придумать Вы — элегантнее того что я нашел на просторах. Я пока что просто подключил другие полифилы до того, как подключается webcomponents-loader, чтобы он не стал загружать свои.
              +2
              моим решением было убедить руководство, что нам лучше перестать поддерживать IE11, чем бороться с этими багами :)
                0
                Вы счастливый человек. У нас, к сожалению пока нет такой возможности. Точнее любое начальство, которое вообще станет со мной говорить, не сможет повлиять на выбор ПО в масштабе всех предприятий, где используется наш софт. Пользователи, конечно, ставят chrome, он не запрещен, но попадаются товарищи, которые используют ie говоря: «это стандарт компании, одобрено безопасностью, и тому подобное»
            +1
            Спасибо за статью. Для adoptedCallback() я так же не могу придумать живой пример применения
              0
              Это интересно, на самом деле. Что имели в виду авторы стандарта? Ведь при добавлении в новый документ, и в его DOM по сути может стрельнуть и connectedCallback(). Для чего эти методы отделили я пока не могу придумать

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

            Самое читаемое