Shadow DOM اعلامی

Declarative Shadow DOM یک ویژگی استاندارد پلتفرم وب است که از نسخه 90 در کروم پشتیبانی می‌شود. توجه داشته باشید که مشخصات این ویژگی در سال 2023 تغییر کرد (از جمله تغییر نام shadowroot به shadowrootmode ) و به‌روزترین نسخه‌های استاندارد شده از همه. بخش هایی از این ویژگی در کروم نسخه 124 فرود آمد.

Browser Support

  • کروم: 111.
  • لبه: 111.
  • فایرفاکس: 123.
  • سافاری: 16.4.

Source

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

تا به حال، تنها راه استفاده از Shadow DOM ساختن ریشه سایه با استفاده از جاوا اسکریپت بود:

const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';

یک API ضروری مانند این برای رندر سمت کلاینت خوب کار می کند: همان ماژول های جاوا اسکریپت که عناصر سفارشی ما را تعریف می کنند، Shadow Roots خود را نیز ایجاد می کنند و محتوای آنها را تنظیم می کنند. با این حال، بسیاری از برنامه های کاربردی وب نیاز به ارائه محتوا در سمت سرور یا HTML ایستا در زمان ساخت دارند. این می تواند بخش مهمی از ارائه یک تجربه معقول به بازدیدکنندگانی باشد که ممکن است قادر به اجرای جاوا اسکریپت نباشند.

توجیهات رندر سمت سرور (SSR) از پروژه ای به پروژه دیگر متفاوت است. برخی از وب‌سایت‌ها باید HTML ارائه‌شده توسط سرور کاملاً کاربردی را برای رعایت دستورالعمل‌های دسترسی ارائه کنند، برخی دیگر ارائه یک تجربه پایه بدون جاوا اسکریپت را به عنوان راهی برای اطمینان از عملکرد خوب در اتصالات یا دستگاه‌های کند انتخاب می‌کنند.

از لحاظ تاریخی، استفاده از Shadow DOM در ترکیب با رندر سمت سرور دشوار بوده است، زیرا هیچ راهی داخلی برای بیان Shadow Roots در HTML تولید شده توسط سرور وجود نداشت. همچنین هنگام اتصال Shadow Roots به عناصر DOM که قبلاً بدون آنها رندر شده اند، پیامدهای عملکردی نیز وجود دارد. این می تواند باعث تغییر طرح بعد از بارگیری صفحه شود، یا به طور موقت فلش محتوای بدون استایل ("FOUC") را در حین بارگیری شیوه نامه های Shadow Root نشان دهد.

Shadow DOM اعلامی (DSD) این محدودیت را حذف می کند و Shadow DOM را به سرور می آورد.

چگونه یک ریشه سایه اعلامی بسازیم

ریشه Shadow Declarative یک عنصر <template> با ویژگی shadowrootmode است:

<host-element>
  <template shadowrootmode="open">
    <slot></slot>
  </template>
  <h2>Light content</h2>
</host-element>

یک عنصر الگو با ویژگی shadowrootmode توسط تجزیه کننده HTML شناسایی می شود و بلافاصله به عنوان ریشه سایه عنصر والد آن اعمال می شود. بارگیری نشانه گذاری HTML خالص از نمونه بالا منجر به درخت DOM زیر می شود:

<host-element>
  #shadow-root (open)
  <slot>
    ↳
    <h2>Light content</h2>
  </slot>
</host-element>

این نمونه کد از قراردادهای پانل عناصر ابزار توسعه کروم برای نمایش محتوای Shadow DOM پیروی می کند. برای مثال، کاراکتر محتوای Light DOM شیاردار را نشان می‌دهد.

این به ما مزایای کپسوله سازی Shadow DOM و نمایش اسلات در HTML ایستا را می دهد. برای تولید کل درخت، از جمله Shadow Root، نیازی به جاوا اسکریپت نیست.

هیدراتاسیون اجزا

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

یک عنصر سفارشی در حال ارتقاء از HTML که شامل یک ریشه سایه اعلامی است، قبلاً آن ریشه سایه را متصل خواهد کرد. این بدان معناست که این عنصر در زمان نمونه‌سازی، ویژگی shadowRoot را از قبل در دسترس خواهد داشت، بدون اینکه کد شما به صراحت یکی را ایجاد کند. بهتر است this.shadowRoot برای هر ریشه سایه موجود در سازنده عنصر خود بررسی کنید. اگر از قبل یک مقدار وجود داشته باشد، HTML برای این مؤلفه شامل یک ریشه سایه اعلامی است. اگر مقدار تهی باشد، ریشه Shadow Declarative در HTML وجود ندارد یا مرورگر از Declarative Shadow DOM پشتیبانی نمی کند.

<menu-toggle>
  <template shadowrootmode="open">
    <button>
      <slot></slot>
    </button>
  </template>
  Open Menu
</menu-toggle>
<script>
  class MenuToggle extends HTMLElement {
    constructor() {
      super();

      // Detect whether we have SSR content already:
      if (this.shadowRoot) {
        // A Declarative Shadow Root exists!
        // wire up event listeners, references, etc.:
        const button = this.shadowRoot.firstElementChild;
        button.addEventListener('click', toggle);
      } else {
        // A Declarative Shadow Root doesn't exist.
        // Create a new shadow root and populate it:
        const shadow = this.attachShadow({mode: 'open'});
        shadow.innerHTML = `<button><slot></slot></button>`;
        shadow.firstChild.addEventListener('click', toggle);
      }
    }
  }

  customElements.define('menu-toggle', MenuToggle);
</script>

المان‌های سفارشی مدتی است که وجود داشته‌اند، و تا به حال هیچ دلیلی برای بررسی ریشه سایه‌های موجود قبل از ایجاد آن با استفاده attachShadow() وجود نداشت. Declarative Shadow DOM شامل یک تغییر کوچک است که به اجزای موجود اجازه می دهد با وجود این کار کنند: فراخوانی متد attachShadow() روی یک عنصر با ریشه Shadow Declarative موجود خطایی ایجاد نمی کند. در عوض، ریشه Shadow Declarative خالی شده و برگردانده می شود. این به اجزای قدیمی‌تر که برای Declarative Shadow DOM ساخته نشده‌اند اجازه می‌دهد به کار خود ادامه دهند، زیرا ریشه‌های اعلانی تا زمانی که یک جایگزین ضروری ایجاد شود حفظ می‌شوند.

برای عناصر سفارشی تازه ایجاد شده، یک ویژگی ElementInternals.shadowRoot جدید راهی صریح برای دریافت ارجاع به ریشه سایه اعلامی موجود عنصر، هم باز و هم بسته ارائه می دهد. این می تواند برای بررسی و استفاده از هر ریشه Shadow Declarative استفاده شود، در حالی که در مواردی که یکی از آنها ارائه نشده است، همچنان به attachShadow() باز می گردد.

class MenuToggle extends HTMLElement {
  constructor() {
    super();

    const internals = this.attachInternals();

    // check for a Declarative Shadow Root:
    let shadow = internals.shadowRoot;

    if (!shadow) {
      // there wasn't one. create a new Shadow Root:
      shadow = this.attachShadow({
        mode: 'open'
      });
      shadow.innerHTML = `<button><slot></slot></button>`;
    }

    // in either case, wire up our event listener:
    shadow.firstChild.addEventListener('click', toggle);
  }
}

customElements.define('menu-toggle', MenuToggle);

یک سایه در هر ریشه

یک ریشه Shadow Declarative فقط با عنصر والد خود مرتبط است. این به این معنی است که ریشه های سایه همیشه با عنصر مرتبط خود قرار می گیرند. این تصمیم طراحی تضمین می کند که ریشه های سایه مانند بقیه یک سند HTML قابل جریان هستند. همچنین برای نوشتن و تولید راحت است، زیرا افزودن ریشه سایه به یک عنصر نیازی به نگهداری رجیستری از ریشه های سایه موجود ندارد.

معاوضه مرتبط کردن ریشه های سایه با عنصر والد آنها این است که نمی توان چندین عنصر را از یک ریشه Shadow Declarative <template> مقداردهی اولیه کرد. با این حال، در بیشتر مواردی که از Shadow DOM اعلامی استفاده می شود، بعید به نظر می رسد، زیرا محتویات هر ریشه سایه به ندرت یکسان است. در حالی که HTML ارائه شده توسط سرور اغلب شامل ساختارهای عناصر مکرر است، محتوای آنها معمولاً متفاوت است - به عنوان مثال، تغییرات جزئی در متن یا ویژگی ها. از آنجایی که محتویات یک ریشه Shadow Declarative سریال کاملا ثابت است، ارتقاء چندین عنصر از یک ریشه Shadow Declarative تنها در صورتی کار می کند که عناصر یکسان باشند. در نهایت، تأثیر ریشه‌های سایه مشابه تکراری بر اندازه انتقال شبکه به دلیل اثرات فشرده‌سازی نسبتاً کم است.

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

استریم باحاله

مرتبط کردن ریشه های سایه اظهاری به طور مستقیم با عنصر مادر، فرآیند ارتقا و اتصال آنها به آن عنصر را ساده می کند. ریشه‌های سایه‌ای اعلامی در طول تجزیه HTML شناسایی می‌شوند و بلافاصله زمانی که با تگ <template> آغازین آنها مواجه می‌شوند، به آن‌ها متصل می‌شوند. HTML تجزیه شده در <template> مستقیماً در ریشه سایه تجزیه می شود، بنابراین می توان آن را "استریم" کرد: همانطور که دریافت می شود رندر می شود.

<div id="el">
  <script>
    el.shadowRoot; // null
  </script>

  <template shadowrootmode="open">
    <!-- shadow realm -->
  </template>

  <script>
    el.shadowRoot; // ShadowRoot
  </script>
</div>

فقط تجزیه کننده

Declarative Shadow DOM یکی از ویژگی های تجزیه کننده HTML است. این بدان معناست که یک ریشه Shadow Declarative فقط برای تگ های <template> با ویژگی shadowrootmode که در طول تجزیه HTML وجود دارند، تجزیه و پیوست می شود. به عبارت دیگر، ریشه های سایه اظهاری را می توان در طول تجزیه اولیه HTML ساخت:

<some-element>
  <template shadowrootmode="open">
    shadow root content for some-element
  </template>
</some-element>

تنظیم ویژگی shadowrootmode یک عنصر <template> هیچ کاری نمی کند، و الگو یک عنصر قالب معمولی باقی می ماند:

const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null

برای جلوگیری از برخی ملاحظات مهم امنیتی، ریشه های سایه اعلامی نیز نمی توانند با استفاده از APIهای تجزیه قطعه مانند innerHTML یا insertAdjacentHTML() ایجاد شوند. تنها راه برای تجزیه HTML با استفاده از ریشه های سایه اعلامی استفاده از setHTMLUnsafe() یا parseHTMLUnsafe() است:

<script>
  const html = `
    <div>
      <template shadowrootmode="open"></template>
    </div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; // No shadow root here
  div.setHTMLUnsafe(html); // Shadow roots included
  const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>

رندر سرور با سبک

استایل شیت های درون خطی و خارجی با استفاده از تگ های استاندارد <style> و <link> به طور کامل در داخل Declarative Shadow Roots پشتیبانی می شوند:

<nineties-button>
  <template shadowrootmode="open">
    <style>
      button {
        color: seagreen;
      }
    </style>
    <link rel="stylesheet" href="/comicsans.css" />
    <button>
      <slot></slot>
    </button>
  </template>
  I'm Blue
</nineties-button>

سبک‌های مشخص‌شده به این روش نیز بسیار بهینه‌سازی شده‌اند: اگر همان شیوه نامه در چندین ریشه سایه اعلامی وجود داشته باشد، فقط یک بار بارگیری و تجزیه می‌شود. مرورگر از یک پشتیبان CSSStyleSheet استفاده می کند که توسط همه ریشه های سایه به اشتراک گذاشته شده است و سربار حافظه تکراری را حذف می کند.

Stylesheet های ساختنی در Declarative Shadow DOM پشتیبانی نمی شوند. این به این دلیل است که در حال حاضر، هیچ راهی برای سریال‌سازی شیوه‌نامه‌های قابل ساخت در HTML وجود ندارد، و هیچ راهی برای مراجعه به آن‌ها هنگام پر کردن adoptedStyleSheets وجود ندارد.

چگونه از فلش محتوای بدون استایل جلوگیری کنیم؟

یکی از مشکلات احتمالی در مرورگرهایی که هنوز از DOM Shadow Declarative پشتیبانی نمی کنند، اجتناب از "فلش محتوای بدون سبک" (FOUC) است، که در آن محتوای خام برای عناصر سفارشی که هنوز ارتقاء نیافته اند نشان داده می شود. قبل از DOM Shadow Declarative، یکی از روش‌های رایج برای اجتناب از FOUC، اعمال قاعده سبک display:none برای عناصر سفارشی بود که هنوز بارگذاری نشده‌اند، زیرا ریشه سایه‌شان متصل و پر نشده است. به این ترتیب محتوا تا زمانی که "آماده" نباشد نمایش داده نمی شود:

<style>
  x-foo:not(:defined) > * {
    display: none;
  }
</style>

با معرفی Declarative Shadow DOM، عناصر سفارشی را می توان در HTML رندر یا تألیف کرد، به طوری که محتوای سایه آنها قبل از بارگیری مؤلفه سمت سرویس گیرنده در جای خود و آماده باشد:

<x-foo>
  <template shadowrootmode="open">
    <style>h2 { color: blue; }</style>
    <h2>shadow content</h2>
  </template>
</x-foo>

در این مورد، قانون display:none "FOUC" مانع از نمایش محتوای ریشه سایه اعلامی می شود. با این حال، حذف این قانون باعث می‌شود مرورگرهای بدون پشتیبانی از Declarative Shadow DOM محتوای نادرست یا بدون سبک را نشان دهند تا زمانی که Declarative Shadow DOM polyfill بارگیری شود و الگوی ریشه سایه را به یک ریشه سایه واقعی تبدیل کند.

خوشبختانه، این مشکل در CSS با تغییر قانون سبک FOUC قابل حل است. در مرورگرهایی که از Declarative Shadow DOM پشتیبانی می کنند، عنصر <template shadowrootmode> بلافاصله به ریشه سایه تبدیل می شود و هیچ عنصر <template> در درخت DOM باقی نمی ماند. مرورگرهایی که از Declarative Shadow DOM پشتیبانی نمی کنند، عنصر <template> را حفظ می کنند، که می توانیم از آن برای جلوگیری از FOUC استفاده کنیم:

<style>
  x-foo:not(:defined) > template[shadowrootmode] ~ *  {
    display: none;
  }
</style>

قانون اصلاح شده "FOUC" به جای پنهان کردن عنصر سفارشی هنوز تعریف نشده، فرزندان خود را هنگامی که از عنصر <template shadowrootmode> پیروی می کنند پنهان می کند. هنگامی که عنصر سفارشی تعریف شد، این قانون دیگر مطابقت ندارد. این قانون در مرورگرهایی که از Declarative Shadow DOM پشتیبانی می کنند نادیده گرفته می شود زیرا فرزند <template shadowrootmode> در طول تجزیه HTML حذف می شود.

تشخیص ویژگی و پشتیبانی مرورگر

Declarative Shadow DOM از Chrome 90 و Edge 91 در دسترس بوده است، اما از یک ویژگی غیر استاندارد قدیمی به نام shadowroot به جای ویژگی استاندارد shadowrootmode استفاده می‌کند. ویژگی جدیدتر shadowrootmode و رفتار جریان در Chrome 111 و Edge 111 موجود است.

به عنوان یک API جدید پلتفرم وب، Declarative Shadow DOM هنوز از پشتیبانی گسترده در همه مرورگرها برخوردار نیست. پشتیبانی از مرورگر را می توان با بررسی وجود ویژگی shadowRootMode در نمونه اولیه HTMLTemplateElement شناسایی کرد:

function supportsDeclarativeShadowDOM() {
  return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}

پلی پر

ساختن یک polyfill ساده شده برای Declarative Shadow DOM نسبتاً ساده است، زیرا یک polyfill نیازی به تکرار معنایی زمان‌بندی یا ویژگی‌های فقط تجزیه‌کننده‌ای که پیاده‌سازی مرورگر به آن مربوط می‌شود، ندارد. برای پر کردن دکلراتیو Shadow DOM، می‌توانیم DOM را اسکن کنیم تا همه عناصر <template shadowrootmode> را پیدا کنیم، سپس آنها را به Shadow Root‌های پیوست شده در عنصر والد تبدیل کنیم. این فرآیند را می توان پس از آماده شدن سند انجام داد، یا توسط رویدادهای خاص تری مانند چرخه های عمر عنصر سفارشی راه اندازی شد.

(function attachShadowRoots(root) {
  if (supportsDeclarativeShadowDOM()) {
    // Declarative Shadow DOM is supported, no need to polyfill.
    return;
  }
  root.querySelectorAll("template[shadowrootmode]").forEach(template => {
    const mode = template.getAttribute("shadowrootmode");
    const shadowRoot = template.parentNode.attachShadow({ mode });

    shadowRoot.appendChild(template.content);
    template.remove();
    attachShadowRoots(shadowRoot);
  });
})(document);

در ادامه مطلب