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

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

ملخّص

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

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

مقدمة

‫Shadow DOM هو أحد معايير Web Component الثلاثة: نماذج 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 المحدّد داخل DOM الظلّ على هذا النطاق. لا تسري قواعد الأنماط خارج نطاق الصفحة، ولا تسري أنماط الصفحة خارج نطاق الصفحة.
  • التركيب: يمكنك تصميم واجهة برمجة تطبيقات تعريفية مستندة إلى الترميز لمكوّنك.
  • تبسيط CSS: يعني نطاق DOM أنّه يمكنك استخدام أدوات اختيار CSS بسيطة وأسماء رموزال/فئات أكثر عمومية، ولا داعي للقلق بشأن تعارض الأسماء.
  • التطبيقات المخصّصة للإنتاجية: يجب تقسيم التطبيقات إلى أجزاء من نموذج DOM بدلاً من صفحة واحدة كبيرة (عامة).

عرض توضيحي لـ fancy-tabs

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

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

ما هو Shadow DOM؟

لمحة عن نموذج DOM

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

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

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

  • يستضيف المتصفّح حاليًا نموذج 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>
    `;
    }
    ...
});

هناك بعض الأمور المثيرة للاهتمام. أولاً، يُنشئ العنصر المخصّص 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 هذا خارج ملف ‎shadowed 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>

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

نتيجة توزيع المتصفّح لـ 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>

عنصر <slot>

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

يُسمح للعناصر "بعبور" حدود 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>

يمكنك أيضًا إنشاء خانات مُعنوَنة. الفتحات المُسمّاة هي مساحات فارغة محدّدة في 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) للمستخدمين لإلغاء الإعدادات التلقائية.

الأنماط التي يحدّدها المكوّن

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

  • لا تسري أدوات اختيار لغة 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> عنصر 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. تُضيف JavaScript للعنصر المخصّص (غير معروضة هنا) هذه القيمة للسمة في الوقت المناسب.

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

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

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 الوصول إلى عنصر shadow DOM في <video> لأنّ المتصفّح ينفّذه باستخدام جذر shadow في الوضع المغلق.

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

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() بالنسبة إلى الأحداث المرتبطة بالعناصر داخل ملف ملف تعريف الارتباط المظلّي، يتم عرض []

في ما يلي ملخّص الأسباب التي تجعلك لا تنشئ أبدًا عناصر ويب باستخدام {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 API أدوات للعمل مع الفتحات والعناصر الموزّعة. تكون هذه العناصر مفيدة عند إنشاء عنصر مخصّص.

حدث 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>. وبالمثل، سيكون document.activeElement هو <x-focus>. إذا تم إنشاء جذر الظل باستخدام 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: السلوك الصحيح.

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

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

delegatesFocus: false ويتم التركيز على الإدخال الداخلي.
delegatesFocus: false يتم التركيز على <input> الداخلي.
‫delegatesFocus: false وx-focus
    يحصل على التركيز (مثلاً، يحتوي على tabindex=&#39;0&#39;).
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> تعريفي. النماذج هي عنصر نائب مثالي لتعريف بنية مكوّن الويب.

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

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

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

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

دعم المتصفح

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

لعرض ميزة رصد نموذج shadow DOM، تحقّق من توفّر attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

حشو بوليستر

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

ثبِّت مجموعات polyfills:

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

استخدِم وحدات الملء:

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 وإخفاء أشجار DOM في المكوّن. إذا كنت تريد حدود أمان حقيقية، استخدِم <iframe>.

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

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

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

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