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

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

ملخّص

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

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

مقدمة

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

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

عرض المصدر على جيت هب

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

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

إذا تذكرت من قبل، لن يتم نقل نموذج DOM Light للمستخدم <slot>. عند توزيع العُقد في <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.

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

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

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

حدث تغيير الخانة

يتم تنشيط الحدث 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>دة 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. لا يتم نشر بعض الأحداث خارج 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>

النتيجة

delegatesFocus: السلوك الحقيقي.

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

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

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

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

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

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

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

المتصفحات المتوافقة

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

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

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

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

إلى أن يصبح التوافق مع المتصفحات متاحًا على نطاق واسع، تمنحك رموز polyfill الظلية والمظللة ميزة الإصدار الأول. يحاكي "نموذج العناصر في المستند" (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 للحصول على تعليمات حول كيفية تغيير أو توسيع نطاق الأنماط.

الخلاصة

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

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

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