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 bunun sayfa tarafından kullanılan mevcut bir adla çakışıp çakışmayacağı belli değildir. 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'yi 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, listede yer alan ancak artık desteği sonlandırılmış bir işlevdir.

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. Yeni bir HTML (JS API ile) oluşturmak için özel öğeler kullanılıyorsa HTML ve CSS'sini sağladığınız yöntem gölge DOM'dur. Bu iki API birleşerek bağımsız HTML, CSS ve JavaScript'e sahip bir bileşen oluşturur.

Gölge DOM, bileşen tabanlı uygulamalar oluşturmak için 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ını kullanabileceğiniz ve adlandırma çakışmaları konusunda endişelenmenize gerek olmadığı anlamına gelir.
  • Üretkenlik: Uygulamaları tek bir büyük (global) sayfa yerine DOM parçaları halinde düşünün.

fancy-tabs demosu

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 aşağıda canlı bir demo göreceksiniz. 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 kullanışlı 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üklediğinde birçok ilginç şey 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) ayrıştı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 ve hepsinden önemlisi, programlar tarafından değiştirilebilir. Bu nedenle, JavaScript kullanarak doğrudan DOM öğeleri 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ölgelerde

Gölge DOM, iki farkı olan normal bir DOM'dur: 1) nasıl oluşturulduğu/kullanıldığı ve 2) sayfanın geri kalanına göre nasıl davrandığı. 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. Kapsamı belirlenmiş bu alt ağaç, gölge ağacı olarak adlandırılır. 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, şu yöntem 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: Bir özel öğe kendisine gölge DOM'u ekler ve DOM/CSS'yi kapsar:

// 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 bazı ilginç şeyler oluyor. Bunlardan ilki, <fancy-tabs> örneği oluşturulduğunda özel öğenin kendi gölge DOM'unu oluşturmasıdır. Bu işlem constructor() içinde yapılabilir. İ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ünyamızda beste, uygulamalarımızı açıkça HTML'den oluşturma şeklimizdir. Farklı yapı taşları (<div>, <header>, <form>, <input>) bir araya gelerek uygulamaları oluşturur. Bu etiketlerin bazıları birbiriyle bile birlikte çalışır. <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 da sihirli.

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

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

Hafif DOM

Bileşeninizin bir kullanıcısının yazdığı 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ğıtması ve nihai ürünü oluşturması sonucunda ortaya çıkan sonuç. 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. Kavram olarak, dağıtılmış düğümler 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ış alanlar, kullanıcıların adlarıyla referans verdiği gölge DOM'unuzda bulunan belirli deliklerdir.

Ö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ı unutmayın. Dilerseniz <button> yerine <h2> yapabiliriz. 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 tanımlı stiller

Gölge DOM'un en yararlı özelliği kapsamlı CSS'dir:

  • Dış sayfadaki CSS seçiciler, 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'sinde kullanılan CSS seçiciler 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 içinde en iyi uygulama, daha basit CSS seçicilerdir. 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ı gölge ağacına da ayarlanır:

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

multiple özelliğini eklediğinizde <select> öğesinin, açılır liste yerine çoklu seçim widget'ını nasıl oluşturduğunu hiç merak ettiniz:

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

<select>, beyan ettiğiniz özelliklere göre kendisini farklı şekillerde biçimlendirebiliyor. 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 iyi bir yaklaşımdır.

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

::slotted(<compound-selector>), bir <slot> öğesine 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, sekme başlıkları için adlandırılmış bir alan ve sekme paneli içeriği için bir alan olmak üzere iki alan vardır. Kullanıcı bir sekme seçtiğinde, seçimini kalın hale getirir ve ilgili 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şeni dışarıdan şekillendirme

Dışarıdan bir bileşene stil eklemenin 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 kendisine stil kazandırmak yeterli olacaktır. Peki bir bileşenin iç kısımlarına stil uygulamak isterseniz ne olur? Bunun için CSS özel özelliklerine ihtiyacımız var.

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

Bileşenin yazarı, CSS özel özelliklerini kullanarak stil kancaları sağlıyorsa kullanıcılar dahili stilleri değiştirebilir. Kavramsal olarak bu fikir <slot> ile benzerdir. Kullanıcıların geçersiz kılması için "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ı tarafından sağlandığı için bileşen arka plan değeri olarak black kullanır. Aksi takdirde varsayılan olarak #9E9E9E olur.

İleri düzey konular

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

"Kapalı" mod adı verilen başka bir gölge DOM seçeneği de vardır. Kapalı gölge ağacı oluşturduğunuzda dış JavaScript, bileşeninizin dahili DOM'sine erişemez. Bu, <video> gibi yerel öğelerin çalışma şekline benzer. JavaScript, <video> gölge DOM'sini kapalı mod gölge kökü kullanarak uyguladığı için JavaScript'e erişemiyor.

Ö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 asla web bileşenleri oluşturmamanız gerektiğiyle ilgili özetimi burada bulabilirsiniz:

  1. Yapay güvenlik duygusu. Hiçbir şey saldırganların Element.prototype.attachShadow alanını ele geçirmesini engelleyemez.

  2. Kapalı mod, özel öğe kodunuzun kendi gölge DOM'sine erişmesini engeller. Bu tamamen başarısız bir sonuç. Bunun yerine, querySelector() gibi öğeleri kullanmak isterseniz daha sonra kullanmak üzere bir referans saklamanı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 özellik eklemeyi unutacağınız durumlar olacaktır. Bir yapılandırma seçeneği. Kullanıcının istediği bir kullanım alanı. Dahili düğümler için uygun biçimlendirme kancalarını eklemeyi unutmak yaygın bir örnektir. 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'deki alanlarla çalışma

Gölge DOM API, slotlar ve dağıtılmış düğümlerle çalışmak için yardımcı programlar sunar. 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 slotla 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).

Örnek olarak, gölge DOM'nizin aşağıdaki gibi göründüğünü varsayalım:

<slot><b>fallback content</b></slot>
KullanımTelefonSonuç
<my-component>bileşen metni</my-component> slot.assignedNodes(); [component text]
<my-component></my-component> slot.assignedNodes(); []
&lt;my-component&gt;&lt;/my-component&gt; 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 ilerlediği bir düğüm dizisi döndürür.

Özel etkinlikleri kullanma

Gölge ağacındaki dahili düğümlerde tetiklenen özel DOM etkinlikleri, composed: true işareti 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çindeki bir <input> öğesini tıkladığını varsayalım:

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

focus etkinliği, <input> yerine <x-focus> kaynağından 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: gerçek 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ıldı.
delegatesFocus: false ve x-focus odaklanır (ör. tabindex=&#39;0&#39; değerine sahiptir).
delegatesFocus: false ve <x-focus> odak kazanır (ör. tabindex="0" öğesine sahiptir).
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'si içindeki başka bir 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ından 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/stil/yapısı kendiliğinden 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ılabilir stilleri sıfırlama

Devralınabilir stiller (background, color, font, line-height vb.) gölge DOM'de 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ı olur. 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> şablonundan öğe oluşturma

.innerHTML kullanarak bir gölge kökü doldurmak yerine açıklayıcı bir <template> kullanabiliriz. Şablonlar, web bileşeninin yapısını açıklamak 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ğladı (v1'in element.attachShadow yöntemi yerine element.createShadowRoot). Eski yöntem çağrıldığında, v0 semantiğiyle gölge kökü oluşturmaya devam edilir. 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 karşılaştırmasını da bulabilirsiniz.

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.

Polyfill'leri takın:

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 birleştiğinde gölge DOM, tam anlamıyla kapsüllenmiş bileşenleri hack'ler olmadan veya <iframe> gibi eski bagajları kullanarak oluşturmak için bir yöntem sunar.

Beni yanlış anlamayın. Gölge DOM kesinlikle karmaşık bir konudur. Ancak öğrenmeye değer bir canavardır. Biraz vakit geçirin. Öğrenin ve soru sorun!

Daha fazla bilgi

SSS

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

Çoklu dolgu ile evet. Tarayıcı desteği sayfasına göz atın.

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

Shadow DOM bir güvenlik özelliği değildir. CSS'nin kapsamını belirlemek ve DOM ağaçlarını bileşen olarak gizlemek için 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 sarmalaması 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 konusuna bakın.