DOM של צל מוצהר

Dlarative Shadow DOM היא תכונה סטנדרטית של פלטפורמת אינטרנט, שנתמכת ב-Chrome מגרסה 90. הערה: המפרט של התכונה הזו השתנה בשנת 2023 (כולל שינוי השם של shadowroot ל-shadowrootmode), והגרסאות הסטנדרטיות העדכניות ביותר של כל חלקי התכונה נחתו בגרסה 124 של Chrome.

תמיכה בדפדפן

  • Chrome: 111.
  • קצה: 111.
  • Firefox: 123.
  • Safari: 16.4.

מקור

Shadow DOM הוא אחד משלושת התקנים של רכיבי אינטרנט, שמעוגלים באמצעות תבניות HTML ו-Custom Elements. התכונה Shadow DOM מאפשרת להגדיר היקף של סגנונות CSS לעץ משנה ספציפי של DOM ולבודד את עץ המשנה הזה משאר המסמך. הרכיב <slot> מאפשר לנו לקבוע היכן יש להוסיף את הצאצאים של רכיב מותאם אישית בתוך עץ הצללה שלו. שילוב של התכונות האלה מאפשר למערכת לבנות רכיבים עצמאיים לשימוש חוזר, שמשתלבים בצורה חלקה באפליקציות קיימות, בדיוק כמו רכיב HTML מובנה.

עד עכשיו, הדרך היחידה להשתמש ב-shadow DOM הייתה ליצור הרמה הבסיסית (root) של הצל באמצעות JavaScript:

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

ממשק API חיוני כמו זה פועל היטב עבור עיבוד בצד הלקוח: אותם מודולים של JavaScript שמגדירים את הרכיבים המותאמים אישית שלנו יוצרים גם את Roots שלהם ומגדירים את התוכן שלהם. עם זאת, אפליקציות אינטרנט רבות צריכות לעבד תוכן בצד השרת או HTML סטטי בזמן ה-build. זה יכול להיות חלק חשוב בהענקת חוויה סבירה למבקרים שייתכן שאינם יודעים להפעיל JavaScript.

ההצדקה לעיבוד בצד השרת (SSR) משתנות מפרויקט לפרויקט. כדי לעמוד בהנחיות הנגישות, אתרים מסוימים חייבים לספק קוד HTML שעבר עיבוד על ידי שרת, שפועל באופן מלא. אתרים אחרים בוחרים לספק חוויית שימוש בסיסית ללא JavaScript כדי להבטיח ביצועים טובים בחיבורים או במכשירים איטיים.

בעבר, היה קשה להשתמש ב-shadow DOM בשילוב עם עיבוד בצד השרת כי לא הייתה דרך מובנית לבטא Roots של Shadow Roots ב-HTML שנוצר על ידי השרת. יש גם השלכות על הביצועים בעת צירוף Shadow Roots לרכיבי DOM שכבר עברו עיבוד בלעדיהם. הדבר עלול לגרום לשינוי הפריסה לאחר טעינת הדף, או להציג באופן זמני הבהוב של תוכן ללא סגנון ('FOUC') בזמן טעינת גיליונות הסגנון של Shadow Root.

Declarative Shadow DOM (DSD) מסיר את המגבלה הזו, ומביאה את Shadow DOM לשרת.

איך לבנות שורש צל הצהרתי

Root Shadow Root הוא רכיב <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>

דוגמת הקוד הזו תואמת למוסכמות של חלונית הרכיבים של כלי הפיתוח ב-Chrome להצגת תוכן של Shadow DOM. לדוגמה, התו מייצג תוכן DOM של אור מחורר.

זה נותן לנו את היתרונות של האנקפסולציה של Shadow DOM והקרנת מקומות ב-HTML סטטי. אין צורך ב-JavaScript כדי להפיק את כל העץ, כולל Root Shadow.

מאזן הנוזלים של הרכיבים

ניתן להשתמש ב-DOM הצהרתי של Shadow DOM בפני עצמו כדרך להסתיר סגנונות או להתאים אישית את מיקום הצאצא, אבל הוא יעיל ביותר כאשר משתמשים בו עם רכיבים מותאמים אישית. רכיבים שנוצרו באמצעות רכיבים מותאמים אישית משודרגים באופן אוטומטי מ-HTML סטטי. עם ההשקה של Delarative Shadow DOM, עכשיו אפשר לרכיב מותאם אישית לקבל Root (Root) לפני השדרוג.

לרכיב מותאם אישית שמשודרג מ-HTML שכולל שורש צל הצהרתי כבר יצורף שורש הצללית הזה. כלומר, לאלמנט יהיה כבר מאפיין shadowRoot זמין כשהוא נוצר בלי שהקוד שלכם יוצר מאפיין כזה באופן מפורש. מומלץ לבדוק את this.shadowRoot אם יש שורש צל קיים ב-constructor של הרכיב. אם כבר קיים ערך, ה-HTML של הרכיב הזה כולל Root Shadow Root. אם הערך הוא null, המשמעות היא שלא קיים שורש של הצללה הצהרתית ב-HTML, או שהדפדפן לא תומך ב-Dlarative 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(). DOM הצהרתי של Shadow DOM כולל שינוי קטן שמאפשר לרכיבים קיימים לפעול למרות זאת: קריאה ל-method attachShadow() באלמנט עם Declarative Shadow Root קיים לא תגרום להצגת שגיאה. במקום זאת, מרוקנים את השורש המוצהר ומחזירים אותו. המצב הזה מאפשר לרכיבים ישנים יותר שלא מותאמים ל-DOM Declarative Shadow DOM להמשיך לפעול, כי השורשים ההצהרתיים נשמרים עד ליצירת החלפה חיונית.

לרכיבים מותאמים אישית שנוצרו לאחרונה, נכס ElementInternals.shadowRoot חדש מספק דרך מפורשת לקבל הפניה לשורש הקיים של הצללית ההצהרתית, גם פתוח וגם סגור. אפשר להשתמש באפשרות הזו כדי לבדוק אם קיים שורש של הצללה הצהרתית ולהשתמש בו, ועדיין לחזור אל attachShadow() במקרים שבהם לא צוין Root.

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);

צל אחד לכל שורש

שורש צל הצהרתי משויך רק לרכיב ההורה שלו. כלומר, שורשי הצללית תמיד ממוקמים ביחס לרכיב המשויך אליהם. החלטת העיצוב הזו מבטיחה שניתן להעביר בסטרימינג את שורשי הצלליות כמו לשאר מסמך HTML. היא גם נוחה ליצירה וליצירה, כי הוספת שורש צל לאלמנט לא מחייבת שמירה על רישום של שורשי הצללית הקיימים.

היתרון של שיוך שורשי צל עם רכיב ההורה שלהם הוא שאין אפשרות לאתחל כמה אלמנטים מאותו Root <template> של הצללית המוצהרת. עם זאת, לא סביר להניח שזה יהיה רלוונטי ברוב המקרים שבהם משתמשים ב-DOM Declarative Shadow DOM, כי התוכן של כל שורש של הצללית זהה לרוב. אמנם HTML שמעובד על ידי שרת מכיל לעתים קרובות מבני רכיבים חוזרים, אך התוכן שלו בדרך כלל שונה. לדוגמה, הבדלים קלים בטקסט או במאפיינים. מכיוון שהתוכן של Root Shadow Root בעלת ההצהרות הקבועות הוא סטטי לחלוטין, שדרוג של מספר רכיבים משורש צל הצהרתי יחיד יפעל רק אם הרכיבים יהיו זהים. לבסוף, ההשפעה של שורשי צל דומים חוזרים על גודל ההעברה ברשת היא קטנה יחסית בגלל השפעות הדחיסה.

בעתיד, ייתכן שתהיה אפשרות לחזור אל שורשי האזורים הכהים ששותפו. אם ה-DOM מקבל תמיכה ביצירת תבניות מובנות, אפשר להתייחס ל-Dual Shadow Roots כתבניות שנוצרות כדי ליצור את שורש הצללית של רכיב נתון. העיצוב הנוכחי של ה-DOM Delarative Shadow DOM מאפשר לאפשרות הזו להתקיים בעתיד על ידי הגבלת השיוך של שורש הצלל לאלמנט יחיד.

סטרימינג מגניב

שיוך של שורשים הצהרתיים של Shadow Roots ישירות לרכיב ההורה שלהם מפשט את תהליך השדרוג ומצרפים אותם לאלמנט הזה. שורשים הצהרתיים של Shadow Roots מזוהים במהלך ניתוח ה-HTML ומצורפים מיד כאשר הם נתקלים בתג הפותח <template> שלהם. קוד HTML שמנתח בתוך <template> מנותח ישירות לשורש הצל, כדי שניתן יהיה "לשדר" אותו ולעבד אותו כפי שהוא.

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

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

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

מנתח בלבד

DOM Delarative Shadow DOM הוא תכונה של מנתח ה-HTML. פירוש הדבר הוא שניתוח נתוני שורש הצהרתי (shadow Root) יצורף רק לתגי <template> עם מאפיין shadowrootmode שנמצאים במהלך ניתוח ה-HTML. במילים אחרות, אפשר לבנות שורשים הצהרתיים של Shadow Roots במהלך ניתוח 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

כדי להימנע משיקולי אבטחה חשובים, אי אפשר גם ליצור שורשים הצהרתיים של Shadow Roots באמצעות ממשקי API לניתוח מקטעים כמו innerHTML או insertAdjacentHTML(). הדרך היחידה לנתח HTML עם החלה הצהרתית (Dlarative Shadow Roots) היא להשתמש ב-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> הרגילים:

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

גם לסגנונות שצוינו בשיטה הזו יש אופטימיזציה גבוהה: אם אותו גיליון סגנונות מופיע בכמה שורשים הצהרתיים של Shadow Root, הוא ייטען וינותח רק פעם אחת. הדפדפן משתמש בגיבוי CSSStyleSheet אחד שמשותף לכל רמות הבסיס של האזורים הכהים, ומבטל את תקורת הזיכרון הכפולה.

גיליונות סגנונות ניתנים לבנייה אינם נתמכים ב-DOM Declarative Shadow DOM. הסיבה לכך היא שבשלב זה אין דרך לערוך ב-HTML גיליונות סגנונות ניתנים לבנייה, ואין דרך להתייחס אליהם באכלוס של adoptedStyleSheets.

איך למנוע הבהוב של תוכן לא מסוגנן

אחת הבעיות הפוטנציאליות בדפדפנים שעדיין לא תומכים ב-Dashlarative Shadow DOM היא הימנעות מ"הבהוב של תוכן לא מעוצב". (FOUC), כאשר התוכן הגולמי מוצג עבור רכיבים מותאמים אישית שעדיין לא שודרגו. לפני הגרסה ההצהרתית של Shadow DOM, אחת מהשיטות הנפוצות למניעת FOUC הייתה להחיל כלל סגנון display:none על רכיבים מותאמים אישית שעדיין לא נטענו, כי לרכיבים האלה לא צורף שורש של הצללה. כך, התוכן לא יוצג עד שהוא יהיה 'מוכן':

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

עם ההשקה של Shadow DOM, אפשר לעבד או לכתוב רכיבים מותאמים אישית ב-HTML, כך שתוכן הצללים שלהם ממוקם במקום ומוכן לפני שהטמעת הרכיב בצד הלקוח נטענת:

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

במקרה הזה, display:none "FOUC" הכלל ימנע את הצגת התוכן של שורש הצל ההצהרתי. עם זאת, הסרת הכלל תגרום להצגת תוכן שגוי או לא מעוצב בדפדפנים שאין בהם תמיכה ב-Delarative Shadow DOM עד polyfill של ה-Dlarative Shadow DOM נטען ולהמיר את תבנית השורש של הצללית לשורש צל אמיתי.

למרבה המזל, אפשר לפתור את הבעיה הזו ב-CSS על ידי שינוי כלל הסגנון של FOUC. בדפדפנים שתומכים ב-declarative Shadow DOM, הרכיב <template shadowrootmode> מומר מיד לרמה הבסיסית (root) של הצל, ולא נשאר רכיב <template> בעץ ה-DOM. דפדפנים שלא תומכים ב-Delarative Shadow DOM משמרים את הרכיב <template>, שבו אנחנו יכולים להשתמש כדי למנוע FOUC:

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

במקום להסתיר את הרכיב המותאם אישית שטרם הוגדר, ה-"FOUC" המתוקן הכלל מסתיר את הילדים שלו כשהם עוקבים אחרי רכיב <template shadowrootmode>. לאחר הגדרת הרכיב המותאם אישית, הכלל לא תואם יותר. בדפדפנים שתומכים ב-declarative Shadow DOM, המערכת מתעלמת מהכלל, כי הצאצא <template shadowrootmode> מוסר במהלך ניתוח ה-HTML.

זיהוי תכונות ותמיכה בדפדפן

התכונה Shadow DOM זמינה החל מ-Chrome 90 ומ-Edge 91, אבל היא השתמשה במאפיין ישן יותר ולא סטנדרטי בשם shadowroot במקום במאפיין shadowrootmode הסטנדרטי. המאפיין shadowrootmode והתנהגות הסטרימינג החדשים זמינים ב-Chrome 111 וב-Edge 111.

מאחר שמדובר בממשק API חדש של פלטפורמת אינטרנט, ל-Delarative Shadow DOM אין עדיין תמיכה רחבה בכל הדפדפנים. ניתן לאתר תמיכה בדפדפן על ידי בדיקה אם קיים מאפיין shadowRootMode באב הטיפוס של HTMLTemplateElement:

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

פוליפיל

תהליך הבנייה של polyfill פשוט עבור DOM Declarative Shadow DOM הוא פשוט יחסית, מאחר ש-polyfill לא צריך לשכפל באופן מושלם את סמנטיקה של התזמון או את המאפיינים של מנתח בלבד, שהטמעת הדפדפן עוסקת בהם. כדי לבצע polyfill Declarative Shadow DOM, ניתן לסרוק את ה-DOM כדי למצוא את כל רכיבי <template shadowrootmode>, ולאחר מכן להמיר אותם ל-shadow Roots מצורפים ברכיב ההורה שלהם. אפשר לבצע את התהליך הזה כשהמסמך מוכן או כשהוא מופעל על ידי אירועים ספציפיים יותר, כמו מחזורי חיים של רכיבים מותאמים אישית.

(function attachShadowRoots(root) {
  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);

קריאה נוספת