Введение в веб-компоненты. Часть 1

в 18:33, , рубрики: Google, IT-стандарты, javascript, shadow dom, w3c, webcomponents, Веб-разработка, стандарты

От переводчика: Представляю вашему вниманию перевод многообещающего стандарта Веб-компонентов от Google, который может стать трендом в ближайшие несколько лет. В данный момент, знание этого стандарта не несёт практического применения, поэтому, если вы не фанат всего нового и интересного, вам возможно будет скучно читать данный перевод.
Перевод выложен на github, поэтому, если вы хотите помочь с переводом или исправить ошибку сделайте pull request, ну или пишите в личку.

Статус: Эксперементальный драфт

Авторы:

Введение

Компонентная модель для Web'а (или Web Components) состоит из четырёх модулей, которые, будучи использованы вместе, позволят разработчикам web-приложений создавать виджеты с богатыми визуальными возможностями при этом легкие в разработке и переиспользовании, что на данный момент невозможно при использовании только CSS и JS-библиотек.

Эти модули:

  • шаблоны (templates), определяют кусок неактивной разметки, которая может быть использована в дальнейшем;
  • декораторы (decorators), позволяют через CSS управлять визуальными и поведенческими изменениями в шаблонах;
  • пользовательские элементы (custom elements), позволяют авторам определить их собственные элементы (включая представление и API этих элементов), которые могут быть использованы в основном HTML-документе;
  • теневой DOM (shadow DOM), определяет, как представление и поведение декораторов и пользовательские элементы сочетаются друг с другом в дереве DOM.

Вместе декораторы и пользовательские элементы называются компонентами

Шаблоны

Элемент <template> содержит разметку предназначенную для использования позже с помощью скрипта или другого модуля, который может использовать шаблон (например, <decorator> и <element>, которые описаны ниже).

Содержимое элемента <template> разбирается анализатор, но оно статично: скрипты не запускаются, картинки не грузятся, и т.д. <template> элемент не рендерится.

В скрипте такой элемент имеет специальное свойство content, которое содержит статическую DOM-структуру определённую в шаблоне.

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

<decorator id="fade-to-white">
    <template>
        <div style="position: relative;">
            <style scoped>
                #fog {
                    position: absolute;
                    left: 0;
                    bottom: 0;
                    right: 0;
                    height: 5em;
                    background: linear-gradient(
                    bottom, white 0, rgba(255, 255, 255, 0) 100);
                }
            </style>
            <content></content>
            <div id="fog"></div>
        </div>
    </template>
</decorator>

Добавление статического DOM-узла в документ делает его «живым», как будто этот DOM-узел был получен через свойство innerHTML.

Декораторы

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

Элемент <decorator> содержит элемент <template>, который определяет разметку используемую для рендеринга декоратора.

<decorator id="fade-to-white">
    <template>
        <div style="position: relative;">
            <style scoped>
                #fog {
                    position: absolute;
                    left: 0;
                    bottom: 0;
                    right: 0;
                    height: 5em;
                    background: linear-gradient(
                    bottom, white 0, rgba(255, 255, 255, 0) 100);
                }
            </style>
            <content></content>
            <div id="fog"></div>
        </div>
    </template>
</decorator>

Элемент <content> указывает на место, куда декоратор (точнее, его содержимое) должно быть вставлено.

Декоратор применяется, используя css-свойство decorator:

.poem {
    decorator: url(#fade-to-white);
    font-variant: small-caps;
}

Декоратор и css описанные выше заставят данную разметку:

<div class="poem" style="font-variant: small-caps;">
    <div style="position: relative;">
        Two roads diverged in a yellow wood,<br>
        …
        <div style="position: absolute; left: 0; …"></div>
    </div>
</div>

рендерится, как будто это была такая разметка (опуская браузерные стили для краткости):

<div class="poem" style="font-variant: small-caps;">
    <div style="position: relative;">
        Two roads diverged in a yellow wood,<br>
        …
        <div style="position: absolute; left: 0; …"></div>
    </div>
</div>

Если документ изменился так, что css-селектор, где был объявлен декоратор, более не действителен, обычно когда селектор со свойством decorator более не применяется к элементу или правило с декоратором было изменено в атрибуте style элемента, декоратор более не применяется, возвращая рендеринг элемента в первоначальное состояние.

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

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

События в декораторах

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

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

Рис. Регистрация обработчиков событий

Регистрация обработчиков событий

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

<decorator id="decorator-event-demo">
    <script>
        function h(event) {
            alert(event.target);
        }
        this.listen({selector: "#b", type: "click", handler: h});
    </script>
    <template>
        <content></content>
        <button id="b">Bar</button>
    </template>
</decorator>

Вызов функции lisnen означает, что когда кнопка будет нажата, сработает обработчик события.

Контроллер событий перенаправит событие, наступившее в на любой ноде, на которой декоратор был применён, в обработчик события.

Рис. Обработка и переназначение событий

Обработка и переназначение событий

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

<span style="decorator: url(#decorator-event-demo);">Foo</span>

Рендерится в:

Foo[Bar]

Клик по кнопке покажет сообщение с [object HTMLSpanElement].

Переопределение свойства target необходимо, тк декоратор определяет отображение; он не влияет на структуру документа. Пока декоратор применён, свойство target переопределяется на ноду, на которую он применён.

Также, если скрипт меняет контент шаблона, изменения игнорируются, точно также, как установка textContent элемента <script> не влечет за собой выполнение скрипта ещё раз.

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

Примеры декораторов

Пример, как декоратор может быть применён для создания простого варианта элемента detail:


details {
    decorator: url(#details-closed);
}
details[open] {
    decorator: url(#details-open);
}

<decorator id="details-closed">
    <script>
        this.listen({
            selector: "#summary", type: "click",
            handler: function (event) {
                event.currentTarget.open = true;
            }
        });
    </script>
    <template>
        <a id="summary">
            > <content select="summary:first-of-type"></content>
        </a>
    </template>
</decorator>

<decorator id="details-open">
    <script>
        this.listen({
            selector: "#summary", type: "click",
            handler: function (event) {
                event.currentTarget.open = false;
            }
        });
    </script>
    <template>
        <a id="summary">
            V <content select="summary:first-of-type"></content>
        </a>
        <content></content>
    </template>
</decorator>

Для этого понадобилось два декоратора. Один представляет detail элемент в закрытом виде, другой в открытом. Каждый декоратор использует обработчик события клика мыши, для изменения состояния открыт/закрыт. Атрибут select элемента <element> будет рассмотрен подробнее ниже.

Пользовательские элементы

Пользовательские элементы — новый тип DOM-элементов, которые могут быть определены автором.

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

Элемент <element> определяет пользовательский элемент.

<element extends="button" name="x-fancybutton">
    …
</element>

Атрибут extends определяется элемент, функционал которого мы хотим расширить. Каждый экземпляр пользовательского элемента будет иметь tagName определённый в атрибуте extends.

Атрибут name определяет пользовательский элемент, который будет связан с этой разметкой. Пространство имён у атрибута name такое же, как у имён тэгов стандартных элементов, поэтому для устранения коллизий, используется префикс x-.

Разные браузеры, определяют HTML элементы по-разному, однако все их интерпретации руководствуются семантикой HTML.

Тк не все браузеры поддерживают пользовательские элементы, авторы должны расширять HTML элементы, которые имеют наиболее близкое значение для нового пользовательского элемента. Например, если мы определяем пользовательский элемент, который является интерактивным и реагирует на клики, выполняя некоторые действия, мы должны расширять кнопку (<button>).

Когда нету HTML элемента, семантически близкого к нужному, автор должен расширять нейтральный элемент, такой как <span>.

Представление

Пользовательский элемент может содержать шаблон:

<element extends="button" name="x-fancybutton">
    <template>
        <style scoped>
            ::bound-element { display: transparent; }
            div.fancy {
                …
            }
        </style>
        <div class="fancy">
            <content></content>
            <div id="t"></div>
            <div id="l"></div>
            <div id="b"></div>
            <div id="r"></div>
        </div>
    </template>
</element>

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

Теневой DOM будет описан ниже.

Использование пользовательских элементов в разметке

Т.к. пользовательские элементы использую существующие HTML тэги (div, button, option и тд), нам нужен атрибут для определения когда мы хотим использовать пользовательский элемент. Таким атрибутом является is, а значением его является название пользовательского элемента. Например:

<element extends="button" name="x-fancybutton">  <!-- definition -->
    …
</element>

<button is="x-fancybutton" onclick="showTimeClicked(event);">  <!-- use -->
    Show time
</button>

Использование пользовательских элементов в скриптах

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

var b = document.createElement("x-fancybutton");
alert(b.outerHTML); // will display '<button is="x-fancybutton"></button>'

Также, вы можете установить атрибут constructor у элемента <element>, чтобы явно указать название конструктора элемента, которое будет экспортировано в объект window. Этот конструктор может быть использован для создания пользовательского элемента:

<element extends="button" name="x-fancybutton" constructor="FancyButton">
    …
</element>

var b = new FancyButton();
document.body.appendChild(b);
</code></pre>

Пользовательский элемент может объявлять методы API добавляя их в свой prototype, в элементе <script>, находящимся внутри элемента <element>:

<element extends="button" name="x-fancybutton" constructor="FancyButton">
    …
    <script>
    FancyButton.prototype.razzle = function () {
        …
    };
    FancyButton.prototype.dazzle = function () {
        …
    };
    </script>
</element>

…

<script>
    var b = new FancyButton();
    b.textContent = "Show time";
    document.body.appendChild(b);
    b.addEventListener("click", function (event) {
        event.target.dazzle();
    });
    b.razzle();
</script>

Для того, чтобы обеспечить простую деградацию, this внутри элемента <script> указывает на родительский элемент типа HTMLElementElement:

<element extends="table" name="x-chart" constructor="Chart">
    <script>
        if (this === window)
            // Use polyfills to emulate custom elements.
            // …
        else {
            // …
        }
    </script>
</element>

Технически, скрипт внутри элемента <script>, когда он является вложенным в <element> или <decorator>, выполняется идентично такому вызову:

(function() {
    // code goes here.
}).call(parentInstance);

В ситуациях, когда название конструктора в объекте window неизвестно, автор пользовательского компонента может воспользоваться свойством generatedConstructor у HTMLElementElement:

<element extends="table" name="x-chart">
    <script>
        // …
        var Chart = this.generatedConstructor;
        Chart.prototype.shizzle = function() { /* … */ };
        // …
    </script>
</element>

Нельзя создать пользовательский элемент указывая is атрибут у существующего DOM-элемента. Выполнение следующего кода ничего не даст:

var div = document.createElement("div");
div.setAttribute("is", "foo");
alert(div.is); // displays null
alert(div.outerHTML); // displays <div></div>

Обновление элемента

Когда объявление пользовательского элемента будет загружено, каждый элемент с атрибутом is установленном в имя пользовательского элемента будет обновлён до пользовательского элемента. Обновление должно быть идентично удалению элемента и замене его на пользовательский элемент.

Когда каждый элемент заменяется, не всплывающее (non-bubbling), неотменяемое (non-cancellable) событие возникает на удаляющемся элементе. Скрипт, который хочет задержать взаимодействие с остальной часть документа до тех пор, пока пользовательский элемент не загрузиться может подписаться на специальное событие:

function showTimeClicked(event) {
    // event.target may be an HTMLButtonElement or a FancyButton

    if (!event.target.razzle) {
        // razzle, part of the FancyButton API, is missing
        // so upgrade has not happened yet
        event.target.addEventListener('upgrade', function (upgradeEvent) {
            showTime(upgradeEvent.replacement);
        });
        return;
    }

    showTime(event.target);
}

function showTime(b) {
    // b is FancyButton
}

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

Методы жизненного цикла

Пользовательский элемент может подписаться на четыре метода жизненного цикла:

  • created — вызов конструктора, создание экземпляра ShadowRoot из элемента <template>. При отсутствии <template>, ShadowRoot устанавливается в null.
  • attributeChanged — вызывается всякий раз, когда атрибут элемента изменяется. Аргументы: имя, старое значение, новое значение.
  • inserted — вызывается, после того, как пользовательский компонент вставлен в DOM-дерево. В данном обработчике можно подгрузить ресурсы для пользовательского конпонента или запустить таймеры.
  • removed — вызывается после того, как пользовательский элемент удалён из DOM-дерева. Тут можно остановить таймеры, которые не нужны, когда пользовательский элемент не находится в DOM-дереве.

Обработчики вызываются с this указывающим на элемент.

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

Подписаться на эти обработчики можно вызвав метод HTMLElementElement.lifecycle:

<element extends="time" name="x-clock">
<script>
// …

        this.lifecycle({
            inserted: function() { this.startUpdatingClock(); },
            removed: function() { this.stopUpdatingClock(); }
        });

// …
</script>
</element>

Расширение пользовательских элементов

В дополнении к HTML-элементам, можно расширить пользовательский элемент указанием имени пользовательского элемента в атрибуте extends элемента <element>:

<element extends="x-clock" name="x-grandfatherclock">
    …
</element>

Обработка ошибок

Есть несколько возможностей для обработки ошибок при рендеринге пользовательских элементов:

  • tagName элемента не соответствует значению атрибута extends пользовательского элемента, например: <div is="x-fancybutton">, но <element name="x-fancybutton" extends="button">. В этом случае, атрибут is отбрасывается во время парсинга.
  • Значение атрибута is не соответствует никакому существующему элементу. Эта ситуация рассматривается как если бы определение пользовательского элемента ещё не было загружено, и элемент будет обновлён​​, как только определение загрузится.
  • Значение атрибута extends не соответствует никакому существующему элементу. В этом случае обработка определения пользовательского элемента будет удерживаться до тех пор, пока элемент обозначенный в атрибуте extends не загрузится.
  • Значение атрибута is не начинается с x-. В данном случае, атрибут is игнорируется.
  • Значение атрибута name не начинается с x-. В данном случае, определение пользовательского элемента считается невалидным и игнорируется.

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

Автор: termi


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js