Shadow DOM v1 - مكونات الويب المستقلة

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

ملخّص

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

إصلاح Shadow DOM CSS وDOM يقدّم الأنماط المُفصَّلة على الويب. بدون خادم. بدون أدوات أو اصطلاحات تسمية، يمكنك تجميع CSS مع الترميز، وإخفاء تفاصيل التنفيذ، ووظيفة المؤلف المستقلة المكونات في vanilla 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

ما هو 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، أنشِئ شجرة نموذج العناصر في المستند (DOM) ذات النطاق والتي يتم ربطها بالعنصر، ولكن بشكل منفصل عن الأطفال الفعليين. تُسمى هذه الشجرة الفرعية ضمن النطاق شجرة الظل. العنصر المرفق به مضيف الظل. يصبح أي شيء تضيفه في الظلال محلي إلى عنصر الاستضافة، بما في ذلك <style>. هذه هي الطريقة التي يتّبعها shadow DOM تحديد نطاق نمط CSS.

جارٍ إنشاء shadow 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 للمستخدم في الظل نموذج 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>

<الخانة> عنصر

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

مثال - الخانات في 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>

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

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

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

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

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

لمراقبة أنواع أخرى من التغييرات على light DOM، يمكنك إعداد MutationObserver في الدالة الإنشائية للعنصر.

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

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

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

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

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

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

نموذج حدث Shadow DOM

عندما ينبثق حدث من shadow DOM، يتم ضبط الهدف للحفاظ على التغليف الذي يوفره shadow DOM. وهذا يعني أن الأحداث تتم إعادة استهدافها لتبدو بمظهر كأنها تأتي من المكون بدلاً من العناصر الداخلية داخل shadow DOM. لا تنتشر بعض الأحداث خارج shadow 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>". وبالمثل، سيكون document.activeElement <x-focus>. فإذا كان جذر الظل تم إنشاؤه باستخدام mode:'open' (اطّلع على الوضع المغلق)، وستكون أيضًا من الوصول إلى العقدة الداخلية التي تم التركيز عليها:

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

إذا كانت هناك مستويات متعددة من shadow DOM قيد التشغيل (مثلاً، عنصر مخصّص داخل عنصر مخصص آخر)، فستحتاج إلى الانتقال بشكل متكرر إلى جذور الظلال البحث عن 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>

النتيجة

التفويض بالتركيز: السلوك الحقيقي.

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

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

التفويض بالتركيز: خطأ والتركيز على المدخل الداخلي.
يركّز delegatesFocus: false و<input> الداخلي.
التفويض بالتركيز: التركيز الخاطئة والتركيز
    التركيز (على سبيل المثال، يحتوي على tabindex=&#39;0&#39;).
delegatesFocus: false و<x-focus> يكتسب التركيز (على سبيل المثال، يحتوي على tabindex="0").
deleganceFocus: خطأ و&quot;نص Shadow DOM القابل للنقر&quot; CANNOT TRANSLATE
    (أو النقر على منطقة فارغة أخرى داخل shadow DOM للعنصر).
delegatesFocus: false و"نص Shadow DOM القابل للنقر" CANNOT TRANSLATE (أو النقر على منطقة فارغة أخرى داخل 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> تعد القوالب عنصرًا نائبًا مثاليًا للإعلان عن هيكل أحد مكونات الويب.

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

التاريخ دعم المتصفِّح

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

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

دعم المتصفح

تم شحن الإصدار 1 من Shadow DOM في Chrome 53 (الحالة)، Opera 40 وSafari 10 وFirefox 63 الحافة في التطوير.

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

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

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

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

تثبيت رموز polyfills:

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 للحصول على إرشادات حول كيفية تعديل الأنماط أو توسيع نطاقها.

الخاتمة

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

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

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

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

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

نعم، يمكنك استخدام الرمز polyfill. يُرجى الاطّلاع على دعم المتصفِّح.

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

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

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

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

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

راجِع جذور الظل المغلقة.