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 جدید (با یک API JS) هستند، سایه DOM راهی است که شما HTML و CSS آن را ارائه می دهید. این دو API با هم ترکیب می‌شوند تا یک جزء با HTML، CSS و جاوا اسکریپت مستقل بسازند.

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

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

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

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

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

Shadow DOM چیست؟

پس زمینه در DOM

HTML وب را قدرتمند می کند زیرا کار با آن آسان است. با اعلام چند تگ، می توانید صفحه ای را در عرض چند ثانیه بنویسید که هم نمایش و هم ساختار دارد. با این حال، HTML به خودی خود آنقدرها هم مفید نیست. درک یک زبان مبتنی بر متن برای انسان آسان است، اما ماشین ها به چیز بیشتری نیاز دارند. Document Object Model یا 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>

همه چیز خوب و خوب است. سپس shadow DOM چیست ؟

DOM… در سایه

Shadow DOM یک DOM معمولی با دو تفاوت است: 1) نحوه ایجاد/استفاده از آن و 2) نحوه رفتار آن در رابطه با بقیه صفحه. به طور معمول، شما گره های DOM را ایجاد می کنید و آنها را به عنوان فرزندان یک عنصر دیگر اضافه می کنید. با Shadow DOM، شما یک درخت DOM با دامنه ایجاد می کنید که به عنصر متصل است، اما از فرزندان واقعی آن جدا است. به این زیردرخت محدوده دار، درخت سایه می گویند. عنصری که به آن متصل است میزبان سایه آن است. هر چیزی که در سایه ها اضافه کنید به عنصر میزبانی محلی تبدیل می شود، از جمله <style> . به این ترتیب shadow 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 برای پر کردن ریشه سایه استفاده می کنم، اما می توانید از سایر API های DOM نیز استفاده کنید. این وب است. ما انتخاب داریم

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

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

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

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

ایجاد سایه DOM برای یک عنصر سفارشی

Shadow DOM به ویژه هنگام ایجاد عناصر سفارشی مفید است. از سایه DOM برای تقسیم بندی HTML، CSS و JS یک عنصر استفاده کنید، بنابراین یک "کامپوننت وب" تولید می شود.

مثال - یک عنصر سفارشی سایه 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>
    `;
    }
    ...
});

یکی دو چیز جالب اینجا در جریان است. اولین مورد این است که وقتی یک نمونه از <fancy-tabs> ایجاد می شود، عنصر سفارشی DOM سایه خود را ایجاد می کند . این کار در constructor() انجام می شود. ثانیاً، چون ما یک ریشه سایه ایجاد می‌کنیم، قوانین CSS داخل <style> به <fancy-tabs> تبدیل می‌شوند.

ترکیب و اسلات

ترکیب بندی یکی از کم درک ترین ویژگی های Shadow DOM است، اما مسلماً مهمترین آن است.

در دنیای توسعه وب ما، ترکیب به این صورت است که چگونه برنامه‌ها را به‌طور آشکار از HTML می‌سازیم. بلوک های ساختمانی مختلف ( <div><header><form><input> s) با هم جمع می شوند تا برنامه ها را تشکیل دهند. برخی از این برچسب ها حتی با یکدیگر کار می کنند. ترکیب بندی به همین دلیل است که عناصر بومی مانند <select> ، <details> ، <form> و <video> بسیار انعطاف پذیر هستند. هر یک از آن تگ ها HTML خاصی را در کودکی می پذیرند و کار خاصی را با آنها انجام می دهند. برای مثال، <select> می داند که چگونه <option> و <optgroup> را در ویجت های کشویی و چند انتخابی رندر کند. عنصر <details> <summary> را به عنوان یک فلش قابل گسترش نمایش می دهد. حتی <video> می داند که چگونه با کودکان خاصی رفتار کند: عناصر <source> رندر نمی شوند، اما بر رفتار ویدیو تأثیر می گذارند. چه جادویی!

اصطلاحات: DOM روشن در مقابل DOM سایه

ترکیب Shadow DOM مجموعه ای از اصول جدید در توسعه وب را معرفی می کند. قبل از پرداختن به علف‌های هرز، بیایید برخی اصطلاحات را استاندارد کنیم تا به همان زبان صحبت کنیم.

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>

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

هنگامی که یک <slot> آنها را به داخل دعوت می کند، اجازه داده می شود که از مرز DOM سایه عبور کنند. این عناصر گره های توزیع شده نامیده می شوند. از نظر مفهومی، گره های توزیع شده می توانند کمی عجیب به نظر برسند. اسلات ها DOM را به صورت فیزیکی حرکت نمی دهند. آنها آن را در مکان دیگری در داخل سایه DOM ارائه می کنند.

یک جزء می تواند صفر یا چند اسلات را در DOM سایه خود تعریف کند. اسلات ها می توانند خالی باشند یا محتوای بازگشتی ارائه کنند. اگر کاربر محتوای 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 ) برای کاربران فراهم کند تا پیش‌فرض‌ها را لغو کنند.

سبک های تعریف شده از مؤلفه ها

مفیدترین ویژگی Shadow DOM CSS است:

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

انتخابگرهای CSS مورد استفاده در سایه DOM به صورت محلی برای مؤلفه شما اعمال می شوند . در عمل، این بدان معناست که می‌توانیم دوباره از نام‌های id/class رایج استفاده کنیم، بدون اینکه نگران تداخل در جای دیگری از صفحه باشیم. انتخابگرهای ساده 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>

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

سایر APIها نیز تحت تأثیر حالت بسته قرار می گیرند:

  • Element.assignedSlot / TextNode.assignedSlot null را برمی‌گرداند
  • Event.composedPath() برای رویدادهای مرتبط با عناصر داخل سایه DOM، []

در اینجا خلاصه من از این است که چرا هرگز نباید اجزای وب را با {mode: 'closed'} ایجاد کنید:

  1. احساس امنیت مصنوعی هیچ چیزی مانع از ربودن Element.prototype.attachShadow توسط مهاجم نمی شود.

  2. حالت بسته مانع از دسترسی کد عنصر سفارشی شما به 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. حالت بسته باعث می شود جزء شما برای کاربران نهایی انعطاف پذیری کمتری داشته باشد . همانطور که اجزای وب را می سازید، زمانی فرا می رسد که فراموش می کنید یک ویژگی را اضافه کنید. یک گزینه پیکربندی موردی که کاربر می خواهد. یک مثال متداول، فراموش کردن گنجاندن قلاب های سبک مناسب برای گره های داخلی است. با حالت بسته، هیچ راهی برای کاربران وجود ندارد که پیش‌فرض‌ها را نادیده بگیرند و سبک‌ها را تغییر دهند. دسترسی به اجزای داخلی بسیار مفید است. در نهایت، کاربران کامپوننت شما را فورک می‌کنند، یکی دیگر را پیدا می‌کنند یا اگر آن چیزی را که می‌خواهند انجام ندهد، کامپوننت خود را ایجاد می‌کنند:(

کار با اسلات در JS

Shadow DOM API ابزارهایی را برای کار با اسلات ها و گره های توزیع شده فراهم می کند. این موارد هنگام نوشتن یک عنصر سفارشی مفید هستند.

رویداد تغییر شکاف

رویداد 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>متن جزء</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>

رسیدگی به تمرکز

اگر از مدل رویداد shadow 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.

اگر چندین سطح از shadow 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: رفتار واقعی.

هنگامی که <x-focus> فوکوس شده است (کلیک کاربر، تب، focus() و غیره)، روی "متن Shadow DOM قابل کلیک" کلیک می شود، یا <input> داخلی فوکوس می شود (از جمله autofocus ) نتیجه را نشان می دهد.

اگر بخواهید delegatesFocus: false را تنظیم کنید، به جای آن چیزی را مشاهده می کنید:

delegatesFocus: غلط است و ورودی داخلی متمرکز است.
delegatesFocus: false و <input> داخلی متمرکز است.
delegatesFocus: false و x-focus فوکوس را به دست می آورند (مثلاً tabindex='0' دارد).
delegatesFocus: false و <x-focus> فوکوس را به دست می آورد (به عنوان مثال دارای tabindex="0" ).
delegatesFocus: false و روی «متن Shadow DOM قابل کلیک» کلیک می شود (یا قسمت خالی دیگری در DOM سایه عنصر کلیک می شود).
delegatesFocus: false و "متن Shadow DOM قابل کلیک" کلیک می شود (یا قسمت خالی دیگری در سایه DOM عنصر کلیک می شود).

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

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

از محتویات CSS استفاده کنید

به طور معمول، طرح/سبک/نقاشی یک جزء وب نسبتاً مستقل است. از محتویات CSS در :host برای یک برد perf استفاده کنید:

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

بازنشانی سبک های ارثی

سبک های قابل ارث بری ( background ، color ، font ، line-height ، و غیره) همچنان در سایه DOM به ارث می رسند. یعنی مرز سایه 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 برای مدتی نسخه قدیمی‌تری از shadow DOM را ارسال کرده است. Blink تا مدتی به پشتیبانی از هر دو نسخه به صورت موازی ادامه خواهد داد. مشخصات v0 روش متفاوتی را برای ایجاد یک ریشه سایه ارائه کرد ( element.createShadowRoot به جای element.attachShadow v1.attachShadow). فراخوانی روش قدیمی برای ایجاد ریشه سایه با معنای v0 ادامه می یابد، بنابراین کد v0 موجود خراب نمی شود.

اگر به مشخصات v0 قدیمی علاقه دارید، مقالات html5rocks را بررسی کنید: 1 ، 2 ، 3 . همچنین مقایسه بسیار خوبی از تفاوت‌های 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 ویژگی v1 را به شما می‌دهند. Shady DOM محدوده DOM Shadow DOM را تقلید می‌کند و shadycss ویژگی‌های سفارشی CSS و محدوده سبکی را که API بومی ارائه می‌دهد، 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 مراجعه کنید.

نتیجه گیری

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

اشتباه نکنید Shadow DOM مطمئناً یک جانور پیچیده است! اما این جانوری است که ارزش یادگیری را دارد. مدتی را با آن بگذرانید. یاد بگیرید و سوال بپرسید!

در ادامه مطلب

سوالات متداول

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

با پلی پر، بله. به پشتیبانی مرورگر مراجعه کنید.

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

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

آیا یک کامپوننت وب باید از سایه DOM استفاده کند؟

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

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

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