نموذج بيانات التظليل التعريفي

Shadow DOM الإعلاني هي ميزة أساسية للمنصة الإلكترونية ومعتمَدة في الإصدار 90 من Chrome. يُرجى العلم أنّه تم تغيير مواصفات هذه الميزة في عام 2023 (بما في ذلك إعادة تسمية shadowroot إلى shadowrootmode)، وأنّ أحدث إصدارات موحّدة من جميع أجزاء الميزة تم توفيرها في الإصدار 124 من Chrome.

دعم المتصفح

  • Chrome: 111.
  • الحافة: 111.
  • Firefox: 123.
  • Safari: الإصدار 16.4.

المصدر

Shadow DOM هو أحد معايير مكونات الويب الثلاثة، ويتم تقريبه باستخدام نماذج HTML والعناصر المخصّصة. يوفّر Shadow DOM طريقة لتحديد نطاق أنماط CSS لشجرة فرعية معيّنة في DOM وعزل هذه الشجرة الفرعية عن باقي المستند. يمنحنا العنصر <slot> طريقةً للتحكم في المكان الذي يجب إدراج العناصر الثانوية التابعة له ضمن شجرة الظل الخاصة به. وتعمل هذه الميزات مجتمعةً على تمكين نظام من إنشاء مكونات مستقلة وقابلة لإعادة الاستخدام تندمج بسلاسة مع التطبيقات الحالية، تمامًا مثل عنصر HTML المدمج.

وحتى الآن، كانت الطريقة الوحيدة لاستخدام Shadow DOM هي إنشاء جذر ظل باستخدام JavaScript:

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

تعمل واجهة برمجة التطبيقات الضرورية مثل هذه مع العرض من جهة العميل: وحدات JavaScript نفسها التي تعرّف العناصر المخصصة تنشئ أيضًا جذور الظل الخاصة بها وتضبط المحتوى. ومع ذلك، تحتاج العديد من تطبيقات الويب إلى عرض المحتوى من جهة خادم المحتوى أو عرض محتوى HTML ثابت في وقت التصميم. ويمكن أن يكون ذلك جزءًا مهمًا من تقديم تجربة معقولة للزائرين الذين قد لا يكونون قادرين على تشغيل JavaScript.

تختلف مبررات العرض من جهة الخادم (SSR) من مشروع إلى آخر. يجب أن توفّر بعض المواقع الإلكترونية رمز HTML الذي يعرضه الخادم يعمل بكامل وظائفه للوفاء بإرشادات إمكانية الوصول، في حين يختار البعض الآخر تقديم تجربة أساسية بدون JavaScript كوسيلة لضمان الأداء الجيد في الاتصالات البطيئة أو الأجهزة.

في السابق، كان من الصعب استخدام Shadow DOM مع العرض من جهة الخادم بسبب عدم توفّر طريقة مضمَّنة للتعبير عن جذور الظل في رمز HTML الذي ينشئه الخادم. هناك أيضًا تأثيرات على الأداء عند إرفاق جذور الظل بعناصر DOM التي سبق أن تم عرضها بدونها. وقد يتسبب ذلك في تغيُّر التصميم بعد تحميل الصفحة، أو قد يؤدي مؤقتًا إلى عرض وميض من محتوى غير نمطي ("FOUC") أثناء تحميل أوراق أنماط Shadow Root.

يزيل Declarative Shadow DOM (DSD) هذا القيد، ويجلب Shadow DOM إلى الخادم.

كيفية إنشاء جذر ظل توضيحي

جذر الظل الإعلاني هو عنصر <template> يضم سمة shadowrootmode:

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

يرصد محلّل HTML اللغوي عنصر نموذج يتضمّن السمة shadowrootmode، ويتم تطبيقه على الفور كجذر الظل للعنصر الرئيسي. يؤدي تحميل ترميز HTML الخالص من النموذج أعلاه إلى شجرة DOM التالية:

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

يتبع نموذج التعليمة البرمجية هذا اصطلاحات لوحة عناصر عناصر مطوري البرامج في Chrome لعرض محتوى Shadow DOM. على سبيل المثال، يمثّل الحرف محتوى Light DOM المزود بخانات.

يمنحنا هذا مزايا تغليف Shadow DOM وإسقاط الخانة في لغة HTML الثابتة. ولا حاجة إلى JavaScript لإنتاج الشجرة بأكملها، بما في ذلك Shadow Root.

ترطيب المكوّنات

يمكن استخدام Shadow DOM التعريفي وحده كوسيلة لتغليف الأنماط أو تخصيص موضع إعلان فرعي، غير أن هذه الطريقة تكون أكثر فعالية عند استخدامها مع العناصر المخصّصة. تتم ترقية المكونات التي تم إنشاؤها باستخدام العناصر المخصصة تلقائيًا من HTML الثابت. مع تقديم نموذج Shadow DOM التعريفي، أصبح من الممكن الآن أن يكون للعنصر المخصّص جذر ظل قبل ترقيته.

العنصر المخصص الذي تتم ترقيته من HTML الذي يتضمن جذر الظل الإعلاني سيتم إرفاق جذر الظل هذا بالفعل. وهذا يعني أنّ السمة shadowRoot ستكون متاحة للعنصر من قبل عند إنشاء مثيل لها، بدون أن ينشئ الرمز سمة واضحة. من الأفضل التحقق من this.shadowRoot بحثًا عن أي جذر ظل حالي في الدالة الإنشائية للعنصر. إذا كانت هناك قيمة من قبل، سيتضمّن HTML لهذا المكوِّن جذر الظل الإعلاني. إذا كانت القيمة فارغة، هذا يعني أنّه لم يتم العثور على جذر الظل الإعلاني في HTML أو أنّ المتصفّح لا يتيح استخدام نموذج إظهار الظل الإعلاني.

<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(). يتضمن نموذج Shadow DOM التعريفي تغييرًا بسيطًا يسمح للمكونات الحالية بالعمل على الرغم من ذلك: عند استدعاء طريقة attachShadow() في عنصر يتضمن جذر تظليل بياني حالي، لن يؤدي ذلك إلى حدوث خطأ. بدلاً من ذلك، يتم إفراغ جذر الظل الإعلاني وإرجاعه. ويسمح هذا الإجراء للمكوّنات القديمة غير المصمّمة من أجل نموذج واجهة Shadow DOM التطبيقية بمواصلة العمل، لأنّه يتم الاحتفاظ بالجذور التعريفية إلى أن يتم إنشاء بديل ضروري.

بالنسبة إلى العناصر المخصّصة التي تم إنشاؤها حديثًا، توفّر السمة 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);

ظل واحد لكل جذر

يرتبط جذر الظل الإعلاني فقط بعنصره الأصلي. وهذا يعني أن جذور الظل تكون دائمًا محددة مع العنصر المرتبط بها. يضمن قرار التصميم هذا أن جذور الظل قابلة للبث مثل باقي مستند HTML. وهو ملائم أيضًا للتأليف والإنشاء، لأنّ إضافة جذر الظل إلى عنصر لا يتطلّب الاحتفاظ بسجلّ لجذور الظل الحالية.

بدلاً من ربط جذور الظل بالعنصر الأصلي، يكون من غير الممكن إعداد عناصر متعدّدة من جذر الظل الإعلاني <template> نفسه. ومع ذلك، من غير المرجّح أن يكون ذلك مهمًا في معظم الحالات التي يتم فيها استخدام نموذج Shadow DOM التعريفي، وذلك لأنّ محتوى كل جذر ظل نادرًا ما يكون متطابقًا. على الرغم من أنّ محتوى HTML المعروض من الخادم غالبًا ما يحتوي على بنى عناصر متكررة، إلا أنّ محتواها يختلف بشكل عام، مثل اختلافات طفيفة في النص أو السمات. وبما أنّ محتوى جذر الظل الإعلاني المتسلسل يكون ثابتًا بالكامل، لن تعمل ترقية عناصر متعددة من جذر تظليل تعريفي واحد إلا إذا كانت العناصر متطابقة. وأخيرًا، يكون تأثير جذور الظل المتشابهة المتكررة على حجم نقل الشبكة صغيرًا نسبيًا بسبب تأثيرات الضغط.

وقد تتمكن في المستقبل من إعادة النظر في جذور الظل المشتركة. إذا استفاد DOM من إنشاء نماذج مضمَّنة، يمكن التعامل مع جذور الظل التحريرية كنماذج تم إنشاء مثيل لها من أجل إنشاء جذر الظل لعنصر معيّن. ويتيح التصميم الحالي لـ Shadow DOM التعريفي توفُّر هذه الإمكانية في المستقبل من خلال حصر ارتباط جذر الظل بعنصر واحد.

البث رائع

يؤدي ربط جذور الظل الإعلاني مباشرةً بعنصرها الرئيسي إلى تبسيط عملية الترقية وإرفاقها بهذا العنصر. يتم رصد جذور الظلال الوصفية أثناء تحليل HTML، ويتم إرفاقها على الفور عند العثور على علامة <template> المفتوحة. يتم تحليل رمز HTML الذي تم تحليله داخل <template> مباشرةً إلى جذر الظل كي يمكن "بثه": يتم عرضه عند استلامه.

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

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

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

المحلل اللغوي فقط

Shadow DOM التعريفي ميزة لمحلّل HTML. يعني ذلك أنّه لن يتم تحليل جذر الظل الإعلاني وإرفاقه إلا لعلامات <template> التي تحتوي على سمة shadowrootmode والتي تكون متاحة أثناء تحليل HTML. بمعنى آخر، يمكن إنشاء جذور الظل التحريرية أثناء تحليل HTML الأولي:

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

لا يؤدي ضبط السمة shadowrootmode للعنصر <template> إلى تنفيذ أي تغيير، ويظل النموذج عنصرًا عاديًا:

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(). الطريقة الوحيدة لتحليل محتوى HTML مع تطبيق جذور الظل التحريرية هي استخدام 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 نسخة احتياطية واحدة تتم مشاركتها بين جميع جذور الظلال، ما يؤدي إلى إزالة تكرارات الذاكرة الزائدة.

لا تتوفّر أوراق الأنماط القابلة للإنشاء في نموذج عرض بيانات الظل الإعلاني. ويرجع ذلك إلى أنّه لا تتوفّر في الوقت الحالي طريقة لإنشاء تسلسل لأوراق الأنماط القابلة للإنشاء في HTML ولا تتوفّر طريقة للإشارة إليها عند تعبئة adoptedStyleSheets.

كيفية تجنُّب وميض المحتوى غير المصمّم

إحدى المشاكل المحتملة في المتصفحات التي لا تتوافق بعد مع نموذج عرض Shadow DOM التعريفي هي تجنُّب "وميض محتوى غير نمطي" (FOUC)، حيث يتم عرض المحتوى الأولي للعناصر المخصّصة التي لم تتم ترقيتها بعد. قبل ظهور نموذج عرض الظل الإعلاني (DOM)، كان أحد الأساليب الشائعة لتجنُّب استخدام FOUC هو تطبيق قاعدة نمط display:none على العناصر المخصّصة التي لم يتم تحميلها بعد، لأنّها لم يتم إرفاق جذر الظل فيها وتعبئتها. بهذه الطريقة، لا يتم عرض المحتوى إلى أن يصبح "جاهزًا":

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

مع إطلاق نموذج عرض الظل الإعلاني (DOM)، يمكن عرض العناصر المخصّصة أو تأليفها بتنسيق HTML بحيث يكون محتوى الظل الخاص بها في مكانه وجاهزًا قبل تحميل تنفيذ المكوّن من جهة العميل:

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

وفي هذه الحالة، تُظهر دالة display:none "FOUC" إلى منع ظهور محتوى جذر الظل التعريفي من الظهور. مع ذلك، ستؤدي إزالة هذه القاعدة إلى عرض محتوى غير صحيح أو غير نمط في المتصفّحات التي لا تتوافق مع نموذج Shadow DOM التعريفي إلى أن يتم تحميل polyfill في نموذج تعريفي Shadow DOM وتحويله إلى نموذج جذر الظل إلى جذر ظل حقيقي.

لحسن الحظ، يمكن حل هذه المشكلة في CSS عن طريق تعديل قاعدة نمط FOUC. في المتصفّحات التي تتيح استخدام نموذج إظهار الظل الإعلاني (DOM)، يتم تحويل العنصر <template shadowrootmode> على الفور إلى جذر الظل بدون ترك أي عنصر <template> في شجرة نموذج العناصر في المستند. في المتصفّحات التي لا تتوافق مع نموذج عرض Shadow DOM التعريفي، تحتفظ بالعنصر <template> الذي يمكننا استخدامه لمنع FOUC:

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

بدلاً من إخفاء العنصر المخصص الذي لم يتم تعريفه بعد، فإن "FOUC" المنقح تخفي قاعدة childchild عند اتّباع عنصر <template shadowrootmode>. وبعد تحديد العنصر المخصّص، لن تعود القاعدة متطابقة. يتم تجاهل القاعدة في المتصفّحات التي تتوافق مع نموذج عرض Shadow DOM التعريفي بسبب إزالة العنصر الثانوي <template shadowrootmode> أثناء تحليل HTML.

رصد الميزات والتوافق مع المتصفحات

يتوفّر نموذج Shadow DOM التعريفي منذ الإصدارين Chrome 90 وEdge 91، ولكنه كان يستخدم سمة غير عادية قديمة تُسمّى shadowroot بدلاً من سمة shadowrootmode الموحّدة. تتوفّر السمة shadowrootmode الجديدة وسلوك البث في إصدارَي Chrome 111 وEdge 111.

بما أنّها واجهة برمجة تطبيقات جديدة لنظام أساسي للويب، فإنّ نموذج Delarative Shadow DOM ليس له حتى الآن توافق على نطاق واسع في جميع المتصفحات. يمكن التأكّد من توافق المتصفّح من خلال التحقّق من توفّر السمة shadowRootMode على النموذج الأوّلي لـ HTMLTemplateElement:

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

الملء التلقائي

يُعد إنشاء شكل polyfill مبسط من أجل نموذج واجهة إعلان Shadow DOM أمرًا بسيطًا نسبيًا، نظرًا لأن استخدام polyfill لا يحتاج إلى تكرار دلالات التوقيت أو خصائص المحلل اللغوي فقط التي يهتم بها تنفيذ المتصفح فقط. لتعبئة polyfill Shadow DOM التعريفي، يمكننا فحص DOM للعثور على جميع عناصر <template shadowrootmode>، ثم تحويلها إلى جذور Shadow Roots مرفقة في العنصر الرئيسي. يمكن تنفيذ هذه العملية عندما يصبح المستند جاهزًا، أو يمكن تنفيذها من خلال أحداث أكثر تحديدًا مثل دورات حياة العناصر المخصّصة.

(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);

محتوى إضافي للقراءة