‫Shadow DOM v1 - عناصر وب مستقل

Shadow DOM به توسعه‌دهندگان وب اجازه می‌دهد DOM و CSS بخش‌بندی‌شده برای مؤلفه‌های وب ایجاد کنند

خلاصه

Shadow DOM شکنندگی ساخت برنامه‌های وب را ازبین می‌برد. شکنندگی از ماهیت جهانی HTML،‏ CSS، و JS ناشی می‌شود. درطول سال‌ها، تعداد بسیار زیادی ابزار برای دور زدن مشکلات اختراع کرده‌ایم. برای مثال، وقتی از شناسه/کلاس HTML جدیدی استفاده می‌کنید، نمی‌توان گفت که آیا با نام موجودی که صفحه استفاده می‌کند تداخل خواهد داشت یا نه. اشکالات جزئی به‌وجود می‌آید، ویژگی CSS به مشکل بزرگی تبدیل می‌شود (!important همه چیز!)، انتخاب‌کننده‌های سبک از کنترل خارج می‌شوند، و عملکرد ممکن است آسیب ببیند. این فهرست ادامه دارد.

Shadow DOM مشکلات CSS و DOM را برطرف می‌کند. این ویژگی سبک‌های محدودشده را به پلاتفرم وب معرفی می‌کند. بدون ابزار یا قراردادهای نام‌گذاری، می‌توانید CSS را با نشانه‌گذاری بسته‌بندی کنید، جزئیات پیاده‌سازی را پنهان کنید، و مؤلفه‌های مستقل را در جاوا اسکریپت وانیلی بنویسید.

مقدمه

‫Shadow DOM یکی از سه استاندارد «مؤلفه وب» است: الگوهای HTML، Shadow DOM و عناصر سفارشی. واردات HTML قبلاً بخشی از فهرست بود اما اکنون منسوخ درنظر گرفته می‌شود.

لازم نیست مؤلفه‌های وبی را که از DOM سایه استفاده می‌کنند تألیف کنید. اما وقتی این کار را انجام می‌دهید، از مزایای آن (محدوده CSS، کپسوله‌سازی DOM، ترکیب) بهره می‌برید و عناصر سفارشی قابل‌استفاده مجدد می‌سازید، که انعطاف‌پذیر، بسیار قابل‌پیکربندی، و به‌شدت قابل‌استفاده مجدد هستند. اگر عناصر سفارشی راهی برای ایجاد HTML جدید (با JS API) باشند، shadow DOM راهی است که HTML و CSS آن را ارائه می‌دهید. این دو میانای برنامه‌سازی کاربردی باهم ترکیب می‌شوند تا مؤلفه‌ای با HTML،‏ CSS، و JavaScript مستقل بسازند.

Shadow DOM به‌عنوان ابزاری برای ساختن برنامه‌های مبتنی بر مؤلفه طراحی شده است. بنابراین، راه‌حل‌هایی برای مشکلات رایج در توسعه وب ارائه می‌دهد:

  • DOM مجزا: DOM مؤلفه مستقل است (برای مثال، document.querySelector() گره‌ها را در DOM سایه مؤلفه برنمی‌گرداند).
  • CSS محدودشده: CSS تعریف‌شده در shadow DOM به آن محدود می‌شود. قوانین سبک به بیرون درز نمی‌کنند و سبک‌های صفحه به داخل نفوذ نمی‌کنند.
  • ترکیب‌بندی: یک میانای برنامه‌سازی کاربردی اعلانی و مبتنی بر نشانه‌گذاری برای مؤلفه خود طراحی کنید.
  • ساده‌سازی CSS - DOM محدودشده به این معنی است که می‌توانید از گزینشگرهای CSS ساده، نام‌های شناسه/کلاس عمومی‌تر استفاده کنید و نگران تداخل نام‌گذاری نباشید.
  • بهره‌وری - برنامه‌ها را به‌صورت تکه‌هایی از DOM درنظر بگیرید نه یک صفحه بزرگ (سراسری).

نسخه نمایشی fancy-tabs

در سراسر این مقاله، به یک مؤلفه نمایشی (<fancy-tabs>) و تکه‌کدهای آن ارجاع خواهم داد. اگر مرورگرتان از APIها پشتیبانی می‌کند، باید نسخه نمایشی زنده آن را در زیر ببینید. درغیراین‌صورت، منبع کامل را در Github بررسی کنید.

مشاهده منبع در Github

DOM سایه چیست؟

پیشینه DOM

HTML وب را تقویت می‌کند چون کار کردن با آن آسان است. با اعلام چند برچسب، می‌توانید در چند ثانیه صفحه‌ای بنویسید که هم ارائه و هم ساختار داشته باشد. بااین‌حال، HTML به‌تنهایی چندان مفید نیست. درک زبان مبتنی بر نوشتار برای انسان‌ها آسان است، اما ماشین‌ها به چیز بیشتری نیاز دارند. مدل شیء سند (DOM) را وارد کنید.

وقتی مرورگر صفحه وبی را بار می‌کند، کارهای جالب زیادی انجام می‌دهد. یکی از کارهایی که انجام می‌دهد تبدیل HTML نویسنده به سند زنده است. اساساً، برای درک ساختار صفحه، مرورگر HTML (رشته‌های نوشتاری ایستا) را به مدل داده (اشیاء/گره‌ها) تجزیه می‌کند. مرورگر با ایجاد درختی از این گره‌ها، سلسله‌مراتب HTML را حفظ می‌کند: DOM. نکته جالب درباره DOM این است که نمایش زنده صفحه شما است. برخلاف HTML ثابتی که ما می‌نویسیم، گره‌های تولیدشده توسط مرورگر حاوی ویژگی‌ها، روش‌ها، و از همه بهتر… قابل دستکاری توسط برنامه‌ها هستند! به‌همین دلیل است که می‌توانیم عناصر DOM را مستقیماً بااستفاده از جاوا اسکریپت ایجاد کنیم:

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>

همه این‌ها خوب و عالی است. پس DOM سایه دیگر چیست؟

DOM… in the shadows

Shadow DOM همان DOM معمولی است با دو تفاوت: ۱) نحوه ایجاد/استفاده از آن و ۲) نحوه رفتار آن در ارتباط با بقیه صفحه. معمولاً گره‌های DOM را ایجاد می‌کنید و آن‌ها را به‌عنوان فرزندان عنصر دیگری اضافه می‌کنید. با DOM سایه، درخت DOM با دامنه‌ای ایجاد می‌کنید که به عنصر متصل است، اما از فرزندان واقعی آن جدا است. این زیردرخت محدودشده درخت سایه نامیده می‌شود. عنصری که به آن متصل است میزبان سایه آن است. هرچیزی که در سایه‌ها اضافه می‌کنید، ازجمله <style>، برای عنصر میزبان محلی می‌شود. این روشی است که DOM سایه محدوده سبک CSS را به‌دست می‌آورد.

درحال ایجاد DOM سایه

ریشه سایه قطعه‌ای از سند است که به عنصر «میزبان» پیوست می‌شود. عمل پیوست کردن ریشه سایه روشی است که عنصر ازطریق آن DOM سایه خود را به‌دست می‌آورد. برای ایجاد 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 نیز استفاده کنید. این وب است. ما حق انتخاب داریم.

مشخصات فهرستی از عناصر را تعریف می‌کند که نمی‌توانند میزبان درخت سایه باشند. به چند دلیل ممکن است عنصری در فهرست باشد:

  • مرورگر ازقبل میزبان DOM سایه داخلی خودش برای عنصر است (<textarea>، <input>).
  • منطقی نیست که عنصر میزبان DOM سایه (<img>) باشد.

برای مثال، این مورد کار نمی‌کند:

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

درحال ایجاد 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 خارج از 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 مسطح

نتیجه توزیع DOM سبک کاربر توسط مرورگر در DOM سایه شما، که محصول نهایی را ارائه می‌دهد. درخت مسطح‌شده چیزی است که درنهایت در DevTools می‌بینید و چیزی است که در صفحه رندر می‌شود.

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

عنصر <slot>

‫Shadow DOM بااستفاده از عنصر <slot>، درخت‌های DOM مختلف را باهم ترکیب می‌کند. جایگاه‌ها، جای‌بان‌هایی در داخل مؤلفه شما هستند که کاربران می‌توانند آن‌ها را با نشان‌گذاری خودشان پر کنند. با تعریف کردن یک یا چند جایگاه، نشان‌گذاری خارجی را برای ارائه شدن در DOM سایه مؤلفه‌تان دعوت می‌کنید. درواقع، شما می‌گویید «نشانه‌گذاری کاربر را در اینجا ارائه کن».

عناصر می‌توانند از مرز DOM سایه «عبور کنند» وقتی که <slot> آن‌ها را دعوت می‌کند. به این عناصر گره‌های توزیع‌شده می‌گویند. ازنظر مفهومی، گره‌های توزیع‌شده ممکن است کمی عجیب به‌نظر برسند. جایگاه‌ها DOM را به‌طور فیزیکی جابه‌جا نمی‌کنند؛ آن را در مکان دیگری در DOM سایه ارائه می‌کنند.

یک مؤلفه می‌تواند صفر یا چند جایگاه در 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 سایه شما هستند که کاربران با نام به آن‌ها ارجاع می‌دهند.

مثال - جایگاه‌ها در 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) برای کاربران فراهم کند تا پیش‌فرض‌ها را لغو کنند.

سبک‌های تعریف‌شده توسط مؤلفه

بدون شک مفیدترین ویژگی DOM سایه CSS با دامنه محدود است:

  • گزینشگرهای CSS از صفحه بیرونی در داخل مؤلفه شما اعمال نمی‌شوند.
  • سبک‌های تعریف‌شده در داخل به بیرون سرایت نمی‌کنند. آن‌ها به عنصر میزبان محدود شده‌اند.

گزینشگرهای CSS استفاده‌شده در 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>

تاکنون فکر کرده‌اید که وقتی مشخصه multiple را اضافه می‌کنید، عنصر <select> چگونه ابزاره چندانتخابی (به‌جای منو کرکره‌ای) را ارائه می‌کند:

<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 فقط در زمینه ریشه سایه کار می‌کند، بنابراین نمی‌توانید از آن خارج از 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>

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 آن‌ها را ارائه می‌کند اما گره‌ها به‌طور فیزیکی در جای خود باقی می‌مانند. سبک‌هایی که قبل‌از توزیع اعمال شده‌اند، بعداز توزیع نیز اعمال می‌شوند. بااین‌حال، وقتی DOM سبک توزیع می‌شود، می‌تواند سبک‌های بیشتری را (که توسط 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;
}

سبک‌های بیرونی همیشه بر سبک‌های تعریف‌شده در 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>

درون DOM سایه آن:

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

در این حالت، مؤلفه از black به‌عنوان مقدار پس‌زمینه استفاده می‌کند زیرا کاربر آن را ارائه کرده است. درغیراین‌صورت، به‌طور پیش‌فرض #9E9E9E خواهد بود.

موضوعات پیشرفته

ایجاد ریشه‌های سایه بسته (باید اجتناب شود)

نوع دیگری از DOM سایه وجود دارد که به آن حالت «بسته» می‌گویند. وقتی درخت سایه بسته ایجاد می‌کنید، جاوا اسکریپت خارجی نمی‌تواند به DOM داخلی مؤلفه شما دسترسی پیدا کند. این شبیه به نحوه عملکرد عناصر بومی مانند <video> است. جاوااسکریپت نمی‌تواند به 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. حالت بسته باعث می‌شود مؤلفه شما برای کاربران نهایی انعطاف‌پذیری کمتری داشته باشد. As you build web components, there will come a time when you forget to add a feature. گزینه پیکربندی. مورد استفاده‌ای که کاربر می‌خواهد. یک نمونه رایج، فراموش کردن گنجاندن قلاب‌های سبک‌بندی کافی برای گره‌های داخلی است. با حالت بسته، هیچ راهی برای کاربران وجود ندارد که پیش‌فرض‌ها را لغو کنند و سبک‌ها را تغییر دهند. امکان دسترسی به بخش‌های داخلی مؤلفه بسیار مفید است. درنهایت، اگر مؤلفه شما کاری را که کاربران می‌خواهند انجام ندهد، آن‌ها مؤلفه شما را منشعب می‌کنند، مؤلفه دیگری پیدا می‌کنند، یا مؤلفه خودشان را ایجاد می‌کنند :(

کار کردن با جایگاه‌ها در JS

‫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} همچنین محتوای جایگزین جایگاه را برمی‌گرداند (اگر هیچ گرهی توزیع نشود).

به‌عنوان مثال، فرض کنید 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

وقتی رویدادی از DOM سایه حباب می‌شود، هدف آن تنظیم می‌شود تا کپسوله‌سازی ارائه‌شده توسط DOM سایه حفظ شود. یعنی رویدادها دوباره هدف‌یابی می‌شوند تا به‌نظر برسد که از مؤلفه آمده‌اند، نه از عناصر داخلی درون 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>

مدیریت فوکوس

اگر مدل رویداد DOM سایه را به‌خاطر داشته باشید، رویدادهایی که در داخل 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.

اگر چندین سطح از DOM سایه در بازی وجود دارد (مثلاً یک عنصر سفارشی در عنصر سفارشی دیگر)، باید به‌صورت بازگشتی به ریشه‌های سایه بروید تا activeElement را پیدا کنید:

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

گزینه دیگری برای تمرکز، گزینه delegatesFocus: true است که رفتار تمرکز عنصر را در درخت سایه گسترش می‌دهد:

  • اگر روی گره‌ای در DOM سایه کلیک کنید و گره ناحیه کانونی‌شدنی نباشد، اولین ناحیه کانونی‌شدنی کانونی می‌شود.
  • وقتی گره‌ای در DOM سایه تمرکز را به‌دست می‌آورد، :focus علاوه‌بر عنصر متمرکزشده، روی میزبان نیز اعمال می‌شود.

مثال - نحوه تغییر رفتار تمرکز توسط delegatesFocus: true

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

<x-focus></x-focus>

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

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

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

نتیجه

delegatesFocus: true behavior.

Above is the result when <x-focus> is focused (user click, tabbed into, focus(), etc.), "Clickable Shadow DOM text" is clicked, or the internal <input> is focused (including autofocus).

اگر delegatesFocus: false را تنظیم کنید، آنچه درعوض خواهید دید به این صورت است:

delegatesFocus: false و ورودی داخلی متمرکز است.
delegatesFocus: false و <input> داخلی متمرکز است.
delegatesFocus: false and x-focus
    gains focus (e.g. it has tabindex='0').
delegatesFocus: false و <x-focus> فوکوس را به‌دست می‌آورد (مثلاً tabindex="0" دارد).
delegatesFocus: false and 'Clickable Shadow DOM text' is
    clicked (or other empty area within the element's shadow DOM is clicked).
delegatesFocus: false و «نوشتار DOM سایه قابل‌کلیک» کلیک می‌شود (یا ناحیه خالی دیگری در DOM سایه عنصر کلیک می‌شود).

نکات و ترفندها

درطول سال‌ها، چیزهایی درباره نوشتن مؤلفه‌های وب یاد گرفته‌ام. فکر می‌کنم برخی‌از این نکات برای نوشتن مؤلفه‌ها و اشکال‌زدایی DOM سایه مفید باشد.

استفاده از CSS containment

معمولاً چیدمان/سبک/رنگ مؤلفه وب نسبتاً مستقل است. برای بهبود عملکرد، از محدودسازی CSS در :host استفاده کنید:

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

درحال بازنشانی سبک‌های قابل‌وراثت

سبک‌های قابل‌وراثت (background،‏ color،‏ font،‏ line-height، و غیره) همچنان در 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>

یافتن همه عناصر سفارشی استفاده‌شده توسط صفحه

گاهی‌اوقات پیدا کردن عناصر سفارشی استفاده‌شده در صفحه مفید است. برای انجام این کار، باید 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 مدتی است که نسخه قدیمی‌تری از DOM سایه را ارائه می‌دهند. Blink برای مدتی به پشتیبانی از هر دو نسخه به‌طور موازی ادامه خواهد داد. مشخصات v0 روش متفاوتی برای ایجاد ریشه سایه ارائه می‌دهد (element.createShadowRoot به‌جای element.attachShadow در v1). فراخوانی روش قدیمی‌تر همچنان ریشه سایه را با معناشناسی v0 ایجاد می‌کند، بنابراین کد v0 موجود خراب نمی‌شود.

اگر به مشخصات قدیمی v0 علاقه دارید، مقاله‌های html5rocks را بررسی کنید: ۱، ۲، ۳. همچنین مقایسه خوبی از تفاوت‌های بین shadow DOM v0 و v1 وجود دارد.

پشتیبانی مرورگر

Shadow DOM v1 در Chrome 53 (وضعیت)، Opera 40،‏ Safari 10، و Firefox 63 ارائه شده است. Edge توسعه را شروع کرده است.

برای تشخیص ویژگی shadow DOM، وجود attachShadow را بررسی کنید:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

شبیه‌ساز مرورگر

تا زمانی که پشتیبانی مرورگر به‌طور گسترده دردسترس قرار گیرد، شبیه‌سازهای مرورگر shadydom و shadycss ویژگی نسخه ۱ را به شما ارائه می‌دهند. Shady DOM از محدوده DOM مربوط به Shadow DOM و شبیه‌سازهای مرورگر shadycss تقلید می‌کند ویژگی‌های سفارشی CSS و محدوده سبک که میانای برنامه‌سازی کاربردی بومی ارائه می‌دهد.

شبیه‌سازهای مرورگر را نصب کنید:

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!
}

برای دستورالعمل‌های مربوط به نحوه shim/scope کردن سبک‌هایتان، https://github.com/webcomponents/shadycss#usage را ببینید.

نتیجه‌گیری

برای اولین‌بار، یک API اولیه داریم که CSS scoping و DOM scoping مناسبی دارد و ترکیب‌بندی واقعی دارد. در ترکیب با دیگر میاناهای برنامه‌سازی کاربردی مؤلفه وب مانند عناصر سفارشی، DOM سایه راهی برای ایجاد مؤلفه‌های واقعاً کپسوله‌شده بدون هک یا استفاده از موارد قدیمی‌تر مانند <iframe> ارائه می‌دهد.

اشتباه برداشت نکنید. Shadow DOM واقعاً چیز پیچیده‌ای است! اما هیولایی است که ارزش یادگیری دارد. کمی وقت با آن بگذرانید. آن را یاد بگیرید و سؤال بپرسید!

مطالعه بیشتر

پرسشگان

آیا می‌توانم امروز از Shadow DOM v1 استفاده کنم؟

با شبیه‌ساز مرورگر، بله. پشتیبانی مرورگر را ببینید.

DOM سایه چه ویژگی‌های امنیتی‌ای ارائه می‌دهد؟

Shadow DOM ویژگی امنیتی نیست. این ابزاری سبک برای CSS با دامنه محدود و پنهان کردن درخت‌های DOM در مؤلفه است. اگر می‌خواهید مرز امنیتی واقعی داشته باشید، از <iframe> استفاده کنید.

آیا عنصر وب باید از Shadow DOM استفاده کند؟

نه! لازم نیست مؤلفه‌های وبی ایجاد کنید که از DOM سایه استفاده می‌کنند. بااین‌حال، نوشتن عناصر سفارشی که از Shadow DOM استفاده می‌کنند به این معنی است که می‌توانید از ویژگی‌هایی مانند محدوده CSS، کپسوله‌سازی DOM، و ترکیب‌بندی بهره ببرید.

تفاوت بین ریشه‌های سایه باز و بسته چیست؟

ریشه‌های سایه بسته را ببینید.