تحسين تداخل CSS باستخدام CSSNestedDeclarations

تاريخ النشر: 8 أكتوبر 2024

لحلّ بعض المشاكل الغريبة في تداخل CSS، قرّرت مجموعة عمل CSS إضافة واجهة CSSNestedDeclarations إلى مواصفات تداخل CSS. ومن خلال هذه الإضافة، لم تعُد التعريفات التي تأتي بعد قواعد الأنماط تنتقل للأعلى، بالإضافة إلى بعض التحسينات الأخرى.

تتوفّر هذه التغييرات في Chrome اعتبارًا من الإصدار 130 وهي جاهزة للاختبار في الإصدار 132 من Firefox Nightly والإصدار 204 من Safari Technology Preview.

توافق المتصفّح

  • 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> يحتوي على background-color green لأن تعريف 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 من التمييز بين الخصائص التي تظهر في بداية محتوى قاعدة النمط وتلك التي تظهر مختلطة مع قواعد أخرى، وذلك لكي يعمل محرك 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;
}

عند تسلسلها في الإصدار 130 من Chrome أو الإصدارات الأحدث، يظهر الرمز على النحو التالي:

↳ 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;
}

معنى هذه السياسة

وهذا يعني أنّ عملية دمج CSS أصبحت أفضل بكثير اعتبارًا من الإصدار 130 من Chrome. ويعني ذلك أيضًا أنّه قد يكون عليك مراجعة بعض الرموز البرمجية إذا كنت تتخلّل بين التعريفات الأساسية القواعد المُدمجة.

راجِع المثال التالي الذي يستخدم الرمز الرائع @starting-style.

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

  opacity: 1;
  scale: 1;
}

قبل الإصدار 130 من Chrome، كان يتم رفع هذه البيانات. سينتهي بك الأمر ببيانَي opacity: 1; وscale: 1; في CSSStyleRule.style، متبوعَين بـ CSSStartingStyleRule (يمثّل قاعدة @starting-style) في CSSStyleRule.cssRules.

اعتبارًا من الإصدار 130 من Chrome، لم تعُد يتمّ تصعيد البيانات، وينتهي بك الأمر بعنصرَين من النوع 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
}