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

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

نموذج التعريف الشخصي لـ Shadow DOM هو ميزة لمنصّات الويب، وهي حاليًا في عملية توحيد المقاييس. ويتم تفعيلها تلقائيًا في الإصدار 111 من Chrome.

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>

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

وفي المستقبل، قد يكون من الممكن إعادة النظر في جذور الظل المشتركة. إذا تمكّن عنصر DOM من استخدام النماذج المضمَّنة، يمكن التعامل مع جذور الظل الوصفية كنماذج يتم إنشاء مثيل لها لإنشاء جذر الظل لعنصر معيّن. يتيح التصميم الحالي لـ Delarative 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>

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

تعريف الظل المستند إلى نموذج تعريفي هو ميزة في محلّل 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 مع تطبيق جذور الظل التعريفية هي تمرير خيار includeShadowRoots جديد إلى DOMParser:

<script>
  const html = `
    <div>
      <template shadowrootmode="open"></template>
    </div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; // No shadow root here
  const fragment = new DOMParser().parseFromString(html, 'text/html', {
    includeShadowRoots: true
  }); // Shadow root 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 احتياطية واحدة تتشاركها جميع جذور الظلال، ما يؤدي إلى إزالة النفقات المتكرّرة من الذاكرة.

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

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

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

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

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

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

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

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

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

بدلاً من إخفاء العنصر المخصّص الذي لم يتم تحديده بعد، تخفي قاعدة "FOUC" المعدَّلة العناصر الثانوية الخاصة بها عند اتّباع عنصر <template shadowrootmode>. بمجرد تحديد العنصر المخصص، لن تكون القاعدة مطابقة. يتم تجاهل القاعدة في المتصفحات التي تتوافق مع نموذج Delarative 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 مبسّط لـ Declarative Shadow DOM هو أمر بسيط نسبيًا، لأنّ رمز polyfill لا يحتاج إلى تكرار دلالات التوقيت أو خصائص المحلِّل فقط التي يتعلق بها تنفيذ المتصفِّح بشكل مثالي. من أجل polyfill Declarative 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);

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