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

طريقة جديدة لتنفيذ Shadow DOM واستخدامه مباشرةً بتنسيق HTML.

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

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

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

تجنُّب وميض المحتوى غير الرسمي

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

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

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

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

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

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

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

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

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

يُعد إنشاء شكل polyfill مبسطًا لـ Delarative 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);

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