จำกัดการเข้าถึงของตัวเลือกด้วยกฎ @scope ของ CSS

ดูวิธีใช้ @scope เพื่อเลือกองค์ประกอบภายในแผนผังย่อยที่จำกัดของ DOM เท่านั้น

การสนับสนุนเบราว์เซอร์

  • 118
  • 118
  • x
  • x

ศิลปะที่ละเอียดอ่อนในการเขียนตัวเลือก CSS

ขณะเขียนตัวเลือก คุณอาจพบว่าตัวเองแยกเป็น 2 โลก ในแง่หนึ่งคุณควรกำหนดอย่างเฉพาะเจาะจงว่าควรเลือกองค์ประกอบใดบ้าง ในทางกลับกัน คุณต้องการให้ตัวเลือกยังคงลบล้างได้ง่ายและไม่เชื่อมโยงกับโครงสร้าง DOM อย่างเหนียวแน่น

เช่น เมื่อต้องการเลือก "รูปภาพหลักในพื้นที่เนื้อหาของคอมโพเนนต์การ์ด" ซึ่งเป็นการเลือกองค์ประกอบที่เฉพาะเจาะจง คุณน่าจะไม่ต้องการเขียนตัวเลือก เช่น .card > .content > img.hero

  • ตัวเลือกนี้มีความเฉพาะเจาะจงสูงอยู่ที่ (0,3,1) ซึ่งทำให้ลบล้างได้ยากเมื่อโค้ดมีขนาดใหญ่ขึ้น
  • การใช้ตัวรวมย่อยโดยตรงจะเชื่อมโยงกับโครงสร้าง DOM อย่างเหนียวแน่น หากมาร์กอัปไม่เคยเปลี่ยน คุณจะต้องเปลี่ยน CSS ด้วย

แต่ก็ไม่ควรเขียนแค่ img เป็นตัวเลือกขององค์ประกอบดังกล่าว เพราะจะเป็นการเลือกองค์ประกอบรูปภาพทั้งหมดในหน้าเว็บ

การหาจุดสมดุลที่เหมาะสมมักไม่ใช่เรื่องง่าย ตลอดหลายปีที่ผ่านมา นักพัฒนาซอฟต์แวร์บางรายได้คิดค้นโซลูชันและวิธีแก้ปัญหาชั่วคราวเพื่อช่วยคุณในสถานการณ์เช่นนี้ เช่น

  • วิธีการอย่างเช่น BEM จะกำหนดให้คุณกำหนดคลาสของ card__img card__img--hero ให้แก่องค์ประกอบนั้นเพื่อไม่ให้มีความจำเพาะเจาะจงต่ำ ในขณะเดียวกันก็ช่วยให้คุณเจาะจงสิ่งที่เลือกได้
  • โซลูชันที่ใช้ JavaScript เช่น CSS ที่กำหนดขอบเขตหรือคอมโพเนนต์ที่จัดรูปแบบจะเขียนตัวเลือกทั้งหมดใหม่โดยการเพิ่มสตริงที่สร้างขึ้นแบบสุ่ม เช่น sc-596d7e0e-4 ลงในตัวเลือก เพื่อป้องกันไม่ให้ตัวเลือกกำหนดเป้าหมายองค์ประกอบที่อีกฝั่งหนึ่งของหน้าเว็บ
  • ไลบรารีบางรายการจะยกเลิกตัวเลือกทั้งหมดเลย และคุณต้องการให้คุณใส่ทริกเกอร์การจัดรูปแบบลงในมาร์กอัปโดยตรง

แล้วหากคุณไม่ต้องการใช้สิ่งเหล่านี้เลยล่ะ จะเกิดอะไรขึ้นหาก CSS มอบวิธีให้ทั้งองค์ประกอบที่คุณเลือกที่มีความเฉพาะเจาะจงสูง โดยที่คุณไม่ต้องเขียนตัวเลือกที่มีความเฉพาะเจาะจงสูง หรือตัวเลือกที่เชื่อมโยงกับ DOM ของคุณอย่างเหนียวแน่น จากนั้น @scope จะเข้ามามีบทบาท โดยมอบวิธีเลือกองค์ประกอบภายในแผนผังย่อยของ DOM เท่านั้น

ขอแนะนำ @scope

@scope ช่วยให้คุณจำกัดการเข้าถึงของเครื่องมือเลือกได้ ซึ่งทำได้โดยการตั้งค่ารากที่กำหนดขอบเขต ซึ่งกำหนดขอบเขตบนของแผนผังย่อยที่คุณต้องการกำหนดเป้าหมาย เมื่อใช้ชุดรากที่กำหนดขอบเขต กฎสไตล์ที่มีอยู่ซึ่งเรียกว่ากฎของรูปแบบที่กำหนดขอบเขต จะเลือกได้จากแผนผังย่อยที่จำกัดของ DOM เท่านั้น

ตัวอย่างเช่น หากต้องการกำหนดเป้าหมายเฉพาะองค์ประกอบ <img> ในคอมโพเนนต์ .card คุณจะต้องตั้งค่า .card เป็นรูทที่กำหนดขอบเขตของกฎระดับ @scope

@scope (.card) {
    img {
        border-color: green;
    }
}

กฎสไตล์ที่กำหนดขอบเขต img { … } จะเลือกองค์ประกอบ <img> ที่อยู่ในขอบเขตขององค์ประกอบ .card ที่ตรงกันได้อย่างมีประสิทธิภาพเท่านั้น

หากไม่ต้องการให้เลือกองค์ประกอบ <img> ภายในพื้นที่เนื้อหาของการ์ด (.card__content) คุณอาจทำให้ตัวเลือก img เจาะจงมากขึ้นได้ อีกวิธีหนึ่งที่ทำได้คือการใช้ข้อเท็จจริงที่ว่า @scope ที่เป็นกฎนั้นยอมรับขีดจำกัดขอบเขตซึ่งกำหนดขอบเขตล่างด้วย

@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}

กฎสไตล์ที่กำหนดขอบเขตนี้จะกำหนดเป้าหมายเฉพาะองค์ประกอบ <img> ที่วางไว้ระหว่างองค์ประกอบ .card ถึง .card__content ในแผนผังระดับบน การกำหนดขอบเขตประเภทนี้ซึ่งมีขอบเขตบนและล่างมักจะเรียกว่าขอบเขตโดนัท

ตัวเลือก :scope

โดยค่าเริ่มต้น กฎของรูปแบบที่กำหนดขอบเขตทั้งหมดจะเกี่ยวข้องกับรูทที่กำหนดขอบเขต คุณยังสามารถกำหนดเป้าหมายองค์ประกอบรากที่กำหนดขอบเขตได้ด้วย ในกรณีนี้ ให้ใช้ตัวเลือก :scope

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

ระบบจะเพิ่ม :scope ไว้หน้าตัวเลือกภายในกฎของรูปแบบที่กำหนดขอบเขตโดยปริยาย คุณแจ้งเรื่องดังกล่าวอย่างชัดแจ้งได้หากต้องการ โดยเตรียม :scope ด้วยตนเอง หรือคุณสามารถเพิ่มตัวเลือก & ไว้ด้านหน้าตัวเลือกจาก CSS Nesting

@scope (.card) {
    img {
       /* Selects img elements that are a child of .card */
    }
    :scope img {
        /* Also selects img elements that are a child of .card */
    }
    & img {
        /* Also selects img elements that are a child of .card */
    }
}

ขีดจำกัดขอบเขตสามารถใช้คลาส Pseudo :scope เพื่อกำหนดความสัมพันธ์ที่เฉพาะเจาะจงกับรากที่กำหนดขอบเขต

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

ขีดจำกัดการกำหนดขอบเขตยังอ้างอิงองค์ประกอบที่อยู่นอกรากที่กำหนดขอบเขตโดยใช้ :scope ได้ด้วย เช่น

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

โปรดทราบว่าตัวกฎของรูปแบบที่กำหนดขอบเขตจะออกจากแผนผังย่อยไม่ได้ การเลือกอย่างเช่น :scope + p ไม่ถูกต้องเนื่องจากพยายามเลือกองค์ประกอบที่ไม่ได้อยู่ในขอบเขต

@scope และความเฉพาะเจาะจง

ตัวเลือกที่คุณใช้ในเบื้องต้นสำหรับ @scope ไม่ส่งผลต่อความเฉพาะเจาะจงของตัวเลือกที่มี ในตัวอย่างด้านล่าง ความเจาะจงของตัวเลือก img ยังคงเป็น (0,0,1)

@scope (#sidebar) {
    img { /* Specificity = (0,0,1) */
        …
    }
}

ความจำเพาะของ :scope คือคลาสจำลองทั่วไป ซึ่งก็คือ (0,1,0)

@scope (#sidebar) {
    :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
        …
    }
}

ในตัวอย่างต่อไปนี้ ภายใน ระบบจะเขียน & ใหม่ให้เป็นตัวเลือกที่ใช้สำหรับรูทที่กำหนดขอบเขต โดยอยู่ภายในตัวเลือก :is() ในตอนท้าย เบราว์เซอร์จะใช้ :is(#sidebar, .card) img เป็นตัวเลือกในการจับคู่ กระบวนการนี้เรียกว่าการลดระดับน้ำตาล

@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        …
    }
}

เนื่องจาก & ถูกกรองค่าด้วย :is() ข้อมูลความจำเพาะของ & จึงคำนวณตามกฎการจำเพาะของ :is(): ความจำเพาะของ & จึงเป็นของอาร์กิวเมนต์ที่มีความเฉพาะเจาะจงมากที่สุด

จากตัวอย่างนี้ ความจำเพาะของ :is(#sidebar, .card) คืออาร์กิวเมนต์ที่มีความเฉพาะเจาะจงมากที่สุด ซึ่งก็คือ #sidebar ดังนั้น จึงกลายเป็น (1,0,0) รวมค่าที่ได้เข้ากับความเฉพาะเจาะจงของ img ซึ่งเท่ากับ (0,0,1) แล้วคุณจะได้ (1,0,1) เป็นความเฉพาะเจาะจงสำหรับตัวเลือกเชิงซ้อนทั้งหมด

@scope (#sidebar, .card) {
    & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
        …
    }
}

ความแตกต่างระหว่าง :scope กับ & ภายใน @scope

นอกจากความแตกต่างในวิธีคำนวณความจำเพาะแล้ว ความแตกต่างอีกอย่างหนึ่งระหว่าง :scope กับ & คือ :scope แสดงถึงรูทที่กำหนดขอบเขตที่ตรงกัน ขณะที่ & แสดงตัวเลือกที่ใช้เพื่อจับคู่รากที่กำหนดขอบเขต

ด้วยเหตุนี้ จึงมีการใช้ & ได้หลายครั้ง ซึ่งตรงข้ามกับ :scope ซึ่งคุณใช้ได้เพียงครั้งเดียวเนื่องจากจับคู่รากที่กำหนดขอบเขตภายในรากที่กำหนดขอบเขตไม่ได้

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :root :root { /* ❌ Does not work */
    …
  }
}

ขอบเขตที่ไม่มีขอบเขต

เมื่อเขียนรูปแบบในบรรทัดด้วยองค์ประกอบ <style> คุณสามารถกำหนดขอบเขตกฎสไตล์ให้กับองค์ประกอบระดับบนที่ล้อมรอบขององค์ประกอบ <style> ได้โดยไม่ได้ระบุรูทที่กำหนดขอบเขตใดๆ ซึ่งทำได้โดยละเว้นการเกริ่นนำของ @scope

<div class="card">
  <div class="card__header">
    <style>
      @scope {
        img {
          border-color: green;
        }
      }
    </style>
    <h1>Card Title</h1>
    <img src="…" height="32" class="hero">
  </div>
  <div class="card__content">
    <p><img src="…" height="32"></p>
  </div>
</div>

ในตัวอย่างข้างต้น กฎที่กำหนดขอบเขตจะกำหนดเป้าหมายเฉพาะองค์ประกอบภายใน div ที่มีชื่อคลาส card__header เท่านั้น เนื่องจาก div เป็นองค์ประกอบระดับบนสุดขององค์ประกอบ <style>

@ขอบเขตใน Cascade

ภายใน CSS Cascade นั้น @scope ได้เพิ่มเกณฑ์ใหม่ด้วย ได้แก่ ระยะห่างจากขอบ ขั้นตอนจะอยู่หลังความจำเพาะเจาะจง แต่มาก่อนลำดับการปรากฏ

การแสดงภาพการเรียงซ้อนของ CSS

ตามตามข้อกำหนด:

เมื่อเปรียบเทียบการประกาศที่ปรากฏในกฎรูปแบบซึ่งมีรากที่กำหนดขอบเขตต่างกัน การประกาศที่มีจำนวนการข้ามขององค์ประกอบหรือองค์ประกอบระดับข้างเคียงน้อยที่สุดระหว่างรูทที่กำหนดขอบเขตและหัวข้อกฎรูปแบบที่มีขอบเขตจะชนะ

ขั้นตอนใหม่นี้สะดวกในการซ้อนคอมโพเนนต์รูปแบบต่างๆ ลองดูตัวอย่างนี้ ซึ่งยังไม่ได้ใช้ @scope

<style>
    .light { background: #ccc; }
    .dark  { background: #333; }
    .light a { color: black; }
    .dark a { color: white; }
</style>
<div class="light">
    <p><a href="#">What color am I?</a></p>
    <div class="dark">
        <p><a href="#">What about me?</a></p>
        <div class="light">
            <p><a href="#">Am I the same as the first?</a></p>
        </div>
    </div>
</div>

เมื่อดูมาร์กอัปเล็กๆ ดังกล่าว ลิงก์ที่ 3 จะเป็น white แทนที่จะเป็น black แม้ว่าจะเป็นระดับย่อยของ div ที่ใช้คลาส .light ก็ตาม ซึ่งเกิดจากลำดับเกณฑ์ของลักษณะที่ปรากฏที่การเรียงซ้อนจะใช้ที่นี่ในการกำหนดผู้ชนะ เห็นว่ามีการประกาศ .dark a เป็นครั้งสุดท้าย จึงชนะจากกฎ .light a

ด้วยเกณฑ์ความใกล้เคียงที่กำหนดขอบเขต ซึ่งตอนนี้สามารถแก้ไขได้แล้ว:

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

เนื่องจากตัวเลือก a ที่กำหนดขอบเขตทั้ง 2 รายการมีความเจาะจงเหมือนกัน เกณฑ์พื้นที่ใกล้เคียงที่กำหนดขอบเขตจึงเริ่มดำเนินการ ซึ่งจะชั่งน้ำหนักตัวเลือกทั้งสองโดยใกล้กับรากที่กำหนดขอบเขตไว้ สำหรับองค์ประกอบ a ที่ 3 นั้น จะมีเพียง 1 ฮอปไปยังรากที่กำหนดขอบเขต .light แต่ 2 ไปยัง .dark ดังนั้น ตัวเลือก a ใน .light จึงจะชนะ

หมายเหตุปิด: การแยกตัวเลือก ไม่ใช่การแยกรูปแบบ

สิ่งสำคัญที่ควรทราบประการหนึ่งคือ @scope จะจำกัดการเข้าถึงของเครื่องมือเลือก แต่ไม่ได้มีการแยกสไตล์ พร็อพเพอร์ตี้ที่รับช่วงต่อไปยังระดับล่างจะยังคงรับค่าเดิมจากขอบเขตล่างของ @scope พร็อพเพอร์ตี้ดังกล่าวรายการหนึ่งคือ color เมื่อประกาศว่าขอบเขตที่อยู่ในขอบเขตโดนัท color จะยังคงสืบทอดไปยังเด็กที่อยู่ในรูของโดนัท

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

ในตัวอย่างด้านบน องค์ประกอบ .card__content และองค์ประกอบย่อยมีสี hotpink เพราะรับค่ามาจาก .card

(รูปภาพปกโดย rustam burkhanov ใน Unsplash)