نموذج Shadow DOM التعريفي هو ميزة عادية في النظام الأساسي للويب، وقد أصبح متوافقًا مع Chrome اعتبارًا من الإصدار 90. يُرجى العلم أنّه تم تغيير مواصفات هذه الميزة في عام 2023 (بما في ذلك إعادة تسمية shadowroot
إلى shadowrootmode
)، وأنّ أحدث إصدارات موحّدة من جميع أجزاء الميزة تم توفيرها في الإصدار 124 من Chrome.
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 مع العرض من جهة الخادم بسبب عدم توفّر طريقة مضمَّنة للتعبير عن جذور الظل في رمز 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 DevTools لعرض محتوى Shadow DOM. على سبيل المثال، يمثّل الحرف ↳
محتوى Light DOM المزود بخانات.
يمنحنا ذلك مزايا تغليف Shadow DOM وعرض خانات في صفحات HTML الثابتة. ولا حاجة إلى JavaScript لإنتاج الشجرة بأكملها، بما في ذلك Shadow Root.
ترطيب المكوّنات
يمكن استخدام 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>
نفسه. ومع ذلك، من غير المرجّح أن يكون ذلك مهمًا في معظم الحالات التي يتم فيها استخدام نموذج 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
احتياطيًا واحدًا تتم مشاركته مع جميع جذور الظل، ما يزيل النفقات العامة المتكرّرة للذاكرة.
لا تتوفّر جداول الأنماط القابلة للإنشاء في نموذج Shadow DOM التعريفي. ويعود السبب في ذلك إلى أنّه لا تتوفّر حاليًا طريقة لتسلسل جداول الأنماط القابلة للإنشاء في HTML، ولا تتوفّر طريقة للإشارة إليها عند تعبئة adoptedStyleSheets
.
كيفية تجنُّب ظهور محتوى غير منسق
إحدى المشاكل المحتملة في المتصفّحات التي لا تتوافق بعد مع نموذج عرض العناصر بالظلال التعريفي هي تجنّب "وميض محتوى غير نمطي" (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 التعريفي إلى أن يتم تحميل العنصر البديل لنموذج 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 التعريفي منذ الإصدارين Chrome 90 وEdge 91، ولكنه كان يستخدم سمة غير عادية قديمة تُسمّى shadowroot
بدلاً من سمة shadowrootmode
الموحّدة. تتوفّر السمة shadowrootmode
الجديدة وسلوك البث في الإصدار 111 من Chrome والإصدار 111 من Edge.
بما أنّها واجهة برمجة تطبيقات جديدة لنظام أساسي للويب، فإنّ نموذج 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 المرتبطة بالعنصر الرئيسي. يمكن تنفيذ هذه العملية عندما يصبح المستند جاهزًا، أو يمكن تنفيذها من خلال أحداث أكثر تحديدًا مثل دورات حياة العناصر المخصّصة.
(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);