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

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

توافق المتصفّح

  • Chrome: 111
  • Edge: 111.
  • Firefox: 123.
  • ‫Safari: 16.4

المصدر

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

يزيل نموذج 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 DevTools لعرض محتوى Shadow DOM. على سبيل المثال، يمثّل الحرف محتوى Light DOM المُدرَج في خانة.

يمنحنا ذلك مزايا تغليف Shadow DOM وعرض الخانات في HTML الثابت. ولا يلزم استخدام JavaScript لإنشاء الشجرة بأكملها، بما في ذلك جذر الظل.

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

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

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

في المستقبل، قد يكون من الممكن إعادة النظر في الجذور المظلّلة المشتركة. إذا أصبح نموذج DOM متوافقًا مع النماذج المضمّنة، يمكن التعامل مع نماذج Shadow 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 احتياطيًا واحدًا تتم مشاركته مع جميع جذور الظل، ما يزيل المساحة الزائدة التي تستهلكها الذاكرة المكرّرة.

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

كيفية تجنُّب ظهور محتوى غير منسق

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

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

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

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

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

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

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

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

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

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

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

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

حشو بوليستر

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

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

مراجع إضافية