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

В последней версии (V1) спецификации Shadow DOM представлена ​​новая концепция при создании теневого корня: режим.

Чтобы создать теневой корень для элемента, вы вызываете метод attachShadow, предоставляя объект в качестве аргумента, который имеет обязательное свойство mode.

let $element = document.createElement("div");
$element.attachShadow({ mode: "open" }); // open or closed

Я не встречал статей, в которых обсуждалась бы практическая разница между использованием open и closed режима Shadow DOM, поэтому я решил собрать небольшую статью вместе с несколькими простыми примерами.

Тень DOM

Прежде чем я опишу разницу между режимами open и closed, важно понять, что такое Shadow DOM и для чего он нужен.

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

class MyWebComponent extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: "open" });
    }
    connectedCallback() {
        this.shadowRoot.innerHTML = `
            <p>I'm in the Shadow Root!</p>
        `;
    }
}

window.customElements.define("my-web-component", MyWebComponent);

В конструкторе мы используем метод attachShadow для создания open теневого корня для настраиваемого элемента. Затем мы можем получить доступ к недавно созданному теневому корню через свойство shadowRoot в экземпляре элемента. Мы можем взаимодействовать с теневым корнем так же, как и с любым деревом DOM. Например, мы могли бы использовать:

this.shadowRoot.querySelector("p");

Для запроса любых элементов абзаца в теневом корне.

Мы также можем использовать appendChild для добавления нового элемента в Shadow Root, или в приведенном выше примере мы просто используем innerHTML для штамповки исходного шаблона.

Если мы добавим этот настраиваемый элемент в документ и откроем инструменты разработчика Chrome, мы получим что-то вроде этого:

Мы видим, что у элемента есть открытый корень тени с элементом абзаца внутри. Но что это нам дает?

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

<head>
    <meta charset="UTF-8">
    <title>Blog Post</title>
    <script src="open-vs-closed.js"></script>
    <style>
        p {
            color: red;
        }
    </style>
</head>

Поскольку элемент абзаца в нашем компоненте находится в корне тени, он не наследует этот стиль.

Кроме того, если мы выполнили следующий JavaScript для основного объекта документа:

document.querySelector("p") // null

Результатом будет null, потому что Shadow Root не будет запрашиваться, обеспечивая желаемую инкапсуляцию.

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

Shadow DOM предлагает гораздо больше возможностей. Если вы хотите узнать больше, я рекомендую прочитать Shadow DOM v1: Self-Conolated Web Components.

Открытый режим

Теперь нам ясно, как работает Shadow DOM с точки зрения его инкапсуляции, и мы можем перейти к рассмотрению различий между открытым и закрытым режимами.

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

Когда вы создаете open Shadow Root с this.attachShadow({ mode: “open” }), как мы видели в приведенных выше примерах, вы можете использовать свойство shadowRoot для доступа к инкапсулированному дереву вложенной DOM.

В приведенном выше примере мы сделали это из определения класса Custom Element в файле connectedCallback. shadowRoot доступен в экземпляре элемента, поэтому мы можем так же легко получить к нему доступ извне из основного приложения.

const $myWebComponent = document.querySelector("my-web-component");
$myWebComponent.shadowRoot.querySelector("p").innerText = "Modified!";

Закрытый режим

closed режим Shadow DOM - интересная особенность, потому что она вызывает путаницу у пользователей, предлагая очень мало взамен.

Режим closed Shadow DOM обеспечивает ту же инкапсуляцию, что и открытый режим, но дополнительно позволяет автору компонента скрывать доступ к ShadowRoot, но не совсем - позвольте мне объяснить.

Как мы только что видели в режиме open, после вызова attachShadow ссылка на теневой корень элементов становится доступной в свойстве shadowRoot. В режиме closed этого не происходит: вы обнаружите, что attachShadow возвращает null.

let $element = document.createElement("div");
$element.attachShadow({ mode: "closed" });
$element.shadowRoot // null

Так что это значит для веб-компонентов?

Поскольку вы не можете использовать свойство shadowRoot для доступа и управления теневым корнем элемента, вам придется вручную сохранить ссылку на теневой корень самостоятельно.

class MyWebComponent extends HTMLElement {
    constructor() {
        super();
        this._root = this.attachShadow({ mode: "closed" });
    }
    connectedCallback() {
        this._root.innerHTML = `
            <p>I'm in the closed Shadow Root!</p>
        `;
    }
}
window.customElements.define("my-web-component", MyWebComponent);

Единственная разница здесь в том, что автор компонента имеет контроль над тем, как теневой корень отображается в элементе:

const $myWebComponent = document.querySelector("my-web-component");
$myWebComponent.shadowRoot // null
$myWebComponent._root // shadow-root (closed)

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

const $myWebComponent = document.querySelector("my-web-component");
$myWebComponent._root.querySelector("p").innerText = "Modified!";

Конечно, вы можете пойти еще дальше и заключить определение Custom Element в закрытие, чтобы ссылка на Shadow Root не была доступна:

(function(){
    const _shadows = new WeakMap();
    class MyWebComponent extends HTMLElement {
        constructor() {
            super();
            _shadows.set(this, this.attachShadow({ mode: "closed" }));
        }
        connectedCallback() {
            _shadows.get(this).innerHTML = `
            <p>I'm in the closed Shadow Root!</p>
        `;
        }
    }

    window.customElements.define("my-web-component", MyWebComponent);
})();

Но на самом деле ничто не мешает кому-то выполнить следующий JavaScript перед определением вашего компонента.

Element.prototype._attachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function () {
    return this._attachShadow( { mode: "open" } );
};

Это перехватит родной attachShadow метод и заставит каждый attachShadow вызов всегда создавать открытый теневой корень, оставляя якобы закрытый теневой корень открытым для манипуляций.

Заключение

Режим closed Shadow DOM имеет единственное преимущество, которое заключается в том, что авторы компонентов могут контролировать, как Shadow Root их компонента открывается (если вообще) внешнему миру.

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

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