الإصدار 1 من Shadow DOM: مكوّنات ويب مستقلة

يتيح Shadow DOM لمطوّري تطبيقات الويب إنشاء DOM وCSS مقسَّمين لمكوّنات الويب

ملخّص

يزيل Shadow DOM هشاشة إنشاء تطبيقات الويب. ينشأ هذا الهشاشة من الطبيعة العالمية لـ HTML وCSS وJS. على مر السنين، ابتكرنا عددًا هائلاً من الأدوات لتجاوز المشاكل. على سبيل المثال، عند استخدام معرّف/فئة HTML جديدة، لا يمكن معرفة ما إذا كان سيتعارض مع اسم حالي تستخدمه الصفحة. تظهر أخطاء دقيقة، وتصبح خصوصية CSS مشكلة كبيرة (!important كل شيء!)، وتخرج أدوات اختيار الأنماط عن السيطرة، ويمكن أن يتأثر الأداء. والقائمة تطول

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

مقدمة

‫Shadow DOM هو أحد معايير مكوّنات الويب الثلاثة: نماذج HTML وShadow DOM والعناصر المخصّصة. عمليات استيراد HTML كانت جزءًا من القائمة ولكنها تعتبر الآن متوقفة نهائيًا.

ليس عليك إنشاء مكوّنات ويب تستخدم shadow DOM. ولكن عند استخدامها، يمكنك الاستفادة من مزاياها (تحديد نطاق CSS وتغليف DOM والتركيب) وإنشاء عناصر مخصّصة قابلة لإعادة الاستخدام، وهي مرنة وقابلة للضبط بدرجة كبيرة وقابلة لإعادة الاستخدام بشكل كبير. إذا كانت العناصر المخصّصة هي الطريقة لإنشاء رمز HTML جديد (باستخدام واجهة برمجة تطبيقات JS)، فإنّ shadow DOM هو الطريقة التي توفّر بها رمزَي HTML وCSS. تتّحد واجهتا برمجة التطبيقات لإنشاء مكوّن يحتوي على HTML وCSS وJavaScript مستقلة.

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

  • نموذج DOM معزول: يكون نموذج DOM الخاص بالمكوّن مستقلاً (على سبيل المثال، لن يعرض document.querySelector() العُقد في shadow DOM الخاص بالمكوّن).
  • لغة CSS ذات النطاق المحدّد: يتم تحديد نطاق لغة CSS المحدّدة داخل Shadow DOM. لا تتسرّب قواعد الأنماط ولا تتداخل أنماط الصفحة.
  • التركيب: صمِّم واجهة برمجة تطبيقات تعريفية مستندة إلى الترميز للمكوّن.
  • تبسيط CSS: يعني DOM ذو النطاق المحدّد أنّه يمكنك استخدام أدوات اختيار لغة CSS البسيطة، وأسماء المعرّفات/الفئات الأكثر عمومية، وعدم القلق بشأن تعارضات الأسماء.
  • الإنتاجية: فكِّر في التطبيقات على أنّها أجزاء من نموذج العناصر في المستند (DOM) بدلاً من صفحة واحدة كبيرة (عالمية).

عرض توضيحي عن fancy-tabs

في هذه المقالة، سأشير إلى مكوّن تجريبي (<fancy-tabs>) ومقتطفات الرمز البرمجي منه. إذا كان متصفحك يدعم واجهات برمجة التطبيقات، فيجب أن ترى عرضًا توضيحيًا مباشرًا لها أدناه. وإلا، يمكنك الاطّلاع على المصدر الكامل على GitHub.

عرض المصدر على Github

ما هو Shadow DOM؟

معلومات أساسية عن نموذج DOM

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

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

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

ينتج ترميز HTML التالي:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

كل هذا جيد. إذًا، ما هو shadow DOM؟

‫DOM… في الظل

‫Shadow DOM هو DOM عادي مع اختلافَين: 1) طريقة إنشائه/استخدامه و2) طريقة سلوكه مقارنةً ببقية الصفحة. عادةً، يمكنك إنشاء عُقد DOM وإلحاقها كعناصر فرعية لعنصر آخر. باستخدام Shadow DOM، يمكنك إنشاء شجرة نموذج العناصر في المستند ذات نطاق محدّد ومرتبطة بالعنصر، ولكنها منفصلة عن العناصر الثانوية الفعلية. تُعرف هذه الشجرة الفرعية المحدّدة النطاق باسم شجرة الظل. ويُطلق على العنصر الذي يتم إرفاقها به اسم مضيف الظل. أي شيء تضيفه في الظلال يصبح محليًا لعنصر الاستضافة، بما في ذلك <style>. هذه هي الطريقة التي يحقق بها shadow DOM نطاق نمط CSS.

إنشاء نموذج DOM ظل

جذر الظل هو جزء من مستند يتم إرفاقه بعنصر "مضيف". وعند إرفاق جذر ظل، يكتسب العنصر نموذج shadow DOM. لإنشاء shadow DOM لعنصر، يمكنك استدعاء element.attachShadow():

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

أستخدم .innerHTML لملء جذر الظل، ولكن يمكنك أيضًا استخدام واجهات برمجة تطبيقات DOM أخرى. هذا هو الويب. لدينا خيار.

تحدّد المواصفات قائمة بالعناصر التي لا يمكنها استضافة شجرة ظل. هناك عدة أسباب قد تجعل العنصر مدرجًا في القائمة:

  • يستضيف المتصفّح حاليًا نموذج shadow DOM الداخلي الخاص به للعنصر (<textarea> و<input>).
  • ليس من المنطقي أن يستضيف العنصر shadow DOM ‏ (<img>).

على سبيل المثال، لا يعمل ما يلي:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

إنشاء shadow DOM لعنصر مخصّص

يكون Shadow DOM مفيدًا بشكل خاص عند إنشاء عناصر مخصّصة. استخدِم shadow DOM لتقسيم HTML وCSS وJS لعنصر معيّن، وبالتالي إنشاء "مكوّن ويب".

مثال: عنصر مخصّص يربط shadow DOM بنفسه، ويغلّف DOM/CSS:

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
        <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
        <div id="tabs">...</div>
        <div id="panels">...</div>
    `;
    }
    ...
});

هناك بعض الأشياء المثيرة للاهتمام التي تحدث هنا. الأول هو أنّ العنصر المخصّص ينشئ shadow DOM الخاص به عند إنشاء مثيل من <fancy-tabs>. يمكنك إجراء ذلك في constructor(). ثانيًا، بما أنّنا ننشئ جذر ظل، سيتم تحديد نطاق قواعد CSS داخل <style> على <fancy-tabs>.

التكوين والفتحات

يُعدّ التركيب من أقل ميزات shadow DOM فهمًا، ولكنّه الأهم بلا شك.

في عالم تطوير تطبيقات الويب، التركيب هو كيفية إنشاء التطبيقات، بشكل تعريفي من HTML. تتجمع الوحدات الأساسية المختلفة (<div> و<header> و<form> و<input>) لتشكيل التطبيقات. تعمل بعض هذه العلامات مع بعضها البعض. التكوين هو السبب في أنّ العناصر الأصلية مثل <select> و<details> و<form> و<video> مرنة للغاية. تقبل كل من هذه العلامات بعض HTML كعناصر فرعية وتقوم بشيء خاص معهم. على سبيل المثال، يعرف <select> كيفية عرض <option> و<optgroup> في القائمة المنسدلة والتطبيقات المصغّرة المتعددة الاختيارات. يعرض العنصر <details> السمة <summary> كسهم قابل للتوسيع. حتى <video> يعرف كيفية التعامل مع عناصر ثانوية معيّنة: لا يتم عرض عناصر <source>، ولكنّها تؤثر في سلوك الفيديو. يا له من سحر!

المصطلحات: Light DOM مقابل shadow DOM

يقدّم تركيب Shadow DOM مجموعة من الأساسيات الجديدة في تطوير الويب. قبل الخوض في التفاصيل، لنتفق على بعض المصطلحات لكي نكون على وفاق.

‫Light DOM

الترميز الذي يكتبه مستخدم المكوّن. يتواجد DOM هذا خارج shadow DOM الخاص بالمكوّن. وهي العناصر الثانوية الفعلية للعنصر.

<better-button>
    <!-- the image and span are better-button's light DOM -->
    <img src="gear.svg" slot="icon">
    <span>Settings</span>
</better-button>

Shadow DOM

هو نموذج DOM الذي يكتبه مؤلف المكوّن. يكون Shadow DOM محليًا للمكوّن ويحدّد بنيته الداخلية ونطاق CSS ويغلف تفاصيل التنفيذ. ويمكنه أيضًا تحديد كيفية عرض الترميز الذي أنشأه مستخدم المكوّن.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

شجرة نموذج العناصر في المستند (DOM) المسطّحة

نتيجة توزيع المتصفّح لمحتوى light DOM الخاص بالمستخدم في shadow DOM، ما يؤدي إلى عرض المنتج النهائي. الشجرة المسطحة هي ما تراه في النهاية في أدوات مطوري البرامج وما يتم عرضه على الصفحة.

<better-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="gear.svg" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
        <span>Settings</span>
        </slot>
    </span>
</better-button>

العنصر <slot>

يجمع Shadow DOM بين أشجار نموذج العناصر في المستند المختلفة باستخدام العنصر <slot>. الخانات هي عناصر نائبة داخل المكوّن يمكن للمستخدمين ملؤها باستخدام الترميز الخاص بهم. من خلال تحديد خانة واحدة أو أكثر، يمكنك دعوة ترميز خارجي للعرض في نموذج العناصر في المستند (DOM) الظل للمكوّن. أنت تقول في الأساس "اعرض ترميز المستخدم هنا".

يُسمح للعناصر بـ "تجاوز" حدود shadow DOM عندما يدعوها <slot> إلى ذلك. تُسمّى هذه العناصر العُقَد الموزَّعة. من الناحية المفاهيمية، يمكن أن تبدو العُقَد الموزَّعة غريبة بعض الشيء. لا تنقل الفتحات DOM فعليًا، بل تعرضه في موقع آخر داخل shadow DOM.

يمكن أن يحدّد المكوّن صفرًا أو أكثر من الخانات في shadow DOM. يمكن أن تكون الخانات فارغة أو توفّر محتوى احتياطيًا. إذا لم يقدّم المستخدم محتوى light DOM، ستعرض الخانة المحتوى الاحتياطي.

<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>

<slot>fallback content</slot> <!-- default slot with fallback content -->

<slot> <!-- default slot entire DOM tree as fallback -->
    <h2>Title</h2>
    <summary>Description text</summary>
</slot>

يمكنك أيضًا إنشاء خانات مُعنوَنة. الفتحات المُسمّاة هي فتحات محددة في نموذج العناصر في الظل يشير إليها المستخدمون بالاسم.

مثال: الخانات في shadow DOM الخاص بـ <fancy-tabs>:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

يعلن مستخدمو المكوّن عن <fancy-tabs> على النحو التالي:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

وإذا كنت تتساءل، فإنّ الشجرة المسطّحة تبدو على النحو التالي:

<fancy-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="panels">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</fancy-tabs>

لاحظ أن المكوِّن الخاص بنا قادر على التعامل مع تكوينات مختلفة، لكن شجرة نموذج العناصر في المستند المسطحة تظل كما هي. يمكننا أيضًا التبديل من <button> إلى <h2>. تم إنشاء هذا المكوّن للتعامل مع أنواع مختلفة من العناصر الفرعية، تمامًا مثل <select>.

التصميم

هناك العديد من الخيارات لتصميم مكوّنات الويب. يمكن تصميم مكوّن يستخدم نموذج DOM الظل من خلال الصفحة الرئيسية أو تحديد أنماطه الخاصة أو توفير عناصر ربط (في شكل خصائص CSS مخصّصة) للمستخدمين لتجاوز الإعدادات التلقائية.

الأنماط المحدّدة في المكوّن

إنّ الميزة الأكثر فائدة في shadow DOM هي نطاق CSS:

  • لا تنطبق أدوات اختيار لغة CSS من الصفحة الخارجية داخل المكوّن.
  • لا تظهر الأنماط المحدّدة في الداخل. ويتم تحديد نطاقها في عنصر المضيف.

تنطبق أدوات اختيار لغة CSS المستخدَمة داخل shadow DOM على مكوّنك محليًا. من الناحية العملية، يعني هذا أنه يمكننا استخدام أسماء المعرّفات/الفئات الشائعة مرة أخرى، بدون القلق بشأن التعارضات في أي مكان آخر على الصفحة. تُعد أدوات اختيار لغة CSS الأبسط من أفضل الممارسات داخل Shadow DOM. وهي مفيدة أيضًا للأداء.

مثال: الأنماط المحدّدة في جذر الظل تكون محلية

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

يتم أيضًا تحديد نطاق أوراق الأنماط على شجرة الظل:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

هل تساءلت يومًا كيف يعرض العنصر <select> تطبيقًا مصغّرًا متعدد الاختيارات (بدلاً من قائمة منسدلة) عند إضافة السمة multiple؟

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

يمكن لـ <select> تصميم نفسه بشكل مختلف بناءً على السمات التي تعلن عنها. يمكن لمكوّنات الويب أن تصمّم نفسها أيضًا، باستخدام أداة الاختيار :host.

مثال: مكوّن ينسّق نفسه

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

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

يتيح لك الشكل الوظيفي لـ :host(<selector>) استهداف المضيف إذا كان يتطابق مع <selector>. هذه طريقة رائعة لمكوّنك لتغليف السلوكيات التي تتفاعل مع تفاعل المستخدم أو الحالة أو نمط العُقد الداخلية بناءً على المضيف.

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

التنسيق استنادًا إلى السياق

تطابق السمة :host-context(<selector>) المكوّن إذا كان هو أو أي من عناصره الرئيسية يطابق السمة <selector>. من الاستخدامات الشائعة لذلك هو تحديد المظهر استنادًا إلى محيط المكوّن. على سبيل المثال، يطبّق العديد من الأشخاص المظاهر من خلال تطبيق فئة على <html> أو <body>:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

سيتم تطبيق النمط :host-context(.darktheme) على <fancy-tabs> عندما يكون عنصرًا فرعيًا لـ .darktheme:

:host-context(.darktheme) {
    color: white;
    background: black;
}

يمكن أن يكون :host-context() مفيدًا في إنشاء المظاهر، ولكن من الأفضل إنشاء نقاط ربط للأنماط باستخدام خصائص CSS المخصّصة.

تنسيق العُقَد الموزَّعة

يطابق ::slotted(<compound-selector>) العُقد التي يتم توزيعها في <slot>.

لنفترض أنّنا أنشأنا مكوّنًا لشارة الاسم:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

يمكن لـ shadow DOM الخاص بالمكوّن أن ينسّق <h2> و.title للمستخدم:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

إذا كنت تتذكر من قبل، فإن <slot>s لا تنقل DOM الخفيف للمستخدم. عندما يتم توزيع العُقد في <slot>، يعرض <slot> نموذج DOM الخاص بها، ولكن تبقى العُقد في مكانها. تستمر الأنماط التي تم تطبيقها قبل التوزيع في التطبيق بعد التوزيع. ومع ذلك، عندما يتم توزيع light DOM، يمكن أن يتخذ أنماطًا إضافية (تلك التي يحددها shadow DOM).

إليك مثال آخر أكثر تفصيلاً من <fancy-tabs>:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        border-radius: 3px;
        padding: 16px;
        height: 250px;
        overflow: auto;
    }
    #tabs {
        display: inline-flex;
        -webkit-user-select: none;
        user-select: none;
    }
    #tabsSlot::slotted(*) {
        font: 400 16px/22px 'Roboto';
        padding: 16px 8px;
        ...
    }
    #tabsSlot::slotted([aria-selected="true"]) {
        font-weight: 600;
        background: white;
        box-shadow: none;
    }
    #panelsSlot::slotted([aria-hidden="true"]) {
        display: none;
    }
    </style>
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
`;

في هذا المثال، هناك خانتان: خانة مُعنوَنة لعناوين علامات التبويب، وخانة لمحتوى لوحة علامات التبويب. عندما يختار المستخدم علامة تبويب، نبرز اختياره ونكشف عن لوحته. ويتم ذلك من خلال اختيار العُقد الموزّعة التي تتضمّن السمة selected. يضيف ملف JS للعنصر المخصّص (غير معروض هنا) هذه السمة في الوقت المناسب.

تنسيق مكوّن من الخارج

هناك طريقتان لتصميم مكوّن من الخارج. أسهل طريقة هي استخدام اسم العلامة كمحدِّد:

fancy-tabs {
    width: 500px;
    color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
    box-shadow: 0 3px 3px #ccc;
}

الأنماط الخارجية لها الأولوية دائمًا على الأنماط المحدّدة في shadow DOM. على سبيل المثال، إذا كتب المستخدم المحدِّد fancy-tabs { width: 500px; }، سيتجاوز قاعدة المكوّن: :host { width: 650px;}.

إنّ تصميم المكوّن نفسه لن يمنحك سوى خيارات محدودة. ولكن ماذا يحدث إذا أردت تنسيق العناصر الداخلية لمكوّن؟ لذلك، نحتاج إلى خصائص CSS المخصصة.

إنشاء عناصر ربط الأنماط باستخدام خصائص CSS المخصّصة

يمكن للمستخدمين تعديل الأنماط الداخلية إذا كان مؤلف المكوّن يوفّر عناصر ربط الأنماط باستخدام خصائص CSS المخصّصة. من الناحية النظرية، تشبه الفكرة <slot>. يمكنك إنشاء "عناصر نائبة للأنماط" ليتمكّن المستخدمون من إلغائها.

مثال: يسمح <fancy-tabs> للمستخدمين بتجاوز لون الخلفية:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

داخل shadow DOM:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

في هذه الحالة، سيستخدم المكوّن black كقيمة الخلفية لأنّ المستخدم قدّمها. وإلا، سيتم ضبطه تلقائيًا على #9E9E9E.

مواضيع متقدّمة

إنشاء جذور ظل مغلقة (يجب تجنُّبها)

هناك نوع آخر من نموذج DOM الظل يُسمى وضع "مغلق". عند إنشاء شجرة ظل مغلقة، لن يتمكّن رمز JavaScript الخارجي من الوصول إلى DOM الداخلي للمكوّن. يشبه ذلك طريقة عمل العناصر الأصلية، مثل <video>. لا يمكن لـ JavaScript الوصول إلى نموذج العناصر في المستند (DOM) الظلّي لـ "<video>" لأنّ المتصفّح ينفّذه باستخدام جذر ظلّي في الوضع المغلق.

مثال - إنشاء شجرة ظل مغلقة:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

تتأثر أيضًا واجهات برمجة التطبيقات الأخرى بوضع الاختبار المُغلق:

  • ‫Element.assignedSlot / TextNode.assignedSlot تُرجع null
  • Event.composedPath() بالنسبة إلى الأحداث المرتبطة بالعناصر داخل shadow DOM، يتم عرض []

إليك ملخّصي حول سبب عدم إنشاء مكوّنات ويب باستخدام {mode: 'closed'}:

  1. شعور زائف بالأمان ما مِن شيء يمنع المهاجم من الاستيلاء على Element.prototype.attachShadow.

  2. يمنع الوضع المغلق رمز العنصر المخصّص من الوصول إلى shadow DOM الخاص به. هذا فشل كامل. بدلاً من ذلك، عليك تخزين مرجع لاستخدامه لاحقًا إذا أردت استخدام ميزات مثل querySelector(). وهذا يتعارض تمامًا مع الغرض الأصلي من "وضع الاختبارات".

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. يجعل الوضع المغلق المكوّن أقل مرونة للمستخدمين النهائيين. أثناء إنشاء مكونات الويب، سيأتي وقت تنسى فيه إضافة ميزة. خيار ضبط حالة استخدام يريدها المستخدم ومن الأمثلة الشائعة على ذلك نسيان تضمين عناصر ربط كافية لتنسيق العُقَد الداخلية. في الوضع المغلق، لا يمكن للمستخدمين إلغاء الإعدادات التلقائية وتعديل الأنماط. إنّ إمكانية الوصول إلى العناصر الداخلية للمكوّن مفيدة للغاية. في النهاية، سيقوم المستخدمون بتفرع المكون الخاص بك، أو العثور على مكون آخر، أو إنشاء مكون خاص بهم إذا لم يفعل ما يريدون :(

التعامل مع الخانات في JavaScript

توفّر واجهة برمجة التطبيقات shadow DOM أدوات للتعامل مع الخانات والعُقَد الموزَّعة. وهي مفيدة عند إنشاء عنصر مخصّص.

حدث slotchange

يتم تنشيط الحدث slotchange عند تغيير العُقد الموزَّعة في الخانة. على سبيل المثال، إذا أضاف المستخدم عناصر فرعية إلى DOM الخفيف أو أزالها منه.

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

لمراقبة أنواع أخرى من التغييرات في نموذج DOM البسيط، يمكنك إعداد MutationObserver في دالة الإنشاء الخاصة بالعنصر.

ما هي العناصر التي يتم عرضها في خانة؟

من المفيد أحيانًا معرفة العناصر المرتبطة بخانة إعلانية معيّنة. يمكنك استدعاء slot.assignedNodes() لمعرفة العناصر التي تعرضها الخانة. سيؤدي الخيار {flatten: true} أيضًا إلى عرض المحتوى الاحتياطي للخانة (في حال عدم توزيع أي عُقد).

على سبيل المثال، لنفترض أنّ shadow DOM يبدو على النحو التالي:

<slot><b>fallback content</b></slot>
الاستخدامالاتصالالنتيجة
<my-component>component text</my-component> slot.assignedNodes(); [component text]
<my-component></my-component> slot.assignedNodes(); []
<my-component></my-component> slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

ما هي الخانة التي تم تخصيص عنصر لها؟

يمكن أيضًا الإجابة عن السؤال العكسي. يخبرك element.assignedSlot بأي من خانات المكونات تم تعيين العنصر الخاص بك.

نموذج أحداث Shadow DOM

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

الأحداث التي تتجاوز حدود الظل هي:

  • أحداث التركيز: blur وfocus وfocusin وfocusout
  • أحداث الماوس: click وdblclick وmousedown وmouseenter وmousemove وما إلى ذلك
  • أحداث العجلة: wheel
  • أحداث الإدخال: beforeinput وinput
  • أحداث لوحة المفاتيح: keydown، keyup
  • أحداث الإنشاء: compositionstart، compositionupdate، compositionend
  • ‫DragEvent: ‏dragstart وdrag وdragend وdrop وما إلى ذلك

نصائح

إذا كانت شجرة الظل مفتوحة، سيؤدي استدعاء event.composedPath() إلى عرض مصفوفة من العُقد التي مرّ بها الحدث.

استخدام الأحداث المخصّصة

لا تخرج أحداث DOM المخصّصة التي يتم تشغيلها على العُقَد الداخلية في شجرة الظل من حدود الظل ما لم يتم إنشاء الحدث باستخدام العلامة composed: true:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

إذا كانت القيمة composed: false (تلقائي)، لن يتمكّن المستهلكون من الاستماع إلى الحدث خارج جذر الظل.

<fancy-tabs></fancy-tabs>
<script>
    const tabs = document.querySelector('fancy-tabs');
    tabs.addEventListener('tab-select', e => {
    // won't fire if `tab-select` wasn't created with `composed: true`.
    });
</script>

التعامل مع التركيز

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

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

سيظهر حدث "focus" كما لو أنّه وارد من "<x-focus>"، وليس من "<input>". وبالمثل، سيتم <x-focus> "document.activeElement". إذا تم إنشاء جذر الظل باستخدام mode:'open' (راجِع الوضع المغلق)، ستتمكّن أيضًا من الوصول إلى العقدة الداخلية التي تم التركيز عليها:

document.activeElement.shadowRoot.activeElement // only works with open mode.

إذا كانت هناك مستويات متعددة من shadow DOM (مثلاً عنصر مخصّص ضمن عنصر مخصّص آخر)، عليك التعمّق بشكل متكرر في جذور shadow للعثور على activeElement:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

هناك خيار آخر للتركيز، وهو الخيار delegatesFocus: true، الذي يوسّع سلوك التركيز للعناصر ضمن شجرة الظل:

  • إذا نقرت على عقدة داخل shadow DOM ولم تكن العقدة منطقة قابلة للتركيز، يتم التركيز على أول منطقة قابلة للتركيز.
  • عندما يتم التركيز على عقدة داخل shadow DOM، يتم تطبيق :focus على المضيف بالإضافة إلى العنصر الذي تم التركيز عليه.

مثال: كيف يغيّر delegatesFocus: true سلوك التركيز

<style>
    :focus {
    outline: 2px solid red;
    }
</style>

<x-focus></x-focus>

<script>
customElements.define('x-focus', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    const root = this.attachShadow({mode: 'open', delegatesFocus: true});
    root.innerHTML = `
        <style>
        :host {
            display: flex;
            border: 1px dotted black;
            padding: 16px;
        }
        :focus {
            outline: 2px solid blue;
        }
        </style>
        <div>Clickable Shadow DOM text</div>
        <input type="text" placeholder="Input inside shadow dom">`;

    // Know the focused element inside shadow DOM:
    this.addEventListener('focus', function(e) {
        console.log('Active element (inside shadow dom):',
                    this.shadowRoot.activeElement);
    });
    }
});
</script>

النتيجة

delegatesFocus: true behavior.

أعلاه هي النتيجة عندما يتم التركيز على <x-focus> (نقرة المستخدم، أو النقر على مفتاح التبويب، أو focus()، وما إلى ذلك)، أو النقر على "نص Shadow DOM قابل للنقر"، أو التركيز على <input> الداخلي (بما في ذلك autofocus).

إذا ضبطت delegatesFocus: false، إليك ما سيظهر لك بدلاً من ذلك:

‫delegatesFocus: false ويتم التركيز على الإدخال الداخلي.
‫delegatesFocus: false و<input> الداخلي في وضع التركيز.
‫delegatesFocus: false وx-focus
    يتم التركيز عليه (مثلاً، يحتوي على tabindex='0').
يتم التركيز على delegatesFocus: false و<x-focus> (على سبيل المثال، يحتوي على tabindex="0").
‫delegatesFocus: false وتم النقر على &quot;نص Shadow DOM قابل للنقر&quot; (أو تم النقر على منطقة فارغة أخرى ضمن Shadow DOM للعنصر).
يتم النقر على delegatesFocus: false و"نص Shadow DOM قابل للنقر" (أو يتم النقر على منطقة فارغة أخرى داخل Shadow DOM للعنصر).

نصائح

على مر السنين، تعلّمتُ بعض المعلومات عن إنشاء مكوّنات الويب. أعتقد أنّ بعض هذه النصائح ستكون مفيدة لك في إنشاء المكوّنات وتصحيح أخطاء shadow DOM.

استخدام احتواء CSS

عادةً ما يكون التنسيق/النمط/الرسم لمكوّن الويب مستقلاً بذاته إلى حد كبير. استخدِم احتواء CSS في :host لتحسين الأداء:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

إعادة ضبط الأنماط القابلة للتوريث

تستمر الأنماط القابلة للتوريث (background وcolor وfont وline-height وما إلى ذلك) في التوريث في shadow DOM. وهذا يعني أنّها تخترق حدود نموذج shadow DOM تلقائيًا. إذا أردت البدء من جديد، استخدِم all: initial; لإعادة ضبط الأنماط القابلة للتوريث على قيمتها الأولية عندما تتجاوز حدود الظل.

<style>
    div {
    padding: 10px;
    background: red;
    font-size: 25px;
    text-transform: uppercase;
    color: white;
    }
</style>

<div>
    <p>I'm outside the element (big/white)</p>
    <my-element>Light DOM content is also affected.</my-element>
    <p>I'm outside the element (big/white)</p>
</div>

<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    :host {
        all: initial; /* 1st rule so subsequent properties are reset. */
        display: block;
        background: white;
    }
    </style>
    <p>my-element: all CSS properties are reset to their
        initial value using <code>all: initial</code>.</p>
    <slot></slot>
`;
</script>

العثور على جميع العناصر المخصّصة التي تستخدمها الصفحة

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

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

إنشاء عناصر من <template>

بدلاً من تعبئة جذر الظل باستخدام .innerHTML، يمكننا استخدام <template> تعريفي. تُعدّ النماذج عنصرًا نائبًا مثاليًا لتحديد بنية مكوّن الويب.

اطّلِع على المثال في "العناصر المخصّصة: إنشاء مكوّنات ويب قابلة لإعادة الاستخدام".

السجلّ والتوافق مع المتصفّح

إذا كنت تتابع مكوّنات الويب خلال العامين الماضيين، ستعرف أنّ الإصدار 35 من Chrome والإصدارات الأحدث/Opera قد تم شحن إصدار أقدم من Shadow DOM لبعض الوقت. سيواصل محرّك Blink إتاحة كلا الإصدارَين بشكل متوازٍ لبعض الوقت. قدّمت مواصفات الإصدار 0 طريقة مختلفة لإنشاء جذر ظل (element.createShadowRoot بدلاً من element.attachShadow في الإصدار 1). يؤدي استدعاء الطريقة القديمة إلى الاستمرار في إنشاء جذر ظل باستخدام دلالات الإصدار 0، لذلك لن يتعطل رمز الإصدار 0 الحالي.

إذا كنت مهتمًا بالمواصفات القديمة للإصدار 0، يمكنك الاطّلاع على مقالات html5rocks: 1، 2، 3. هناك أيضًا مقارنة رائعة بين الاختلافات بين الإصدارين 0 و1 من Shadow DOM.

دعم المتصفح

تم شحن Shadow DOM v1 في Chrome 53 (الحالة) وOpera 40 وSafari 10 وFirefox 63. بدأت Edge عملية التطوير.

لاكتشاف ميزة shadow DOM، تحقَّق من توفّر attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

إلى أن يتوفّر دعم المتصفّح على نطاق واسع، تمنحك مكوّنات polyfill من النوعين shadydom وshadycss ميزة الإصدار 1. يحاكي Shady DOM نطاق DOM لـ Shadow DOM وshadycss polyfills خصائص CSS المخصّصة ونطاق الأنماط الذي توفّره واجهة برمجة التطبيقات الأصلية.

ثبِّت الرموز البرمجية polyfill:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

استخدِم الرموز البرمجية polyfill التالية:

function loadScript(src) {
    return new Promise(function(resolve, reject) {
    const script = document.createElement('script');
    script.async = true;
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
    });
}

// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
    loadScript('/bower_components/shadydom/shadydom.min.js')
    .then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
    .then(e => {
        // Polyfills loaded.
    });
} else {
    // Native shadow dom v1 support. Go to go!
}

يمكنك الاطّلاع على https://github.com/webcomponents/shadycss#usage للحصول على تعليمات حول كيفية استخدام shim/scope في الأنماط.

الخاتمة

لأول مرة على الإطلاق، لدينا واجهة برمجة تطبيقات بدائية تقوم بتحديد نطاق CSS المناسب، وتحديد نطاق DOM، ولها تركيبة حقيقية. عند دمج Shadow DOM مع واجهات برمجة تطبيقات أخرى لمكوّنات الويب، مثل العناصر المخصّصة، يوفّر طريقة لإنشاء مكوّنات مغلّفة بالكامل بدون استخدام حيل أو استخدام عناصر قديمة مثل <iframe>.

لا تفهموني خطأ. إنّ Shadow DOM هو بالتأكيد وحش معقّد. لكنّها أداة تستحقّ التعلّم اقضِ بعض الوقت معها. تعرَّف على هذه الميزة واطرح الأسئلة.

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

الأسئلة الشائعة

هل يمكنني استخدام الإصدار 1 من Shadow DOM اليوم؟

نعم، باستخدام polyfill. يُرجى الاطّلاع على المتصفّحات المتوافقة.

ما هي ميزات الأمان التي يوفّرها shadow DOM؟

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

هل يجب أن يستخدم مكوّن الويب shadow DOM؟

لا. ليس عليك إنشاء مكوّنات ويب تستخدم shadow DOM. ومع ذلك، فإنّ إنشاء عناصر مخصّصة تستخدم Shadow DOM يعني أنّه يمكنك الاستفادة من ميزات مثل تحديد نطاق CSS وتغليف DOM والتركيب.

ما الفرق بين جذور الظل المفتوحة والمغلقة؟

يُرجى الاطّلاع على جذور الظل المغلقة.