एलान वाला शैडो डीओएम

सीधे एचटीएमएल में शैडो DOM को लागू और इस्तेमाल करने का नया तरीका.

डिक्लेरेटिव शैडो DOM एक स्टैंडर्ड वेब प्लैटफ़ॉर्म सुविधा है, जो Chrome पर वर्शन 90 से काम करती है. ध्यान दें कि 2023 में इस सुविधा के स्पेसिफ़िकेशन में बदलाव हुए थे. इनमें, shadowroot का नाम बदलकर shadowrootmode किया गया था. साथ ही, इस सुविधा के सभी हिस्सों के सबसे अप-टू-डेट स्टैंडर्ड वर्शन, Chrome के वर्शन 124 में लॉन्च कर दिए गए थे.

शैडो DOM तीन वेब कॉम्पोनेंट के स्टैंडर्ड में से एक है, जिसे एचटीएमएल टेंप्लेट और कस्टम एलिमेंट से राउंड आउट किया जाता है. शैडो डीओएम से किसी खास डीओएम सबट्री को सीएसएस स्टाइल के दायरे में लाने और उस सबट्री को दस्तावेज़ के बाकी हिस्सों से अलग करने का तरीका मिलता है. <slot> एलिमेंट की मदद से, हम यह कंट्रोल कर सकते हैं कि किसी कस्टम एलिमेंट के चाइल्ड एंट्री को उसके शैडो ट्री में कहां डाला जाना चाहिए. इन सभी सुविधाओं की मदद से, सिस्टम में अपने-आप शामिल होने वाले और फिर से इस्तेमाल किए जा सकने वाले कॉम्पोनेंट बनाए जा सकते हैं. ये कॉम्पोनेंट, पहले से मौजूद एचटीएमएल एलिमेंट की तरह ही, मौजूदा ऐप्लिकेशन के साथ आसानी से इंटिग्रेट हो जाते हैं.

अब तक, Shadow DOM का उपयोग करने का एकमात्र तरीका JavaScript का उपयोग करके शैडो रूट बनाना था:

const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';

इस तरह का एक ज़रूरी एपीआई, क्लाइंट-साइड रेंडरिंग के लिए सही तरीके से काम करता है: हमारे कस्टम एलिमेंट को तय करने वाले JavaScript मॉड्यूल अपने शैडो रूट भी बनाते हैं और उनका कॉन्टेंट सेट करते हैं. हालांकि, कई वेब ऐप्लिकेशन को बिल्ड के समय कॉन्टेंट को सर्वर-साइड या स्टैटिक एचटीएमएल को रेंडर करना होगा. यह वेबसाइट पर आने वाले उन लोगों को अच्छा अनुभव देने की प्रक्रिया का एक अहम हिस्सा हो सकता है जो शायद JavaScript का इस्तेमाल नहीं कर सकते.

सर्वर-साइड रेंडरिंग (एसएसआर) की वजहें हर प्रोजेक्ट के हिसाब से अलग-अलग होती हैं. कुछ वेबसाइटों को सुलभता से जुड़े दिशा-निर्देशों का पालन करने के लिए, पूरी तरह से काम करने वाला सर्वर के ज़रिए रेंडर किया गया एचटीएमएल देना चाहिए. वहीं दूसरी वेबसाइटों ने धीमे कनेक्शन या डिवाइस पर अच्छी परफ़ॉर्मेंस की गारंटी देने के लिए, बिना JavaScript वाला बेसलाइन अनुभव उपलब्ध कराने का विकल्प चुना है.

पहले, सर्वर-साइड रेंडरिंग के साथ शैडो डीओएम का इस्तेमाल करना मुश्किल होता था, क्योंकि सर्वर से जनरेट किए गए एचटीएमएल में शैडो रूट को ज़ाहिर करने का कोई तरीका पहले से मौजूद नहीं था. शैडो रूट को डीओएम एलिमेंट में अटैच करने पर भी परफ़ॉर्मेंस पर असर पड़ता है. ये उन एलिमेंट के बिना रेंडर हो जाते हैं जो पहले ही रेंडर किए जा चुके हैं. इस वजह से, पेज लोड होने के बाद लेआउट शिफ़्ट हो सकता है या Shadow Root की स्टाइलशीट लोड करते समय बिना स्टाइल वाले कॉन्टेंट ("FOUC") का फ़्लैश दिखा सकता है.

डिक्लेरेटिव शैडो डीओएम (डीएसडी), इस पाबंदी को हटाता है और सर्वर पर शैडो DOM को ला देता है.

डिक्लेरेटिव शैडो रूट बनाना

डिक्लेरेटिव शैडो रूट, shadowrootmode एट्रिब्यूट वाला <template> एलिमेंट है:

<host-element>
  <template shadowrootmode="open">
    <slot></slot>
  </template>
  <h2>Light content</h2>
</host-element>

एचटीएमएल पार्सर, shadowrootmode एट्रिब्यूट वाले टेंप्लेट एलिमेंट की पहचान करता है और उसे तुरंत अपने पैरंट एलिमेंट के शैडो रूट के तौर पर लागू कर देता है. ऊपर दिए गए सैंपल नतीजों से प्योर एचटीएमएल मार्कअप को नीचे दिए गए डीओएम ट्री में लोड करने से लोड होता है:

<host-element>
  #shadow-root (open)
  <slot>
    ↳
    <h2>Light content</h2>
  </slot>
</host-element>

यह कोड सैंपल, Chrome DevTools के एलिमेंट पैनल के उन तरीकों का पालन करता है जो शैडो डीओएम कॉन्टेंट दिखाने के लिए इस्तेमाल किए गए हैं. उदाहरण के लिए, ↳ वर्ण स्लॉट किए गए लाइट DOM कॉन्टेंट को दिखाता है.

इससे हमें स्टैटिक एचटीएमएल में शैडो DOM के एनकैप्सुलेशन और स्लॉट प्रोजेक्शन के फ़ायदे मिलते हैं. शैडो रूट के साथ-साथ पूरा ट्री बनाने के लिए किसी भी JavaScript की ज़रूरत नहीं होती.

कॉम्पोनेंट हाइड्रेशन

डिक्लेरेटिव शैडो डीओएम का इस्तेमाल स्टाइल को एनकैप्सुलेट करने या चाइल्ड प्लेसमेंट को पसंद के मुताबिक बनाने के लिए किया जा सकता है. हालांकि, कस्टम एलिमेंट के साथ इस्तेमाल करने पर यह सबसे ज़्यादा कारगर साबित होता है. कस्टम एलिमेंट का इस्तेमाल करके बनाए गए कॉम्पोनेंट, स्टैटिक एचटीएमएल से अपने-आप अपग्रेड हो जाते हैं. डिक्लेरेटिव शैडो डीओएम की शुरुआत होने के बाद, अब किसी कस्टम एलिमेंट को अपग्रेड करने से पहले शैडो रूट बनाया जा सकता है.

एचटीएमएल से अपग्रेड किए जा रहे कस्टम एलिमेंट में शैडो रूट पहले से ही अटैच होगा. इसमें डिक्लेरेटिव शैडो रूट भी शामिल होगा. इसका मतलब है कि जब आपका कोड इंस्टैंशिएट किया जाएगा, तो एलिमेंट में एक ShadowRoot प्रॉपर्टी पहले से उपलब्ध होगी. इसके लिए, आपका कोड साफ़ तौर पर नया कोड नहीं बनाएगा. अपने एलिमेंट के कंस्ट्रक्टर में किसी भी मौजूदा शैडो रूट के लिए, this.shadowRoot की जांच करना सबसे अच्छा होता है. अगर पहले से कोई वैल्यू मौजूद है, तो इस कॉम्पोनेंट के एचटीएमएल में डिक्लेरेटिव शैडो रूट शामिल किया जाता है. अगर वैल्यू शून्य है, तो इसका मतलब है कि एचटीएमएल में कोई डिक्लेरेटिव शैडो रूट मौजूद नहीं था या ब्राउज़र डिक्लेरेटिव शैडो DOM पर काम नहीं करता.

<menu-toggle>
  <template shadowrootmode="open">
    <button>
      <slot></slot>
    </button>
  </template>
  Open Menu
</menu-toggle>
<script>
  class MenuToggle extends HTMLElement {
    constructor() {
      super();

      // Detect whether we have SSR content already:
      if (this.shadowRoot) {
        // A Declarative Shadow Root exists!
        // wire up event listeners, references, etc.:
        const button = this.shadowRoot.firstElementChild;
        button.addEventListener('click', toggle);
      } else {
        // A Declarative Shadow Root doesn't exist.
        // Create a new shadow root and populate it:
        const shadow = this.attachShadow({mode: 'open'});
        shadow.innerHTML = `<button><slot></slot></button>`;
        shadow.firstChild.addEventListener('click', toggle);
      }
    }
  }

  customElements.define('menu-toggle', MenuToggle);
</script>

कस्टम एलिमेंट काफ़ी समय से मौजूद हैं, लेकिन अब तक attachShadow() का इस्तेमाल करके शैडो रूट बनाने से पहले, किसी मौजूदा शैडो रूट की जांच करने की ज़रूरत नहीं थी. डिक्लेरेटिव शैडो डीओएम में एक छोटा सा बदलाव होता है, जो मौजूदा कॉम्पोनेंट को काम करने की अनुमति देता है. इसके बावजूद, ऐसा होता है: मौजूदा डेक्लेरेटिव शैडो रूट वाले एलिमेंट पर attachShadow() तरीके को कॉल करने से गड़बड़ी नहीं. इसके बजाय, डिक्लेरेटिव शैडो रूट को खाली करके वापस भेज दिया जाता है. इससे डिक्लेरेटिव शैडो डीओएम के लिए नहीं बनाए गए पुराने कॉम्पोनेंट को काम करना जारी रखने की अनुमति मिलती है. इसकी वजह यह है कि डिक्लेरेटिव रूट तब तक सुरक्षित रहते हैं, जब तक ज़रूरी बदलाव नहीं किया जाता.

नए बनाए गए कस्टम एलिमेंट के लिए, नई ElementInternals.shadowRoot प्रॉपर्टी में किसी एलिमेंट के मौजूदा डिक्लेरेटिव शैडो रूट का रेफ़रंस साफ़ तौर पर दिया जा सकता है. इसमें, ओपन और क्लोज़्ड, दोनों तरह के कस्टम एलिमेंट शामिल हैं. इसका इस्तेमाल, किसी भी डिक्लेरेटिव शैडो रूट की जांच करने और उसे इस्तेमाल करने के लिए किया जा सकता है. साथ ही, अगर आपने कोई शैडो रूट नहीं दिया है, तो इस तरीके का इस्तेमाल attachShadow() पर ही किया जा सकता है.

class MenuToggle extends HTMLElement {
  constructor() {
    super();

    const internals = this.attachInternals();

    // check for a Declarative Shadow Root:
    let shadow = internals.shadowRoot;
    if (!shadow) {
      // there wasn't one. create a new Shadow Root:
      shadow = this.attachShadow({
        mode: 'open'
      });
      shadow.innerHTML = `<button><slot></slot></button>`;
    }

    // in either case, wire up our event listener:
    shadow.firstChild.addEventListener('click', toggle);
  }
}
customElements.define('menu-toggle', MenuToggle);

हर रूट पर एक शैडो

डिक्लेरेटिव शैडो रूट सिर्फ़ अपने पैरंट एलिमेंट से जुड़ा होता है. इसका मतलब है कि शैडो रूट हमेशा अपने संबंधित एलिमेंट के साथ साथ रहते हैं. डिज़ाइन से जुड़े इस फ़ैसले से यह पक्का हो जाता है कि शैडो रूट को एचटीएमएल दस्तावेज़ के बाकी हिस्से की तरह स्ट्रीम किया जा सकता है. यह संलेखन और जनरेट करने के लिए भी सुविधाजनक है, क्योंकि किसी एलिमेंट में शैडो रूट जोड़ने के लिए मौजूदा शैडो रूट की रजिस्ट्री को बनाए रखने की ज़रूरत नहीं होती.

शैडो रूट को उनके पैरंट एलिमेंट से जोड़ने में एक समस्या यह है कि कई एलिमेंट को एक ही डिक्लेरेटिव शैडो रूट <template> से शुरू नहीं किया जा सकता. हालांकि, ज़्यादातर मामलों में इस बात की संभावना कम होती है, जहां डिक्लेरेटिव शैडो DOM का इस्तेमाल किया जाता है. इसकी वजह यह है कि हर शैडो रूट का कॉन्टेंट शायद ही एक जैसा होता है. सर्वर के रेंडर किए गए एचटीएमएल में अक्सर दोहराए जाने वाले एलिमेंट स्ट्रक्चर होते हैं. हालांकि, उनका कॉन्टेंट आम तौर पर अलग होता है. उदाहरण के लिए, टेक्स्ट या एट्रिब्यूट में थोड़ा अंतर. क्रम से लगाए गए डिक्लेरेटिव शैडो रूट का कॉन्टेंट पूरी तरह स्टैटिक होता है. इसलिए, डिक्लेरेटिव शैडो रूट से कई एलिमेंट को एक ही डिक्लेरेटिव शैडो रूट से अपग्रेड किया जा सकेगा. ऐसा सिर्फ़ तब होगा, जब एलिमेंट एक जैसे होंगे. आखिर में, कंप्रेशन के असर की वजह से, नेटवर्क ट्रांसफ़र साइज़ पर बार-बार एक जैसे शैडो रूट का असर कम पड़ता है.

आने वाले समय में, ऐसा हो सकता है कि शैडो रूट का फिर से इस्तेमाल किया जा सके. अगर डीओएम को पहले से मौजूद टेंप्लेट के लिए सहायता मिलती है, तो डिक्लेरेटिव शैडो रूट को ऐसे टेंप्लेट के तौर पर माना जा सकता है जिन्हें किसी एलिमेंट के लिए शैडो रूट बनाने के लिए इंस्टैंशिएट किया जाता है. मौजूदा डिक्लेरेटिव शैडो डीओएम डिज़ाइन, शैडो रूट असोसिएशन को किसी एक एलिमेंट तक सीमित करके, आने वाले समय में इस संभावना को बनाए रखने की अनुमति देता है.

स्ट्रीमिंग अच्छी है

डिक्लेरेटिव शैडो रूट को सीधे उनके पैरंट एलिमेंट से जोड़ने से, अपग्रेड करने और उन्हें उस एलिमेंट से जोड़ने की प्रोसेस आसान हो जाती है. डिक्लेरेटिव शैडो रूट का पता एचटीएमएल पार्स करने के दौरान लगाया जाता है. साथ ही, ओपनिंग <template> टैग मिलते ही, ये रूट अटैच हो जाते हैं. <template> में पार्स किए गए एचटीएमएल को सीधे शैडो रूट में पार्स किया जाता है, ताकि इसे "स्ट्रीम" किया जा सके. जैसे ही यह मिलता है, रेंडर किया जा सकता है.

<div id="el">
  <script>
    el.shadowRoot; // null
  </script>

  <template shadowrootmode="open">
    <!-- shadow realm -->
  </template>

  <script>
    el.shadowRoot; // ShadowRoot
  </script>
</div>

सिर्फ़ पार्सर

डिक्लेरेटिव शैडो DOM, एचटीएमएल पार्सर की एक सुविधा है. इसका मतलब है कि डिक्लेरेटिव शैडो रूट को सिर्फ़ shadowrootmode एट्रिब्यूट वाले <template> टैग के लिए पार्स और अटैच किया जाएगा. ये एट्रिब्यूट, एचटीएमएल पार्स करने के दौरान मौजूद होते हैं. दूसरे शब्दों में, डिक्लेरेटिव शैडो रूट्स को शुरुआती एचटीएमएल पार्स करने के दौरान बनाया जा सकता है:

<some-element>
  <template shadowrootmode="open">
    shadow root content for some-element
  </template>
</some-element>

<template> एलिमेंट का shadowrootmode एट्रिब्यूट सेट करने से कुछ नहीं होता और टेंप्लेट एक सामान्य टेंप्लेट एलिमेंट रहता है:

const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null

सुरक्षा से जुड़ी कुछ ज़रूरी बातों से बचने के लिए, innerHTML या insertAdjacentHTML() जैसे फ़्रैगमेंट पार्स करने वाले एपीआई का इस्तेमाल करके भी डिक्लेरेटिव शैडो रूट नहीं बनाए जा सकते. डिक्लेरेटिव शैडो रूट की मदद से, एचटीएमएल को पार्स करने के लिए, setHTMLUnsafe() या parseHTMLUnsafe() का इस्तेमाल करें:

<script>
  const html = `
    <div>
      <template shadowrootmode="open"></template>
    </div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; // No shadow root here
  div.setHTMLUnsafe(html); // Shadow roots included
  const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>

स्टाइल के साथ सर्वर रेंडर करना

इनलाइन और बाहरी स्टाइलशीट, स्टैंडर्ड <style> और <link> टैग का इस्तेमाल करके डिक्लेरेटिव शैडो रूट में पूरी तरह से काम करती हैं:

<nineties-button>
  <template shadowrootmode="open">
    <style>
      button {
        color: seagreen;
      }
    </style>
    <link rel="stylesheet" href="/comicsans.css" />
    <button>
      <slot></slot>
    </button>
  </template>
  I'm Blue
</nineties-button>

इस तरीके से तय की गई स्टाइल को भी काफ़ी हद तक ऑप्टिमाइज़ किया जाता है: अगर एक ही स्टाइलशीट कई डिक्लेरेटिव शैडो रूट में मौजूद है, तो इसे सिर्फ़ एक बार लोड और पार्स किया जाता है. ब्राउज़र, एक बैकिंग CSSStyleSheet का इस्तेमाल करता है, जो सभी शैडो रूट के साथ शेयर किया जाता है. इससे, डुप्लीकेट मेमोरी ओवरहेड हट जाता है.

डिक्लेरेटिव शैडो DOM में कंस्ट्रक्शन करने लायक स्टाइलशीट का इस्तेमाल नहीं किया जा सकता. ऐसा इसलिए है, क्योंकि फ़िलहाल एचटीएमएल में कंस्ट्रक्ट की जा सकने वाली स्टाइलशीट को क्रम में लगाने का कोई तरीका नहीं है. साथ ही, adoptedStyleSheets को अपने-आप भरने के दौरान, इन स्टाइलशीट को रेफ़र करने का कोई तरीका नहीं है.

बिना स्टाइल वाले कॉन्टेंट की झलक दिखाने से बचना

जो ब्राउज़र अभी डिक्लेरेटिव शैडो डीओएम का समर्थन नहीं करते हैं, उनमें एक संभावित समस्या "बिना स्टाइल वाले कॉन्टेंट के फ़्लैश" (एफ़ओयूसी) से बचना है, जहां ऐसे कस्टम एलिमेंट के लिए रॉ कॉन्टेंट दिखाया जाता है जिन्हें अभी तक अपग्रेड नहीं किया गया है. डिक्लेरेटिव शैडो DOM से पहले, एफ़ओयूसी से बचने की एक आम तकनीक यह थी कि उन कस्टम एलिमेंट पर display:none स्टाइल नियम लागू किया जाना चाहिए जो अब तक लोड नहीं हुए हैं. इसकी वजह यह है कि इन एलिमेंट में शैडो रूट अटैच और पॉप्युलेट नहीं किया गया था. इस तरह, कॉन्टेंट तब तक नहीं दिखता, जब तक कि वह "तैयार न हो":

<style>
  x-foo:not(:defined) > * {
    display: none;
  }
</style>

डिक्लेरेटिव शैडो DOM की शुरुआत के साथ, कस्टम एलिमेंट को एचटीएमएल में इस तरह रेंडर या लिखा जा सकता है कि उनका शैडो कॉन्टेंट अपनी जगह पर मौजूद हो और क्लाइंट-साइड कॉम्पोनेंट के लागू होने से पहले तैयार हो:

<x-foo>
  <template shadowrootmode="open">
    <style>h2 { color: blue; }</style>
    <h2>shadow content</h2>
  </template>
</x-foo>

इस मामले में, display:none "FOUC" नियम, डिक्लेरेटिव शैडो रूट के कॉन्टेंट को दिखने से रोकेगा. हालांकि, इस नियम को हटाने से, डिक्लेरेटिव शैडो डीओएम सपोर्ट के बिना ब्राउज़र गलत या बिना स्टाइल वाला कॉन्टेंट दिखाएंगे. ऐसा तब तक होगा, जब तक कि डिक्लेरेटिव शैडो डीओएम polyfill लोड नहीं हो जाता और शैडो रूट टेंप्लेट असली शैडो रूट में नहीं बदल देता.

अच्छी बात यह है कि इसे एफ़ओयूसी शैली के नियम में बदलाव करके सीएसएस में हल किया जा सकता है. डिक्लेरेटिव शैडो डीओएम के साथ काम करने वाले ब्राउज़र में, <template shadowrootmode> एलिमेंट को शैडो रूट में तुरंत बदल दिया जाता है. डीओएम ट्री में कोई <template> एलिमेंट नहीं रहता. डिक्लेरेटिव शैडो डीओएम के साथ काम न करने वाले ब्राउज़र, <template> एलिमेंट को सुरक्षित रखते हैं. इसका इस्तेमाल, एफ़ओयूसी को रोकने के लिए किया जा सकता है:

<style>
  x-foo:not(:defined) > template[shadowrootmode] ~ *  {
    display: none;
  }
</style>

अब तक तय नहीं किए गए कस्टम एलिमेंट को छिपाने के बजाय, बदले गए "FOUC" नियम के तहत किसी <template shadowrootmode> एलिमेंट को फ़ॉलो करने पर इसके बच्चे छिप जाते हैं. कस्टम एलिमेंट तय होने के बाद, नियम मेल नहीं खाता. इस नियम को उन ब्राउज़र में अनदेखा कर दिया जाता है जिनमें डिक्लेरेटिव शैडो डीओएम काम करता है, क्योंकि एचटीएमएल पार्स करने के दौरान <template shadowrootmode> चाइल्ड को हटा दिया जाता है.

सुविधा की पहचान और ब्राउज़र समर्थन

डिक्लेरेटिव शैडो DOM, Chrome 90 और Edge 91 से उपलब्ध है. हालांकि, इसमें स्टैंडर्ड shadowrootmode एट्रिब्यूट के बजाय, shadowroot नाम के पुराने नॉन-स्टैंडर्ड एट्रिब्यूट का इस्तेमाल किया गया है. नया shadowrootmode एट्रिब्यूट और स्ट्रीमिंग की सुविधा, Chrome 111 और Edge 111 में उपलब्ध है.

यह वेब प्लैटफ़ॉर्म के नए एपीआई के तौर पर, एलान वाला शैडो DOM सभी ब्राउज़र पर अभी तक बड़े पैमाने पर काम नहीं करता है. ब्राउज़र के साथ काम करने की सुविधा का पता लगाने के लिए, HTMLTemplateElement के प्रोटोटाइप में shadowRootMode प्रॉपर्टी की मौजूदगी की जांच करें:

function supportsDeclarativeShadowDOM() {
  return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}

पॉलीफ़िल

डिक्लेरेटिव शैडो डीओएम के लिए, आसान पॉलीफ़िल बनाना काफ़ी आसान है. इसकी वजह यह है कि पॉलीफ़िल को टाइम सिमेंटिक या सिर्फ़ पार्सर वाली विशेषताओं को उस तरह से कॉपी करने की ज़रूरत नहीं है जो ब्राउज़र को लागू करने पर लागू होती है. पॉलीफ़िल डिक्लेरेटिव शैडो डीओएम को स्कैन करके, सभी <template shadowrootmode> एलिमेंट ढूंढे जा सकते हैं. इसके बाद, उन्हें पैरंट एलिमेंट पर अटैच किए गए शैडो रूट में बदला जा सकता है. यह प्रोसेस, दस्तावेज़ तैयार होने पर की जा सकती है या कस्टम एलिमेंट की लाइफ़साइकल जैसे कुछ खास इवेंट से ट्रिगर होती है.

(function attachShadowRoots(root) {
  root.querySelectorAll("template[shadowrootmode]").forEach(template => {
    const mode = template.getAttribute("shadowrootmode");
    const shadowRoot = template.parentNode.attachShadow({ mode });
    shadowRoot.appendChild(template.content);
    template.remove();
    attachShadowRoots(shadowRoot);
  });
})(document);

इसके बारे में और पढ़ें