שיפור ההטמעה של CSS באמצעות CSSNestedDeclarations

תאריך פרסום: 8 באוקטובר 2024

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

השינויים האלה זמינים ב-Chrome מגרסה 130 ומוכנים לבדיקה ב-Firefox Nightly 132 וב-Safari Technology Preview 204.

תמיכה בדפדפנים

  • Chrome: 130.
  • Edge:‏ 130.
  • Firefox: 132.
  • Safari: לא נתמך.

הבעיה בהטמעת CSS ללא CSSNestedDeclarations

אחד ממלכודות ההטמעה של CSS הוא שבמקור, קטע הקוד הבא לא פועל כצפוי:

.foo {
    width: fit-content;

    @media screen {
        background-color: red;
    }
    
    background-color: green;
}

אם תעיינו בקוד, תוכלו להניח שלרכיב <div class=foo> יש green background-color כי ההצהרה על background-color: green; מופיעה בסוף. עם זאת, זה לא המצב ב-Chrome לפני גרסה 130. בגרסאות האלה, שבהן אין תמיכה ב-CSSNestedDeclarations, הערך של background-color ברכיב הוא red.

לאחר ניתוח הכלל בפועל של Chrome לפני 130 שימושים, הוא מבצע את הפעולות הבאות:

.foo {
    width: fit-content;
    background-color: green;

    @media screen {
        & {
            background-color: red;
        }
    }
}

קובץ ה-CSS אחרי הניתוח עבר שני שינויים:

  • ה-background-color: green; הועבר למעלה כדי להצטרף לשתי ההצהרות האחרות.
  • השדה CSSMediaRule שהוצב בו שכתב נכתב כדי לכווץ את ההצהרות שלו בCSSStyleRule נוסף באמצעות הבורר &.

שינוי אופייני נוסף שרואים כאן הוא שהמנתח משאיר מאחור מאפיינים שהוא לא תומך בהם.

אפשר לבדוק בעצמכם את ה-CSS אחרי הניתוח על ידי קריאה חוזרת של cssText מ-CSSStyleRule.

אתם יכולים לנסות זאת בעצמכם בגן המשחקים האינטראקטיבי הזה:

למה קובץ ה-CSS הזה נכתב מחדש?

כדי להבין למה התרחשה הכתיבה מחדש הפנימית הזו, צריך להבין איך ה-CSSStyleRule הזה מיוצג במודל האובייקטים של CSS‏ (CSSOM).

ב-Chrome בגרסאות קודמות ל-130, קטע ה-CSS ששיתפתם מקודם עובר סריאליזציה לקוד הבא:

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = ".foo"
  .resolvedSelectorText = ".foo"
  .specificity = "(0,1,0)"
  .style (CSSStyleDeclaration, 2) =
    - width: fit-content
    - background-color: green
  .cssRules (CSSRuleList, 1) =
    ↳ CSSMediaRule
    .type = MEDIA_RULE
    .cssRules (CSSRuleList, 1) =
      ↳ CSSStyleRule
        .type = STYLE_RULE
        .selectorText = "&"
        .resolvedSelectorText = ":is(.foo)"
        .specificity = "(0,1,0)"
        .style (CSSStyleDeclaration, 1) =
          - background-color: red

מבין כל המאפיינים של CSSStyleRule, שני המאפיינים הבאים רלוונטיים במקרה הזה:

  • הנכס style, שהוא מופע של CSSStyleDeclaration שמייצג את ההצהרות.
  • המאפיין cssRules, שהוא CSSRuleList שמכיל את כל אובייקטי ה-CSSRule בתצוגת עץ.

מאחר שכל ההצהרות מקודק ה-CSS מסתיימות בנכס style של CSStyleRule, יש אובדן מידע. בבדיקת המאפיין style לא ברור שה-background-color: green הוצהר אחרי CSSMediaRule המקונן.

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = ".foo"
  .style (CSSStyleDeclaration, 2) =
    - width: fit-content
    - background-color: green
  .cssRules (CSSRuleList, 1) =
    ↳ …

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

לגבי ההצהרות בתוך ה-CSSMediaRule שמקובצות פתאום ב-CSSStyleRule: הסיבה לכך היא שה-CSSMediaRule לא תוכנן להכיל הצהרות.

בגלל ש-CSSMediaRule יכול להכיל כללים בתצוגת עץ, שאפשר לגשת אליהם דרך המאפיין cssRules, ההצהרות מוקף באופן אוטומטי ב-CSSStyleRule.

↳ CSSMediaRule
  .type = MEDIA_RULE
  .cssRules (CSSRuleList, 1) =
    ↳ CSSStyleRule
      .type = STYLE_RULE
      .selectorText = "&"
      .resolvedSelectorText = ":is(.foo)"
      .specificity = "(0,1,0)"
      .style (CSSStyleDeclaration, 1) =
        - background-color: red

איך פותרים את הבעיה הזו?

קבוצת העבודה של שירות ה-CSS בדקה כמה אפשרויות לפתרון הבעיה הזו.

אחד מהפתרונות המוצעים היה לכווץ את כל ההצהרות הבסיסיות בתוך CSSStyleRule בתוך בורר בתוך בורר (&). הרעיון הזה נמחק מסיבות שונות, כולל תופעות הלוואי הלא רצויות הבאות של הסרת & כתוצאה מ:is(…):

  • יש לה השפעה על הספציפיות. הסיבה לכך היא ש-:is() מקבל את הספציפיות של הארגומנט הספציפי ביותר שלו.
  • הוא לא פועל טוב עם פסאודו-רכיבים בסלקטורים החיצוניים המקוריים. הסיבה לכך היא ש-:is() לא מקבל פסאודו-רכיבים בארגומנט של רשימת הבוררים.

לדוגמה:

#foo, .foo, .foo::before {
  width: fit-content;
  background-color: red;

  @media screen {
    background-color: green;
  }
}

לאחר ניתוח קטע הקוד הזה, הוא הופך לזה ב-Chrome לפני גרסה 130:

#foo,
.foo,
.foo::before {
  width: fit-content;
  background-color: red;

  @media screen {
    & {
      background-color: green;
    }
  }
}

זו בעיה כי ה-CSSRule בתצוגת עץ עם הבורר &:

  • המערכת מפחיתה את הנתונים ל-:is(#foo, .foo), ומוציאה את .foo::before מרשימת הבוררים.
  • הוא ספציפי ל-(1,0,0) ולכן קשה יותר להחליף אותו מאוחר יותר.

אפשר לבדוק את זה על ידי בדיקת הפורמט שאליו הכלל עובר סריאליזציה:

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = "#foo, .foo, .foo::before"
  .resolvedSelectorText = "#foo, .foo, .foo::before"
  .specificity = (1,0,0),(0,1,0),(0,1,1)
  .style (CSSStyleDeclaration, 2) =
    - width: fit-content
    - background-color: red
  .cssRules (CSSRuleList, 1) =
    ↳ CSSMediaRule
      .type = MEDIA_RULE
      .cssRules (CSSRuleList, 1) =
        ↳ CSSStyleRule
          .type = STYLE_RULE
          .selectorText = "&"
          .resolvedSelectorText = ":is(#foo, .foo, .foo::before)"
          .specificity = (1,0,0)
          .style (CSSStyleDeclaration, 1) =
            - background-color: green

מבחינה חזותית, המשמעות היא גם שהערך של background-color ב-.foo::before הוא red במקום green.

גישה נוספת שקבוצת העבודה של CSS בחנה היא להקיף את כל ההצהרות המוטמעות בכלל @nest. הבקשה נדחתה בגלל שהיא עלולה לגרום לנסיגה בחוויית המפתחים.

אנחנו גאים להציג את הממשק CSSNestedDeclarations

הפתרון שאליו הגיעה קבוצת העבודה בנושא CSS הוא הצגת כלל ההצהרות בתצוגת עץ.

הכלל הזה לגבי הצהרות בתצוגת עץ מיושם ב-Chrome החל מגרסה 130.

תמיכה בדפדפנים

  • Chrome:‏ 130.
  • Edge:‏ 130.
  • Firefox: 132.
  • Safari: לא נתמך.

ההוספה של הכלל לגבי הצהרות בתצוגת עץ משנה את האופן שבו מתבצע הניתוח של קובצי ה-CSS, כך שהמערכת תארוז באופן אוטומטי הצהרות רצופות בתצוגת עץ ישירה במכונה מסוג CSSNestedDeclarations. כשממיינים את המאפיין, המכונה של CSSNestedDeclarations מופיעה במאפיין cssRules של CSSStyleRule.

שוב, ניקח לדוגמה את CSSStyleRule הבא:

.foo {
  width: fit-content;

  @media screen {
    background-color: red;
  }
    
  background-color: green;
}

כשממיינים בסדרה ב-Chrome 130 ואילך, זה נראה כך:

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = ".foo"
  .resolvedSelectorText = ".foo"
  .specificity = (0,1,0)
  .style (CSSStyleDeclaration, 1) =
    - width: fit-content
  .cssRules (CSSRuleList, 2) =
    ↳ CSSMediaRule
      .type = MEDIA_RULE
      .cssRules (CSSRuleList, 1) =
        ↳ CSSNestedDeclarations
          .style (CSSStyleDeclaration, 1) =
            - background-color: red
    ↳ CSSNestedDeclarations
      .style (CSSStyleDeclaration, 1) =
        - background-color: green

בגלל שהכלל CSSNestedDeclarations מסתיים ב-CSSRuleList, המנתח יכול לשמור על המיקום של הצהרת background-color: green: אחרי ההצהרה background-color: red (ששייכת ל-CSSMediaRule).

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

הוכחה לכך היא קריאה של cssText של CSSStyleRule. בזכות הכלל של ההצהרות בתצוגת עץ, הוא זהה ל-CSS שהזנתם:

.foo {
  width: fit-content;

  @media screen {
    background-color: red;
  }
    
  background-color: green;
}

משמעות המדיניות מבחינתכם

המשמעות היא שהחל מגרסה 130 של Chrome, ההטמעה של CSS בתוך CSS השתפרה משמעותית. אבל, המשמעות היא גם שיכול להיות שתצטרכו לעבור על חלק מהקוד אם שילוב של הצהרות בסיסיות עם כללים בתצוגת עץ.

בדוגמה הבאה נעשה שימוש בפונקציה הנהדרת @starting-style

/* This does not work in Chrome 130 */
#mypopover:popover-open {
  @starting-style {
    opacity: 0;
    scale: 0.5;
  }

  opacity: 1;
  scale: 1;
}

לפני Chrome 130, ההצהרות האלה היו מועברות (hoisted). בסיום, ההצהרות opacity: 1; ו-scale: 1; ייכנסו ל-CSSStyleRule.style, ולאחר מכן CSSStartingStyleRule (שמייצג את הכלל @starting-style) ב-CSSStyleRule.cssRules.

החל מגרסת Chrome 130, ההצהרות לא מועברות יותר לחלק העליון של הקוד, ובסופו של דבר נוצרים שני אובייקטים בתצוגת עץ של CSSRule ב-CSSStyleRule.cssRules. לפי הסדר: CSSStartingStyleRule אחד (שמייצג את הכלל @starting-style) ו-CSSNestedDeclarations אחד שמכיל את ההצהרות של opacity: 1; scale: 1;.

בגלל השינוי הזה, ההצהרות @starting-style מוחלפות על ידי ההצהרות שנכללות במכונה CSSNestedDeclarations, וכך מסירה את האנימציה של הרשומה.

כדי לתקן את הקוד, צריך לוודא שהבלוק @starting-style מופיע אחרי ההצהרות הרגילות. למשל:

/* This works in Chrome 130 */
#mypopover:popover-open {
  opacity: 1;
  scale: 1;

  @starting-style {
    opacity: 0;
    scale: 0.5;
  }
}

אם משאירים את ההצהרות בתצוגת עץ מעל הכללים בתצוגת עץ כשמשתמשים בתצוגת עץ של CSS, הקוד יפעל בדרך כלל בצורה תקינה בכל הגרסאות של כל הדפדפנים שתומכים בתצוגת עץ של CSS.

לסיום, כדי לזהות את הזמינות של CSSNestedDeclarations, אפשר להשתמש בקטע הקוד הבא של JavaScript:

if (!("CSSNestedDeclarations" in self && "style" in CSSNestedDeclarations.prototype)) {
  // CSSNestedDeclarations is not available
}