Gölge DOM v1 - Bağımsız Web Bileşenleri

Gölge DOM, web geliştiricilerin web bileşenleri için bölümlendirilmiş DOM ve CSS oluşturmasına olanak tanır.

Özet

Gölge DOM, web uygulamaları geliştirmenin zayıflığını ortadan kaldırır. Bu kararsızlık, HTML, CSS ve JS'nin küresel yapısından kaynaklanır. Yıllar içinde, bu sorunları aşmak için çok sayıda aracı geliştirdik . Örneğin, yeni bir HTML kimliği/sınıfı kullandığınızda bu kimliğin sayfa tarafından kullanılan mevcut bir adla çakışıp çakışmayacağı bilinemez. Belirgin olmayan hatalar ortaya çıkar, CSS özgülüğü büyük bir sorun haline gelir (!important her şey!), stil seçicileri kontrolden çıkar ve performans zarar görebilir. Liste bu şekilde uzayıp gidiyor.

Gölge DOM, CSS ve DOM'u düzeltir. Web platformuna kapsamlı stiller sunar. Araçlar veya adlandırma kuralları olmadan, CSS'yi işaretlemeyle bir araya getirebilir, uygulama ayrıntılarını gizleyebilir ve normal JavaScript'de kendi kendine yeten bileşenler oluşturabilirsiniz.

Giriş

Gölge DOM, üç Web Bileşeni standardından biridir: HTML Şablonları, Gölge DOM ve Özel öğeler. HTML içe aktarma, listenin bir parçasıydı ancak artık desteği sonlandırılmış olarak kabul ediliyor.

Gölge DOM kullanan web bileşenleri oluşturmanız gerekmez. Ancak bunu yaptığınızda, avantajlarından (CSS kapsamı, DOM sarmalaması, kompozisyon) yararlanır ve esnek, yüksek oranda yapılandırılabilir ve son derece yeniden kullanılabilir özel öğeler oluşturursunuz. Özel öğeler yeni bir HTML oluşturmanın yoluysa (JS API ile) gölge DOM, HTML ve CSS'sini sağlamanın yoludur. Bu iki API, kendi kendine yeten HTML, CSS ve JavaScript içeren bir bileşen oluşturmak için birleştirilir.

Gölge DOM, bileşen tabanlı uygulamalar oluşturmaya yönelik bir araç olarak tasarlanmıştır. Bu nedenle, web geliştirmede sık karşılaşılan sorunlara çözümler sunar:

  • İzole DOM: Bileşenin DOM'u kendi kendine yeter (ör. document.querySelector(), bileşenin gölge DOM'undaki düğümleri döndürmez).
  • Kapsamlı CSS: Gölge DOM içinde tanımlanan CSS, bu DOM'un kapsamına alınır. Stil kuralları dışarı sızmaz ve sayfa stilleri içeriye sızmaz.
  • Kompozisyon: Bileşeniniz için açıklayıcı, işaretlemeye dayalı bir API tasarlayın.
  • CSS'yi basitleştirir: Kapsamlı DOM, basit CSS seçicileri ve daha genel kimlik/sınıf adları kullanabileceğiniz ve adlandırma çakışmaları konusunda endişelenmenize gerek olmadığı anlamına gelir.
  • Üretkenlik: Uygulamaları tek bir büyük (evrensel) sayfa yerine DOM parçaları olarak düşünün.

fancy-tabs demo

Bu makale boyunca bir demo bileşeninden (<fancy-tabs>) bahsedecek ve bu bileşendeki kod snippet'lerine referans vereceğim. Tarayıcınız API'leri destekliyorsa hemen aşağıda canlı bir demo görürsünüz. Aksi takdirde, GitHub'daki kaynak kodun tamamına göz atın.

Kaynağı GitHub'da görüntüleyin

Gölge DOM nedir?

DOM hakkında bilgi

HTML, kullanımı kolay olduğu için web'i destekler. Birkaç etiket tanımlayarak hem sunum hem de yapıya sahip bir sayfayı saniyeler içinde oluşturabilirsiniz. Ancak HTML tek başına çok faydalı değildir. Metne dayalı bir dili insanların anlaması kolaydır ancak makinelerin daha fazlasına ihtiyacı vardır. Belge Nesne Modeli'ni (DOM) girin.

Tarayıcı, bir web sayfasını yüklerken birçok ilginç işlem yapar. Bu işlemlerden biri, yazarın HTML'sini canlı bir dokümana dönüştürmektir. Temel olarak tarayıcı, sayfanın yapısını anlamak için HTML'yi (statik metin dizeleri) bir veri modeline (nesneler/düğümler) ayırır. Tarayıcı, bu düğümlerden bir ağaç oluşturarak HTML'nin hiyerarşisini korur: DOM. DOM'un en iyi yanı, sayfanızın canlı bir temsili olmasıdır. Yazdığımız statik HTML'den farklı olarak tarayıcı tarafından üretilen düğümler özellikler ve yöntemler içerir. En iyisi de programlar tarafından değiştirilebilir. Bu nedenle, DOM öğelerini doğrudan JavaScript kullanarak oluşturabiliyoruz:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

aşağıdaki HTML işaretlemesini oluşturur:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

Bu iyi bir gelişme. Peki gölge DOM nedir?

DOM… gölgede

Gölge DOM, normal DOM'dan iki açıdan farklıdır: 1) Oluşturma/kullanım şekli ve 2) Sayfanın geri kalanıyla ilişkili davranışı. Normalde DOM düğümleri oluşturur ve bunları başka bir öğenin çocuğu olarak eklersiniz. Gölge DOM ile, öğeye bağlı ancak gerçek alt öğelerinden ayrı bir kapsamlı DOM ağacı oluşturursunuz. Bu kapsamlı alt ağaca gölge ağaç denir. Bu öğenin bağlı olduğu öğe, gölge ana makinesidir. Gölgelere eklediğiniz her şey, <style> dahil olmak üzere barındıran öğeye yerel olur. Gölge DOM, CSS stil kapsamını bu şekilde belirler.

Gölge DOM oluşturma

Gölge kök, bir "ana makine" öğesine eklenen bir doküman parçasıdır. Öğe, gölge kökü ekleyerek gölge DOM'unu kazanır. Bir öğe için gölge DOM oluşturmak üzere element.attachShadow() işlevini çağırın:

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

Gölge kökü doldurmak için .innerHTML kullanıyorum ancak diğer DOM API'lerini de kullanabilirsiniz. Bu, web'dir. Seçimimiz var.

Spesifikasyon, gölge ağacı barındıramayan bir öğe listesi tanımlar. Bir öğenin listede olmasının birkaç nedeni vardır:

  • Tarayıcı, öğe için zaten kendi dahili gölge DOM'unu barındırıyor (<textarea>, <input>).
  • Öğenin bir gölge DOM (<img>) barındırması anlamlı değil.

Örneğin, aşağıdakiler işe yaramaz:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

Özel öğe için gölge DOM oluşturma

Gölge DOM, özellikle özel öğeler oluştururken kullanışlıdır. Bir öğenin HTML, CSS ve JS'sini bölümlere ayırmak ve böylece bir "web bileşeni" oluşturmak için gölge DOM'u kullanın.

Örnek: Özel bir öğe, DOM/CSS'sini kapsayarak gölge DOM'u kendisine bağlar:

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
        <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
        <div id="tabs">...</div>
        <div id="panels">...</div>
    `;
    }
    ...
});

Burada ilginç birkaç şey var. Bunlardan ilki, <fancy-tabs> örneği oluşturulduğunda özel öğenin kendi gölge DOM'unu oluşturmasıdır. Bu işlem constructor()'te yapılır. İkinci olarak, gölge kök oluşturduğumuz için <style> içindeki CSS kurallarının kapsamı <fancy-tabs> olarak belirlenir.

Kompozisyon ve yuvalar

Oluşturma, gölge DOM'un en az anlaşılan özelliklerinden biridir ancak tartışmasız en önemlisidir.

Web geliştirme dünyasında, kompozisyon, uygulamaları HTML'den açık bir şekilde oluşturma şeklimizdir. Farklı yapı taşları (<div>, <header>, <form>, <input>) bir araya gelerek uygulamaları oluşturur. Bu etiketlerden bazıları birlikte bile kullanılabilir. <select>, <details>, <form> ve <video> gibi yerel öğelerin bu kadar esnek olmasının nedeni kompozisyondur. Bu etiketlerin her biri, belirli HTML'leri alt öğe olarak kabul eder ve bunlarla özel bir işlem yapar. Örneğin, <select>, <option> ve <optgroup> öğelerini açılır menü ve çoklu seçim widget'ları olarak nasıl oluşturacağını bilir. <details> öğesi, <summary> öğesini genişletilebilir bir ok olarak gösterir. <video> bile belirli çocuklarla nasıl başa çıkacağını biliyor: <source> öğeleri oluşturulmaz ancak videonun davranışını etkiler. Ne kadar büyüleyici!

Terminoloji: ışık DOM ve gölge DOM

Gölge DOM kompozisyonu, web geliştirmede bir dizi yeni temel kavram sunar. Ayrıntılara girmeden önce, aynı dili konuşmak için bazı terimleri standartlaştıralım.

Hafif DOM

Bileşeninizin kullanıcısı tarafından yazılan işaretleme. Bu DOM, bileşenin gölge DOM'unun dışındadır. Öğenin gerçek alt öğeleridir.

<better-button>
    <!-- the image and span are better-button's light DOM -->
    <img src="gear.svg" slot="icon">
    <span>Settings</span>
</better-button>

Gölge DOM

Bileşen yazarının yazdığı DOM. Gölge DOM, bileşene özgüdür ve dahili yapısını, kapsamlı CSS'sini tanımlar ve uygulama ayrıntılarınızı kapsar. Ayrıca, bileşeninizin tüketicisi tarafından oluşturulan işaretlemenin nasıl oluşturulacağını da tanımlayabilir.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

Düzleştirilmiş DOM ağacı

Tarayıcının, kullanıcının ışık DOM'unu gölge DOM'unuza dağıtarak nihai ürünü oluşturması. DevTools'ta gördüğünüz ve sayfa üzerinde oluşturulan şey düzleştirilmiş ağaçtır.

<better-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="gear.svg" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
        <span>Settings</span>
        </slot>
    </span>
</better-button>

<slot> öğesi

Gölge DOM, <slot> öğesini kullanarak farklı DOM ağaçlarını birlikte oluşturur. Yuvalar, bileşeninizdeki kullanıcıların kendi işaretlemeleriyle doldurabileceği yer tutuculardır. Bir veya daha fazla yuva tanımlayarak harici işaretlemeyi bileşeninizin gölge DOM'unda oluşturmaya davet edersiniz. Özetle, "Kullanıcı işaretlemesini burada oluştur" demiş olursunuz.

Bir <slot> öğeleri davet ettiğinde öğelerin gölge DOM sınırını "aşmasına" izin verilir. Bu öğelere dağıtılmış düğümler denir. Dağıtılmış düğümler kavramsal olarak biraz tuhaf görünebilir. Yuvalar DOM'u fiziksel olarak taşımaz, gölge DOM'un içinde başka bir konumda oluşturur.

Bir bileşen, gölge DOM'unda sıfır veya daha fazla yuva tanımlayabilir. Slotlar boş olabilir veya ikame içerik sağlayabilir. Kullanıcı ışık DOM içeriği sağlamazsa slot, yedek içeriğini oluşturur.

<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>

<slot>fallback content</slot> <!-- default slot with fallback content -->

<slot> <!-- default slot entire DOM tree as fallback -->
    <h2>Title</h2>
    <summary>Description text</summary>
</slot>

Adlandırılmış aralık da oluşturabilirsiniz. Adlandırılmış yuvalar, gölge DOM'unuzda kullanıcıların adla referans verdiği belirli boşluklardır.

Örnek: <fancy-tabs> bileşeninin gölge DOM'undaki yuvalar:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

Bileşen kullanıcıları <fancy-tabs> öğesini şu şekilde tanımlar:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

Düzleştirilmiş ağaç aşağıdaki gibi görünür:

<fancy-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="panels">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</fancy-tabs>

Bileşenimizin farklı yapılandırmaları işleyebildiğini ancak düzleştirilmiş DOM ağacının aynı kaldığını fark edin. <button> yerine <h2>'e de geçebiliriz. Bu bileşen, <select> gibi farklı çocuk türlerini ele alacak şekilde yazılmıştır.

Stil

Web bileşenlerine stil uygulamak için birçok seçenek vardır. Gölge DOM kullanan bir bileşen, ana sayfa tarafından stillendirilebilir, kendi stillerini tanımlayabilir veya kullanıcıların varsayılanları geçersiz kılması için kanca (CSS özel mülkleri biçiminde) sağlayabilir.

Bileşen tarafından tanımlanan stiller

Gölge DOM'un en kullanışlı özelliği şüphesiz kapsamlı CSS'dir:

  • Dış sayfadaki CSS seçicileri bileşeninizin içinde geçerli değildir.
  • İçinde tanımlanan stiller dışarı taşmaz. Bunlar, barındırma öğesi kapsamına sahiptir.

Gölge DOM'da kullanılan CSS seçicileri, bileşeninize yerel olarak uygulanır. Pratikte bu, sayfanın başka yerlerinde çakışmalar olup olmayacağı konusunda endişelenmeden ortak kimlik/sınıf adlarını tekrar kullanabileceğimiz anlamına gelir. Gölge DOM'da daha basit CSS seçiciler kullanmak en iyi uygulamadır. Ayrıca performansı artırmaya da yardımcı olur.

Örnek: Gölge kökünde tanımlanan stiller yereldir

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Stil sayfaları da gölge ağacına göre kapsamlandırılır:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

multiple özelliğini eklediğinizde <select> öğesinin nasıl bir çoklu seçim widget'ı (açılır menü yerine) oluşturduğunu merak ettiniz mi?

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

<select>, üzerinde tanımladığınız özelliklere bağlı olarak kendisini farklı şekilde biçimlendirebilir. Web bileşenleri, :host seçiciyi kullanarak kendi stillerini de belirleyebilir.

Örnek: Kendi stilini belirleyen bir bileşen

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

:host ile ilgili bir sorun, üst sayfada bulunan kuralların, öğede tanımlanan :host kurallarına kıyasla daha yüksek özgürlüğe sahip olmasıdır. Yani dış stiller kazanır. Bu, kullanıcıların üst düzey stilinizi dışarıdan geçersiz kılmasına olanak tanır. Ayrıca :host yalnızca gölge kök bağlamında çalışır. Bu nedenle, gölge DOM dışında kullanamazsınız.

:host(<selector>) işlevsel biçimi, bir <selector> ile eşleşirse düzenleyeni hedeflemenize olanak tanır. Bu, bileşeninizin kullanıcı etkileşimine tepki veren veya ana makineye göre dahili düğümleri duruma ya da stile göre kapsüllemesi için mükemmel bir yöntemdir.

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

Bağlama göre stil uygulama

:host-context(<selector>), kendisi veya üst öğelerinden herhangi biri <selector> ile eşleşirse bileşenle eşleşir. Bu özelliğin yaygın bir kullanımı, bileşenin çevresine göre tema oluşturmaktır. Örneğin, birçok kullanıcı <html> veya <body> öğesine sınıf uygulayarak tema oluşturma işlemini gerçekleştirir:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

:host-context(.darktheme), .darktheme öğesinin alt öğesi olduğunda <fancy-tabs> öğesine stil uygular:

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() tema oluşturmak için yararlı olabilir ancak CSS özel özelliklerini kullanarak stil kancaları oluşturmak daha da iyi bir yaklaşımdır.

Dağıtılmış düğümlere stil uygulama

::slotted(<compound-selector>), <slot> içine dağıtılan düğümlerle eşleşir.

Bir ad rozeti bileşeni oluşturduğumuzu varsayalım:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

Bileşenin gölge DOM'u, kullanıcının <h2> ve .title öğelerini biçimlendirebilir:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

Daha önce de belirttiğimiz gibi, <slot> öğeleri kullanıcının hafif DOM'unu hareket ettirmez. Düğümler bir <slot>'e dağıtıldığında <slot>, DOM'lerini oluşturur ancak düğümler fiziksel olarak yerinde kalır. Dağıtımdan önce uygulanan stiller, dağıtımdan sonra da uygulanmaya devam eder. Ancak ışık DOM dağıtıldığında ek stiller (gölge DOM tarafından tanımlananlar) alabilir.

<fancy-tabs>'ten daha ayrıntılı bir örnek:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        border-radius: 3px;
        padding: 16px;
        height: 250px;
        overflow: auto;
    }
    #tabs {
        display: inline-flex;
        -webkit-user-select: none;
        user-select: none;
    }
    #tabsSlot::slotted(*) {
        font: 400 16px/22px 'Roboto';
        padding: 16px 8px;
        ...
    }
    #tabsSlot::slotted([aria-selected="true"]) {
        font-weight: 600;
        background: white;
        box-shadow: none;
    }
    #panelsSlot::slotted([aria-hidden="true"]) {
        display: none;
    }
    </style>
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
`;

Bu örnekte iki alan vardır: sekme başlıkları için adlandırılmış bir alan ve sekme paneli içeriği için bir alan. Kullanıcı bir sekme seçtiğinde, seçimini kalınlaştırır ve panelini gösteririz. Bu işlem, selected özelliğine sahip dağıtılmış düğümler seçerek yapılır. Özel öğenin JS'si (burada gösterilmez) bu özelliği doğru zamanda ekler.

Bir bileşene dışarıdan stil uygulama

Bir bileşene dışarıdan stil uygulamanın birkaç yolu vardır. En kolay yol, etiket adını seçici olarak kullanmaktır:

fancy-tabs {
    width: 500px;
    color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
    box-shadow: 0 3px 3px #ccc;
}

Dış stiller, gölge DOM'da tanımlanan stillere göre her zaman önceliklidir. Örneğin, kullanıcı fancy-tabs { width: 500px; } seçicisini yazarsa bu, bileşenin kuralını (:host { width: 650px;}) geçersiz kılar.

Bileşenin stilini belirlemek, istediğiniz görünüme ulaşmak için yeterli olmayabilir. Peki bir bileşenin iç kısımlarına stil uygulamak isterseniz ne olur? Bunun için CSS özel mülklerine ihtiyacımız var.

CSS özel özelliklerini kullanarak stil kancaları oluşturma

Bileşenin yazarı CSS özel özelliklerini kullanarak stil bağlantıları sağlarsa kullanıcılar dahili stilleri değiştirebilir. Kavramsal olarak bu fikir <slot>'e benzer. Kullanıcıların geçersiz kılabileceği "stil yer tutucuları" oluşturursunuz.

Örnek: <fancy-tabs>, kullanıcıların arka plan rengini geçersiz kılmasına olanak tanır:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

Gölge DOM'u içinde:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

Bu durumda, kullanıcı sağladığı için bileşen arka plan değeri olarak black değerini kullanır. Aksi takdirde varsayılan olarak #9E9E9E olur.

Gelişmiş konular

Kapalı gölge kökleri oluşturma (kaçınılmalıdır)

"Kapalı" modu adı verilen başka bir gölge DOM çeşidi vardır. Kapalı bir gölge ağacı oluşturduğunuzda, JavaScript dışındaki bileşenler bileşeninizin dahili DOM'una erişemez. Bu, <video> gibi yerel öğelerin işleyiş şekline benzer. Tarayıcı, <video> öğesini kapalı modlu gölge kök kullanarak uyguladığından JavaScript, <video> öğesinin gölge DOM'una erişemez.

Örnek: Kapalı gölge ağacı oluşturma:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

Kapalı mod diğer API'leri de etkiler:

  • Element.assignedSlot / TextNode.assignedSlot, null değerini döndürür
  • Gölge DOM'daki öğelerle ilişkili etkinlikler için Event.composedPath(), [] döndürür

{mode: 'closed'} ile neden hiçbir zaman web bileşeni oluşturmamanız gerektiğini özetlemek isterim:

  1. Yapay güvenlik duygusu. Saldırganların Element.prototype.attachShadow'yi ele geçirmesini engelleyen hiçbir şey yoktur.

  2. Kapalı mod, özel öğe kodunuzun kendi gölge DOM'una erişmesini engeller. Bu tamamen başarısız bir sonuç. Bunun yerine, querySelector() gibi öğeleri kullanmak istiyorsanız daha sonra referans olarak kullanabileceğiniz bir yer ayırmanız gerekir. Bu, kapalı modun asıl amacını tamamen geçersiz kılar.

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. Kapalı mod, bileşeninizi son kullanıcılar için daha az esnek hale getirir. Web bileşenleri oluştururken bir özelliği eklemeyi unutabilirsiniz. Bir yapılandırma seçeneği. Kullanıcının istediği bir kullanım alanı. Bunun yaygın bir örneği, dahili düğümler için yeterli stil kancaları eklemeyi unutmaktır. Kapalı modda, kullanıcıların varsayılan ayarları geçersiz kılma ve stilleri değiştirmesi mümkün değildir. Bileşenin iç yapısına erişebilmek çok faydalıdır. Kullanıcılar, istedikleri işlevi görmüyorsa bileşeninizi çatallayacak, başka bir bileşen bulacak veya kendi bileşenlerini oluşturacaktır :(

JS'de slotlarla çalışma

Gölge DOM API'si, yuvalar ve dağıtılmış düğümlerle çalışmak için yardımcı programlar sağlar. Bunlar, özel bir öğe yazarken kullanışlıdır.

slotchange etkinliği

slotchange etkinliği, bir yuvanın dağıtılan düğümleri değiştiğinde tetiklenir. Örneğin, kullanıcı hafif DOM'a çocuk ekler/kaldırırsa.

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

Light DOM'da yapılan diğer tür değişiklikleri izlemek için öğenizin kurucusunda bir MutationObserver oluşturabilirsiniz.

Bir yuvada hangi öğeler oluşturuluyor?

Bazen bir yuvayla ilişkili öğeleri bilmek yararlı olabilir. Yuvanın hangi öğeleri oluşturduğunu bulmak için slot.assignedNodes() işlevini çağırın. {flatten: true} seçeneği, bir yuvanın yedek içeriğini de döndürür (dağıtılan düğüm yoksa).

Örneğin, gölge DOM'unuzun aşağıdaki gibi olduğunu varsayalım:

<slot><b>fallback content</b></slot>
KullanımTelefonSonuç
<my-component>component text</my-component> slot.assignedNodes(); [component text]
<my-component></my-component> slot.assignedNodes(); []
<my-component></my-component> slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

Öğeler hangi yuvaya atanır?

Ters soruyu yanıtlamak da mümkündür. element.assignedSlot, öğenizin hangi bileşen yuvasına atandığını belirtir.

Gölge DOM etkinlik modeli

Bir etkinlik gölge DOM'dan yukarı doğru ilerlediğinde hedefi, gölge DOM'un sağladığı kapsüllemeyi korumak için ayarlanır. Yani etkinlikler, gölge DOM'unuzdaki dahili öğeler yerine bileşenden gelmiş gibi görünecek şekilde yeniden hedeflenir. Bazı etkinlikler gölge DOM'dan bile yayılmaz.

Gölge sınırını aşan etkinlikler şunlardır:

  • Odak etkinlikleri: blur, focus, focusin, focusout
  • Fare etkinlikleri: click, dblclick, mousedown, mouseenter, mousemove vb.
  • Tekerlek Etkinlikleri: wheel
  • Giriş etkinlikleri: beforeinput, input
  • Klavye Etkinlikleri: keydown, keyup
  • Beste Etkinlikleri: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop vb.

İpuçları

Gölge ağacı açıksa event.composedPath() çağrısı, etkinliğin geçtiği bir düğüm dizisi döndürür.

Özel etkinlikleri kullanma

Gölge ağacındaki dahili düğümlerde tetiklenen özel DOM etkinlikleri, etkinlik composed: true işaretçisi kullanılarak oluşturulmadığı sürece gölge sınırının dışına çıkmaz:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

composed: false (varsayılan) ise tüketiciler, gölge kökünüzün dışındaki etkinliği dinleyemez.

<fancy-tabs></fancy-tabs>
<script>
    const tabs = document.querySelector('fancy-tabs');
    tabs.addEventListener('tab-select', e => {
    // won't fire if `tab-select` wasn't created with `composed: true`.
    });
</script>

Odağı yönetme

Gölge DOM'un etkinlik modelinden hatırlarsanız gölge DOM içinde tetiklenen etkinlikler, barındıran öğeden geliyormuş gibi ayarlanır. Örneğin, gölge kök içinde bir <input> tıkladığınızı varsayalım:

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

focus etkinliği, <input> yerine <x-focus>'ten gelmiş gibi görünür. Benzer şekilde, document.activeElement <x-focus> olur. Gölge kökü mode:'open' ile oluşturulduysa (kapalı mod bölümüne bakın) odak alan dahili düğüme de erişebilirsiniz:

document.activeElement.shadowRoot.activeElement // only works with open mode.

Birden fazla gölge DOM seviyesi varsa (ör. başka bir özel öğe içindeki özel öğe) activeElement öğesini bulmak için gölge köklerine yinelemeli olarak girmeniz gerekir:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

Odak için başka bir seçenek de delegatesFocus: true seçeneğidir. Bu seçenek, gölge ağacındaki öğelerin odak davranışını genişletir:

  • Gölge DOM'daki bir düğümü tıklarsanız ve düğüm odaklanılabilir bir alan değilse ilk odaklanılabilir alan odaklanır.
  • Gölge DOM'daki bir düğüm odaklandığında :focus, odaklanan öğeye ek olarak ana makine için de geçerli olur.

Örnek: delegatesFocus: true, odak davranışını nasıl değiştirir?

<style>
    :focus {
    outline: 2px solid red;
    }
</style>

<x-focus></x-focus>

<script>
customElements.define('x-focus', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    const root = this.attachShadow({mode: 'open', delegatesFocus: true});
    root.innerHTML = `
        <style>
        :host {
            display: flex;
            border: 1px dotted black;
            padding: 16px;
        }
        :focus {
            outline: 2px solid blue;
        }
        </style>
        <div>Clickable Shadow DOM text</div>
        <input type="text" placeholder="Input inside shadow dom">`;

    // Know the focused element inside shadow DOM:
    this.addEventListener('focus', function(e) {
        console.log('Active element (inside shadow dom):',
                    this.shadowRoot.activeElement);
    });
    }
});
</script>

Sonuç

delegatesFocus: Doğru davranış.

Yukarıda, <x-focus> odaklandığında (kullanıcı tıklaması, sekmeyle açma, focus() vb.) elde edilen sonuç gösterilmektedir. "Tıklanabilir Gölge DOM metni" tıklanır veya dahili <input> (autofocus dahil) odaklanır.

delegatesFocus: false değerini ayarlarsanız bunun yerine şunu görürsünüz:

delegatesFocus: false ve dahili giriş odaklıdır.
delegatesFocus: false ve dahili <input> odaklanır.
delegatesFocus: false ve x-focus odaklanır (ör. tabindex=&#39;0&#39; değerine sahiptir).
delegatesFocus: false ve <x-focus> odaklanır (ör. tabindex="0" içerir).
delegatesFocus: false ve &quot;Tıklanabilir Gölge DOM metni&quot; tıklanır (veya öğenin gölge DOM&#39;undaki diğer boş alan tıklanır).
delegatesFocus: false ve "Tıklanabilir Gölge DOM metni" tıklanır (veya öğenin gölge DOM'undaki diğer boş alan tıklanır).

İpuçları ve Püf Noktaları

Yıllar içinde web bileşenleri oluşturma hakkında bir iki şey öğrendim. Bu ipuçlarının bazılarını bileşen oluşturmak ve gölge DOM'da hata ayıklama için faydalı bulacağınızı düşünüyorum.

CSS kapsayıcı kullanma

Genellikle bir web bileşeninin düzeni/stili/boyası oldukça bağımsızdır. Performans kazancı için :host içinde CSS kapsayıcı kullanın:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

Devralınabilir stilleri sıfırlama

Devralınabilir stiller (background, color, font, line-height vb.) gölge DOM'da devralmaya devam eder. Yani varsayılan olarak gölge DOM sınırını delerler. Yeni bir başlangıç yapmak istiyorsanız kalıtsal stilleri gölge sınırını aştığında ilk değerlerine sıfırlamak için all: initial; simgesini kullanın.

<style>
    div {
    padding: 10px;
    background: red;
    font-size: 25px;
    text-transform: uppercase;
    color: white;
    }
</style>

<div>
    <p>I'm outside the element (big/white)</p>
    <my-element>Light DOM content is also affected.</my-element>
    <p>I'm outside the element (big/white)</p>
</div>

<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    :host {
        all: initial; /* 1st rule so subsequent properties are reset. */
        display: block;
        background: white;
    }
    </style>
    <p>my-element: all CSS properties are reset to their
        initial value using <code>all: initial</code>.</p>
    <slot></slot>
`;
</script>

Bir sayfanın kullandığı tüm özel öğeleri bulma

Bazen sayfada kullanılan özel öğeleri bulmak yararlıdır. Bunu yapmak için, sayfada kullanılan tüm öğelerin gölge DOM'unu yinelemeli olarak incelemeniz gerekir.

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

<template> öğesinden öğe oluşturma

.innerHTML kullanarak bir gölge kökü doldurmak yerine açıklayıcı bir <template> kullanabiliriz. Şablonlar, bir web bileşeninin yapısını belirtmek için ideal bir yer tutucudur.

"Özel öğeler: yeniden kullanılabilir web bileşenleri oluşturma" başlıklı makaledeki örneği inceleyin.

Geçmiş ve tarayıcı desteği

Son birkaç yıldır web bileşenlerini takip ediyorsanız Chrome 35+/Opera'nın bir süredir gölge DOM'un eski bir sürümünü kullanıma sunduğunu biliyorsunuzdur. Blink, bir süre boyunca her iki sürümü de paralel olarak desteklemeye devam edecektir. v0 spesifikasyonu, gölge kök oluşturmak için farklı bir yöntem sağlamıştır (v1'deki element.attachShadow yerine element.createShadowRoot). Eski yöntem çağrıldığında v0 semantiğiyle bir gölge kök oluşturulmaya devam eder. Bu nedenle, mevcut v0 kodu bozulmaz.

Eski v0 spesifikasyonuyla ilgileniyorsanız html5rocks makalelerine göz atın: 1, 2, 3. Ayrıca gölge DOM v0 ile v1 arasındaki farkların mükemmel bir karşılaştırması da mevcuttur.

Tarayıcı desteği

Shadow DOM v1, Chrome 53 (durum), Opera 40, Safari 10 ve Firefox 63'te kullanıma sunulmuştur. Edge geliştirmeye başladı.

Gölge DOM'u algılamak için attachShadow öğesinin varlığını kontrol edin:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Çoklu dolgu

Tarayıcı desteği yaygın olarak kullanıma sunuluncaya kadar shadydom ve shadycss polyfill'leri size v1 özelliğini sunar. Shady DOM, gölge DOM'un DOM kapsamını taklit eder ve shadycss, CSS özel özelliklerini ve yerel API'nin sağladığı stil kapsamını çoklu doldurur.

Polifillleri yükleyin:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

Polifillleri kullanın:

function loadScript(src) {
    return new Promise(function(resolve, reject) {
    const script = document.createElement('script');
    script.async = true;
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
    });
}

// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
    loadScript('/bower_components/shadydom/shadydom.min.js')
    .then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
    .then(e => {
        // Polyfills loaded.
    });
} else {
    // Native shadow dom v1 support. Go to go!
}

Stillerinizi nasıl dolduracağınız/kapsayacak şekilde ayarlayacağınızla ilgili talimatlar için https://github.com/webcomponents/shadycss#usage adresine bakın.

Sonuç

İlk kez, doğru CSS kapsamı ve DOM kapsamı sağlayan ve gerçek bir bileşime sahip bir API ilkelimiz var. Özel öğeler gibi diğer web bileşeni API'leriyle birlikte gölge DOM, <iframe> gibi eski öğeleri kullanmadan veya hilelere başvurmadan gerçekten kapsüllenmiş bileşenler oluşturmanın bir yolunu sunar.

Beni yanlış anlamayın. Gölge DOM kesinlikle karmaşık bir konudur. Ancak öğrenmeye değer bir canavardır. Bu cihazla biraz zaman geçirin. Bu özelliği öğrenin ve soru sorun.

Daha fazla bilgi

SSS

Gölge DOM 1. sürümünü şu anda kullanabilir miyim?

Evet, polyfill ile. Tarayıcı desteği başlıklı makaleyi inceleyin.

Gölge DOM hangi güvenlik özelliklerini sağlar?

Shadow DOM bir güvenlik özelliği değildir. CSS kapsamını belirlemek ve bileşendeki DOM ağaçlarını gizlemek için kullanılan hafif bir araçtır. Gerçek bir güvenlik sınırı istiyorsanız <iframe> kullanın.

Bir web bileşeninin gölge DOM kullanması gerekir mi?

Hayır. Gölge DOM kullanan web bileşenleri oluşturmanız gerekmez. Ancak gölge DOM kullanan özel öğeler oluşturmak, CSS kapsamı, DOM sarmalama ve kompozisyon gibi özelliklerden yararlanabileceğiniz anlamına gelir.

Açık ve kapalı gölge kökleri arasındaki fark nedir?

Kapalı gölge kökleri başlıklı makaleyi inceleyin.