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

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

Özet

Gölge DOM, web uygulamaları oluşturmanın kırılganlığını ortadan kaldırır. Kırılganlık HTML, CSS ve JS'nin küresel yapısından kaynaklanır. Yıllar içinde bu sorunları atlatmak için olağanüstü bir sayıda tools icat ettik. Örneğin, yeni bir HTML kimliği/sınıfı kullandığınızda, bunun sayfa tarafından kullanılan mevcut adla çakışıp çakışmayacağını belirleyemezsiniz. Ufak tefek hatalar göze çarpar, CSS'nin belirginliği büyük bir sorun haline gelir (!important, her şey dahil), stil seçiciler kontrolden çıkıyor ve performansta düşüş görülebilir. Liste bu şekilde devam eder.

Gölge DOM, CSS ve DOM düzeltmeleri Kapsamlı stilleri web platformuna sunar. Araçlar veya adlandırma kuralları olmadan, CSS'yi işaretlemeyle bir araya getirebilir, uygulama ayrıntılarını gizleyebilir ve vanilla JavaScript ile bağımsız bileşenler yazabilirsiniz.

Giriş

Gölge DOM; üç Web Bileşeni standardından biridir: HTML Şablonları, Gölge DOM ve Özel öğeler. HTML İçe Aktarmaları eskiden listenin bir parçasıydı ancak artık kullanımdan kaldırıldı.

Gölge DOM kullanan web bileşenleri yazmanız gerekmez. Ancak bunu yaptığınızda, özelliğin avantajlarından (CSS kapsamı, DOM kapsülleme, bileşim) yararlanır ve dayanıklı, yüksek düzeyde yapılandırılabilir ve son derece yeniden kullanılabilir olan yeniden kullanılabilir özel öğeler derlersiniz. Yeni bir HTML (JS API ile) oluşturmak için özel öğeler kullanıyorsanız, HTML ve CSS'yi sağlamak için gölge DOM kullanılır. 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 sorunlar için çözümler sunar:

  • Yalıtılmış DOM: Bir bileşenin DOM'u bağımsızdır (ör. document.querySelector(), bileşenin gölge DOM'sindeki düğümleri döndürmez).
  • Kapsamlı CSS: Gölge DOM'un içinde tanımlanan CSS onun kapsamına alınır. Stil kuralları sızmaz ve sayfa stilleri taşmaz.
  • Yapı: Bileşeniniz için bildirim temelli, işaretlemeye dayalı bir API tasarlayın.
  • CSS'yi basitleştirir: Kapsamlı DOM, basit CSS seçiciler ile daha genel kimlik/sınıf adlarını kullanabileceğiniz ve adlandırma çakışmaları konusunda endişelenmenizin gerekmediği anlamına gelir.
  • Üretkenlik: Uygulamaları, büyük (genel) bir sayfa yerine DOM parçaları halinde düşünün.

fancy-tabs demo

Bu makale boyunca bir demo bileşeninden (<fancy-tabs>) bahsedecek ve bu bileşendeki kod snippet'lerinden bahsedeceğim. Tarayıcınız API'leri destekliyorsa hemen aşağıda canlı bir demosunu görebilirsiniz. Aksi takdirde, GitHub'daki kaynağın tamamını inceleyin.

Kaynağı GitHub'da göster

Gölge DOM nedir?

DOM arka planı

HTML, çalışması kolay olduğu için Web'i destekler. Birkaç etiket tanımlayarak, hem sunum hem de yapıya sahip bir sayfayı saniyeler içinde yazabilirsiniz. Ancak, HTML tek başına çok faydalı değildir. İnsanlar metin tabanlı bir dili kolayca anlayabilir, ancak makineler daha fazlasına ihtiyaç duyar. Belge Nesne Modeli'ni (DOM) girin.

Tarayıcı bir web sayfasını yüklediğinde pek çok ilginç işlem gerçekleştirir. Yaptığı şeylerden 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üğümlerin bir ağacını oluşturarak HTML hiyerarşisini korur: DOM. DOM'nin güzel yanı, sayfanızın canlı bir temsili olmasıdır. Yazdığımız statik HTML'nin aksine, tarayıcı tarafından oluşturulan düğümler özellikler ve yöntemler içerir. En önemlisi 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>

Her şey yolundadır. Öyleyse gölge DOM nedir?

Gölgelerde DOM...

Gölge DOM, iki farkı olan normal bir DOM'dir: 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 alt öğeleri olarak eklersiniz. Gölge DOM ile öğeye ekli ancak gerçek alt öğelerinden ayrı olan kapsamlı bir DOM ağacı oluşturursunuz. Bu kapsamalı alt ağaç, gölge ağacı olarak adlandırılır. Bağlı olduğu öğe, gölge ana makinesidir. Gölgelere eklediğiniz her şey, <style> dahil olmak üzere barındırma öğesinde yerel hale gelir. Gölge DOM, CSS stil kapsamını bu şekilde gerçekleştirir.

Gölge DOM oluşturuluyor

Gölge kökü, bir "barındırıcı" öğesine eklenen bir doküman parçasıdır. Bir gölge kök ekleme işlemi, öğenin gölge DOM'unu nasıl elde ettiğini gösterir. Bir öğe için gölge DOM oluşturmak üzere element.attachShadow() çağrısı yapı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ünü doldurmak için .innerHTML kullanıyorum, ancak başka DOM API'leri de kullanabilirsiniz. Bu web'dir. Seçimimiz var.

Spesifikasyon, gölge ağacı barındıramayan öğelerin listesini tanımlar. Bir öğenin listede yer almasının birkaç nedeni olabilir:

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

Örneğin, şu yöntem işe yaramaz:

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

Özel bir öğ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ırarak bir "web bileşeni" oluşturmak için gölge DOM kullanın.

Örnek: Özel bir öğe, DOM/CSS'yi içine alarak gölge DOM'u kendisine ekler:

// 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. Birincisi, bir <fancy-tabs> örneği oluşturulduğunda özel öğenin kendi gölge DOM'unu oluşturmasıdır. Bu işlem constructor() sayfasında gerçekleştirilir. İkinci olarak, bir gölge kök oluşturduğumuzdan <style> içindeki CSS kuralları <fancy-tabs> kapsamına alınır.

Beste ve alanlar

Kompozisyon, gölge DOM'nin en az anlaşılan özelliklerinden biridir, ancak muhtemelen en önemlisidir.

Web geliştirme dünyamızda, uygulamaları HTML'den yararlanarak oluşturma şeklimiz bileşimdir. Farklı yapı taşları (<div>, <header>, <form>, <input>) bir araya gelerek uygulamaları oluşturur. Bu etiketlerin bazıları birbirleriyle bile çalışır. <select>, <details>, <form> ve <video> gibi yerel öğelerin bu kadar esnek olmasının nedeni bileşimdir. Bu etiketlerin her biri, belirli HTML'yi alt öğe olarak kabul eder ve bu etiketlerle özel bir şey yapar. Örneğin <select>, <option> ve <optgroup> öğelerini açılır menülerde ve çok seçimli widget'larda nasıl oluşturacağını bilir. <details> öğesi, <summary> öğesini genişletilebilir ok olarak oluşturur. <video> bazı alt öğeler üzerinde nasıl davranacağını bilir: <source> öğeler oluşturulmaz ancak videonun davranışını etkiler. Muazzam bir sihir!

Terminoloji: ışık DOM ile gölge DOM karşılaştırması

Gölge DOM bileşimi, web geliştirmenin birçok yeni temelini kullanıma sunar. Yabani aromalara girmeden önce, aynı dili konuşabilmemiz için bazı terminolojiyi standartlaştıralım.

Hafif DOM

Bileşeninizin kullanıcısının yazdığı işaretleme. Bu DOM, bileşenin gölge DOM'sinin dışında bulunur. Öğ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

Bir bileşen yazarının yazdığı DOM. Gölge DOM, bileşen için yereldir; dahili yapısını ve kapsamlı CSS'yi tanımlar ve uygulama ayrıntılarınızı içerir. Bileşeninizin tüketicisi tarafından yazılan 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>

DOM ağacı düzleştirildi

Tarayıcının, kullanıcının ışık DOM'unu gölge DOM'nize dağıtarak nihai ürünü oluşturmasının sonucudur. Birleştirilmiş ağaç, nihai olarak Geliştirici Araçları'nda göreceğiniz ve sayfada oluşturulan 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. Slotlar, bileşeninizin içinde, kullanıcıların kendi işaretlemeleriyle doldurabileceği yer tutuculardır. Bir veya daha fazla slot tanımlayarak, dışarıdan işaretlemeyi bileşeninizin gölge DOM'unda oluşturmaya davet edersiniz. Esas olarak "Kullanıcının işaretlemesini burada oluştur" diyebilirsiniz.

Bir <slot> öğeleri onları davet ettiğinde, öğelerin gölge DOM sınırını "geçmesine" izin verilir. Bu öğelere dağıtılmış düğümler denir. Kavram olarak, dağıtılmış düğümler biraz tuhaf görünebilir. Alanlar, DOM'yi fiziksel olarak taşımaz. Gölge DOM'un içindeki başka bir konumda oluşturulur.

Bir bileşen, gölge DOM'unda sıfır veya daha fazla alan tanımlayabilir. Slotlar boş olabilir veya yedek içerik sağlayabilir. Kullanıcı ışık DOM içeriği sağlamazsa bu alan, yedek içeriği 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ış alanlar da oluşturabilirsiniz. Adlandırılmış alanlar, gölge DOM'unuzda kullanıcıların adlarıyla başvurdukları belirli deliklerdir.

Örnek - <fancy-tabs>'nin gölge DOM'sindeki alanlar:

#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> özelliğini ş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>

Ayrıca, merak ediyorsanız, 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ğine ancak birleştirilmiş DOM ağacının aynı kaldığına dikkat edin. Dilerseniz <button> adlı CSS'den <h2> adlı CSS'ye de geçiş yapabiliriz. Bu bileşen, <select> gibi farklı alt öğe türlerini işlemek üzere tasarlanmıştır.

Stil

Web bileşenlerinin stil özelliklerini ayarlamaya yönelik birçok seçenek vardır. Gölge DOM kullanan bir bileşene ana sayfa tarafından stil eklenebilir, kendi stillerini tanımlayabilir veya kullanıcıların varsayılan değerleri geçersiz kılması için kancalar (CSS özel özellikleri biçiminde) sağlanabilir.

Bileşen tanımlı stiller

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

  • Dış sayfadaki CSS seçiciler, bileşeninizin içine uygulanmıyor.
  • İçeride tanımlanan stillerin taşmaması. Bunlar, barındırma öğesine ayarlanır.

Gölge DOM içinde kullanılan CSS seçiciler yerel olarak bileşeninize uygulanır. Pratikte bu, sayfanın başka bir yerinde çakışmalar konusunda endişelenmeden genel kimlik/sınıf adlarını tekrar kullanabileceğimiz anlamına gelir. Gölge DOM için daha basit CSS seçiciler en iyi uygulamadır. Bunlar performans açısından da iyidir.

Örnek: Gölge kökte 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 dahil edilir:

#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 menü yerine çok seçimli bir widget'ı nasıl oluşturduğunu hiç merak ettiniz mi:

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

<select>, üzerinde beyan ettiğiniz özelliklere göre kendini farklı bir şekilde biçimlendirebilir. Web bileşenleri de :host seçiciyi kullanarak kendilerini biçimlendirebilir.

Örnek: Bileşenin kendisine stil atanması

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

:host ile ilgili bir kazanım, üst sayfadaki kuralların öğede tanımlanan :host kurallara göre daha yüksek spesifikliğ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 bir gölge kökü bağlamında çalışır. Bu nedenle, gölge DOM'un dışında kullanılamaz.

:host(<selector>) öğesinin işlevsel biçimi, bir <selector> ile eşleşmesi durumunda ana makineyi hedeflemenize olanak tanır. Bu, bileşeninizin kullanıcı etkileşimine veya duruma tepki veren davranışları kapsüllemesi ya da ana makineye bağlı olarak dahili düğümleri biçimlendirmesi için harika bir yoldur.

<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 dayalı stil oluşturma

:host-context(<selector>), bileşen veya üst öğelerinden herhangi biri <selector> ile eşleşirse bileşenle eşleşir. Bunun yaygın bir kullanımı, bileşenlerin çevrelerine dayalı temalardır. Örneğin, birçok kişi <html> veya <body> için bir sınıf uygulayarak tema oluşturmaktadır:

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

:host-context(.darktheme), .darktheme öğesinin alt etiketi olduğunda <fancy-tabs> stilini belirler:

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

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

Dağıtılmış düğümlerin stil özelliklerini ayarlama

::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 stilini belirleyebilir:

<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 öncekilerden hatırlıyorsanız <slot> öğeleri kullanıcının ışık DOM'sini taşımaz. Düğümler bir <slot> içine dağıtıldığında <slot>, DOM'larını oluşturur ancak düğümler fiziksel olarak aynı konumda kalır. Dağıtımdan önce uygulanan stiller, dağıtımdan sonra da uygulanmaya devam eder. Bununla birlikte, ışık DOM'u dağıtıldığında ek stiller (gölge DOM tarafından tanımlananlar) alabilir.

<fancy-tabs> kaynağından daha kapsamlı başka 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çimi kalın harflerle gösterilir ve ilgili panel gösterilir. Bunun için selected özelliğine sahip dağıtılmış düğümler seçilir. Özel öğenin JS'si (burada gösterilmemiştir), bu özelliği doğru zamanda ekler.

Bileşenin stilini dışarıdan belirleme

Bir bileşeni dışarıdan biçimlendirmenin 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'de tanımlanan stilleri her zaman kazanır. Örneğin, kullanıcı fancy-tabs { width: 500px; } seçiciyi yazarsa bileşenin :host { width: 650px;} kuralını geçersiz kılar.

Bileşenin stil özelliklerini uygulayarak yalnızca bu noktaya ulaşabilirsiniz. Peki, bir bileşenin iç öğelerini stil oluşturmak 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 stillerde ince ayar yapabilirler. Kavram olarak fikir şuna benzer: <slot>. 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'unun içinde:

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

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

İleri düzey konular

Kapalı gölge kökleri oluşturma (kaçınılması gerekir)

"Kapalı" mod olarak adlandırılan başka bir gölge DOM türü de vardır. Kapalı bir gölge ağacı oluşturduğunuzda JavaScript dışındaki JavaScript bileşeninizin dahili DOM'una erişemez. Bu, <video> gibi yerel öğelerin işleyiş şekline benzer. JavaScript, <video> gölge DOM'una erişemez, çünkü tarayıcı bu DOM'u kapalı mod gölge kökü kullanarak uygular.

Örnek - kapalı bir 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ı moddan diğer API'ler de etkilenir:

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

{mode: 'closed'} ile neden hiçbir zaman web bileşeni oluşturmamanız gerektiğiyle ilgili özetim aşağıdadır:

  1. Yapay güvenlik hissi. Bir saldırganın Element.prototype.attachShadow ürününü ele geçirmesini hiçbir şey durduramaz.

  2. Kapalı mod, özel öğe kodunuzun kendi gölge DOM'una erişmesini engeller. Bu başarısızlıktır. Bunun yerine, querySelector() gibi öğeleri kullanmak istiyorsanız 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şenlerini oluştururken bir özellik eklemeyi unutacağınız zamanlar olacaktır. Bir yapılandırma seçeneğidir. Kullanıcının istediği bir kullanım alanı. Dahili düğümler için yeterli stil kancaları eklemeyi unutmak yaygın bir örnektir. Kapalı modda, kullanıcıların varsayılanları geçersiz kılması ve stilleri değiştirmesi mümkün değildir. Bileşenin dahili öğelerine erişebilmek çok faydalıdır. Sonuçta, kullanıcılar bileşeninizi çatallar, başka bir bileşen bulur veya istediklerini yapmıyorsa kendi bileşenini oluştururlar :(

JS'deki alanlarla çalışma

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

slotchange etkinliği

slotchange etkinliği, bir slotun dağıtılmış düğümleri değiştiğinde tetiklenir. Örneğin, kullanıcı ışık DOM'sine alt öğeleri eklerse veya bu DOM'den alt öğeleri kaldırırsa bu durumla karşılaşılabilir.

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

Işık DOM'da yapılan diğer değişiklik türlerini izlemek için öğe oluşturucunuzda bir MutationObserver ayarlayabilirsiniz.

Bir alanda hangi öğeler oluşturuluyor?

Bazen bir alanla hangi öğelerin ilişkilendirildiğini bilmek yararlı olur. Alanın hangi öğeleri oluşturduğunu bulmak için slot.assignedNodes() yöntemini çağırın. {flatten: true} seçeneği, bir alanın yedek içeriğini de döndürür (hiçbir düğüm dağıtılmıyorsa).

Örneğin, gölge DOM'unuzun 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(); []
<my-component></my-component> slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

Bir öğe hangi alana atanır?

Tersi soruyu da yanıtlayabilirsiniz. element.assignedSlot, öğenizin hangi bileşen yuvalarına atandığını belirtir.

Gölge DOM etkinlik modeli

Bir etkinlik, gölge DOM'dan baloncuk olarak çıkış yaptığında, hedefi, gölge DOM'un sağladığı kapsüllemeyi koruyacak şekilde ayarlanır. Yani etkinlikler, gölge DOM'unuzdaki dahili öğelerden değil, bileşenden gelmiş gibi görünecek şekilde yeniden hedeflenir. Bazı etkinlikler gölge DOM'nin dışına bile dağıtılmaz.

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

  • Odaklanılan Etkinlikler: blur, focus, focusin, focusout
  • Fare Etkinlikleri: click, dblclick, mousedown, mouseenter, mousemove vb.
  • Çark 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 dizisini 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) değerine ayarlanırsa tüketiciler etkinliği 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ğı işleme

Gölge DOM'un etkinlik modelinden hatırlarsanız, gölge DOM'un içinde tetiklenen etkinlikler, barındıran öğeden geliyormuş gibi görünecek şekilde ayarlanır. Örneğin, gölge kökünün içindeki bir <input> öğesini 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> kaynağından geldiği anlaşılıyor. Benzer şekilde, document.activeElement değeri <x-focus> olacak. Gölge kökü mode:'open' ile oluşturulduysa (kapalı mod'a bakın) odağı alan dahili düğüme de erişebilirsiniz:

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

Oynatmada birden fazla gölge DOM düzeyi varsa (örneğin, başka bir özel öğe içindeki özel bir öğe) activeElement öğesini bulmak için gölge DOM'u tekrar tekrar ayrıntılı olarak incelemeniz gerekir:

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

Bir başka odak seçeneği de bir gölge ağacında öğenin odak davranışını genişleten delegatesFocus: true seçeneğidir.

  • Gölge DOM'un içindeki bir düğümü tıklarsanız ve düğüm odaklanabilir bir alan değilse ilk odaklanılabilir alan odaklanır.
  • Gölge DOM'un içindeki bir düğüm odaklandığında, :focus odaklanılan öğeye ek olarak ana makineye de uygulanır.

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

<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ç

delelegatesFocus: gerçek davranış.

Yukarıda, <x-focus> odaklanıldığında (kullanıcı tıklaması, sekmeli focus() vb.) elde edilen sonuç elde edilir. "Tıklanabilir Gölge DOM metni" tıklanmış veya dahili <input> odaklanılmış (autofocus dahil).

delegatesFocus: false öğesini ayarlamış olsaydınız aşağıdaki ekran yerine aşağıdaki ekranı görürsünüz:

delelegatesFocus: false (yanlış) ve dahili girişe odaklanılır.
delegatesFocus: false ve dahili <input> odaklanılmış.
delegatesFocus: yanlış ve x-odaklama, odağı artırır (ör. tabindex=&#39;0&#39;).
delegatesFocus: false ve <x-focus> odak kazanı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;u içindeki başka bir boş alan tıklanır).
delegatesFocus: false ve "Tıklanabilir Gölge DOM metni" tıklanır (veya öğenin gölge DOM'u içindeki başka bir boş alan tıklanır).

İpuçları ve Püf Noktaları

Yıllar içinde, web bileşenleri yazma hakkında birkaç şey öğrendim. Bu ipuçlarından bazılarını, bileşenleri yazmak ve gölge DOM'de hata ayıklamak için yararlı bulacağınızı düşünüyorum.

CSS kapsama alanını kullanın

Genellikle, bir web bileşeninin düzeni/stili/boyaması oldukça bağımsızdır. Performans kazanmak için :host içinde CSS kapsama özelliğini kullanın:

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

Devralınabilir stiller sıfırlanıyor

Devralınabilir stiller (background, color, font, line-height vb.) gölge DOM'da devralmaya devam eder. Diğer bir deyişle, varsayılan olarak gölge DOM sınırını dellerler. Yeni bir seçenek listesiyle başlamak istiyorsanız devralınabilir stilleri gölge sınırını aştıklarında ilk değerlerine sıfırlamak için all: initial; 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 sayfa tarafından kullanılan 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'sini yinelemeli olarak çekmeniz 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 aracılığıyla bir gölge kökü doldurmak yerine bildirim temelli <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" bölümündeki örneğe bakın.

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

Son birkaç yıldır web bileşenlerini takip ediyorsanız Chrome 35 ve sonraki sürümlerin/Opera'nın bir süredir gölge DOM'un eski bir sürümünü göndermekte olduğunu biliyorsunuzdur. Blink, bir süre boyunca her iki sürümü de paralel olarak desteklemeye devam edecek. v0 spesifikasyonu, gölge kökü oluşturmak için farklı bir yöntem sundu (v1'in element.attachShadow yerine element.createShadowRoot). Daha eski yöntemin çağrılması, v0 semantiğine sahip bir gölge kökü oluşturmaya devam ettiği için mevcut v0 kodu bozulmaz.

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

Tarayıcı desteği

Gölge DOM v1, Chrome 53 (durum), Opera 40, Safari 10 ve Firefox 63 ile gönderilir. Edge geliştirme işlemine başladı.

Gölge DOM algılama özelliğini kullanmak için attachShadow öğesinin mevcut olup olmadığını kontrol edin:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyester Lifi

Tarayıcı desteği yaygın bir şekilde kullanıma sunulana kadar, shadydom ve shadycss çoklu dolguları size v1 özelliğini sağlar. Gölgeli DOM, Gölge DOM'un DOM kapsamını ve Shadycss polyfill'lerinin CSS özel özelliklerini ve yerel API'nin sağladığı stil kapsamını taklit eder.

Çoklu dolguları yükleyin:

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

Çoklu dolguları 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 daraltma/kapsama alma ile ilgili talimatlar için https://github.com/webcomponents/shadycss#usage adresine bakın.

Sonuç

İlk kez CSS kapsamı ile DOM kapsamını doğru yapan ve gerçek bir bileşime sahip bir temel API ürünümüz var. Gölge DOM, özel öğeler gibi diğer web bileşeni API'leriyle birlikte kullanıldığında, gerçekten kapsüllenmiş bileşenleri korsan olmadan veya <iframe> gibi eski bagajları kullanmadan yazma imkanı sunar.

Beni yanlış anlamayın. Gölge DOM kesinlikle karmaşık bir canavar. Ama bu, öğrenmeye değer bir canavar. Biraz vakit ayırın. Öğrenin ve sorular sorun.

Daha fazla bilgi

SSS

Gölge DOM v1'i bugün kullanabilir miyim?

Polyfill ile evet. Tarayıcı desteği başlıklı makaleyi inceleyin.

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

Gölge DOM, bir güvenlik özelliği değildir. CSS'nin kapsamını belirlemeye ve DOM ağaçlarını bileşende gizlemeye yönelik hafif bir araçtır. Gerçek bir güvenlik sınırı istiyorsanız <iframe> kullanın.

Web bileşenlerinin 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 yazmak; CSS kapsamı, DOM kapsülleme ve bileşim gibi özelliklerden yararlanabileceğiniz anlamına gelir.

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

Kapalı gölge kökleri bölümüne bakın.