ภายใน Polyfill การค้นหาคอนเทนเนอร์

Gerald Monaco
Gerald Monaco

คิวรีคอนเทนเนอร์คือฟีเจอร์ CSS ใหม่ที่ให้คุณเขียนตรรกะการจัดรูปแบบที่กำหนดเป้าหมายไปยังฟีเจอร์ขององค์ประกอบหลัก (เช่น ความกว้างหรือความสูง) เพื่อจัดรูปแบบองค์ประกอบย่อย เมื่อเร็วๆ นี้ เราได้ปล่อยการอัปเดตครั้งใหญ่สำหรับ polyfill ซึ่งสอดคล้องกับหน้าการสนับสนุนในเบราว์เซอร์

ในโพสต์นี้ คุณจะได้เห็นวิธีการทำงานของ Polyfill วิธีการทำงานของ Polyfill อุปสรรค และแนวทางปฏิบัติแนะนำในการใช้เพื่อให้ผู้เข้าชมได้รับประสบการณ์ที่ยอดเยี่ยม

กลไกภายใน

การแปลง

เมื่อโปรแกรมแยกวิเคราะห์ CSS ภายในเบราว์เซอร์พบ at-rule ที่ไม่รู้จัก เช่น กฎ @container ใหม่ล่าสุด ระบบจะทิ้งกฎนั้นราวกับว่าไม่เคยมีมาก่อน ดังนั้นสิ่งแรกและสำคัญที่สุดที่ Polyfill ต้องทำคือแปลงคำค้นหา @container ไปยังสิ่งที่จะไม่ทิ้ง

ขั้นตอนแรกในการแปลงภาษาคือแปลงกฎ @container ระดับบนสุดเป็นข้อความค้นหา @media ซึ่งมักจะทำให้เนื้อหามีการจัดกลุ่มไว้ด้วยกัน เช่น เมื่อใช้ CSSOM API และเมื่อดูแหล่งที่มา CSS

ก่อน
@container (width > 300px) {
  /* content */
}
หลัง
@media all {
  /* content */
}

ก่อนการค้นหาคอนเทนเนอร์ CSS ไม่มีวิธีที่ผู้เขียนจะเปิดหรือปิดใช้กลุ่มกฎโดยไม่มีกฎเกณฑ์ หากต้องการใช้การแปลงข้อมูลพฤติกรรมนี้ กฎภายในการค้นหาคอนเทนเนอร์จะต้องได้รับการเปลี่ยนรูปแบบด้วย @container แต่ละรายการจะได้รับรหัสที่ไม่ซ้ำกัน (เช่น 123) ซึ่งจะใช้เพื่อเปลี่ยนรูปแบบตัวเลือกแต่ละรายการเพื่อให้มีผลเฉพาะเมื่อองค์ประกอบมีแอตทริบิวต์ cq-XYZ ที่มีรหัสนี้ โพลีฟิลล์จะตั้งค่าแอตทริบิวต์นี้เมื่อรันไทม์

ก่อน
@container (width > 300px) {
  .card {
    /* ... */
  }
}
หลัง
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

สังเกตการใช้คลาสจำลอง :where(...) โดยปกติแล้ว การรวมตัวเลือกแอตทริบิวต์เพิ่มเติมจะเพิ่มความจำเพาะของตัวเลือกดังกล่าว เมื่อใช้คลาสจำลอง คุณจะใช้เงื่อนไขเพิ่มเติมได้ในขณะที่ยังคงความเฉพาะเจาะจงเดิมไว้ ลองดูตัวอย่างต่อไปนี้เพื่อทําความเข้าใจความสำคัญของเรื่องนี้

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

เนื่องจาก CSS นี้ องค์ประกอบที่มีคลาส .card ควรมี color: red เสมอ เนื่องจากกฎภายหลังจะลบล้างกฎก่อนหน้าด้วยตัวเลือกและความเฉพาะเจาะจงเดียวกันเสมอ ดังนั้น การแปลงกฎแรกและใส่ตัวเลือกแอตทริบิวต์เพิ่มเติมโดยไม่มี :where(...) จะเพิ่มความเฉพาะเจาะจงและทําให้ใช้ color: blue อย่างไม่ถูกต้อง

อย่างไรก็ตาม คลาสจำลอง :where(...) ค่อนข้างใหม่ สําหรับเบราว์เซอร์ที่ไม่รองรับ โพลีฟีลจะมอบวิธีแก้ปัญหาที่ปลอดภัยและง่ายดาย คุณสามารถจงใจเพิ่มความเฉพาะเจาะจงของกฎได้โดยการเพิ่มตัวเลือก :not(.container-query-polyfill) จำลองลงในกฎ @container ด้วยตนเอง ดังนี้

ก่อน
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
หลัง
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

ซึ่งมีประโยชน์หลายประการ ดังนี้

  • ตัวเลือกใน CSS ต้นฉบับมีการเปลี่ยนแปลง ดังนั้นความแตกต่างของความเฉพาะเจาะจงจึงปรากฏอย่างชัดเจน ซึ่งยังทำหน้าที่เป็นเอกสารประกอบเพื่อให้คุณทราบว่าสิ่งใดได้รับผลกระทบเมื่อคุณไม่จำเป็นต้องรองรับวิธีแก้ปัญหาชั่วคราวหรือ polyfill อีกต่อไป
  • ความเฉพาะเจาะจงของกฎจะเหมือนกันเสมอ เนื่องจาก polyfill จะไม่เปลี่ยนแปลงกฎ

ในระหว่างการแปลง โพลีฟีลจะแทนที่ค่าว่างนี้ด้วยตัวเลือกแอตทริบิวต์ที่มีความเฉพาะเจาะจงเดียวกัน Polyfill จะใช้ตัวเลือกทั้ง 2 รายการเพื่อหลีกเลี่ยงปัญหาที่ไม่คาดคิด โดยตัวเลือกแหล่งที่มาเดิมจะใช้เพื่อพิจารณาว่าองค์ประกอบควรได้รับแอตทริบิวต์ polyfill หรือไม่ และตัวเลือกที่แปลงแล้วจะใช้สำหรับการจัดสไตล์

องค์ประกอบจำลอง

คำถามที่คุณอาจถามตัวเองคือ หาก polyfill ตั้งค่าแอตทริบิวต์ cq-XYZ บางรายการในองค์ประกอบเพื่อรวมรหัสคอนเทนเนอร์ที่ไม่ซ้ำกัน 123 แล้วจะรองรับองค์ประกอบจำลองซึ่งไม่สามารถตั้งค่าแอตทริบิวต์ได้อย่างไร

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

ก่อน
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
หลัง
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

ระบบจะย้ายตัวเลือกแบบมีเงื่อนไขไปยังท้ายองค์ประกอบต้นทาง #foo แทนที่จะเปลี่ยนรูปแบบเป็น #foo::before:where([cq-XYZ~="123"]) (ซึ่งไม่ถูกต้อง)

แต่ยังมีสิ่งอื่นๆ ที่จำเป็นด้วย คอนเทนเนอร์ไม่ได้รับอนุญาตให้แก้ไขสิ่งที่ไม่ได้อยู่ภายในคอนเทนเนอร์ (และคอนเทนเนอร์ไม่สามารถอยู่ภายในตัวคอนเทนเนอร์เองได้) แต่ให้คิดว่าสิ่งที่จะเกิดขึ้นคือ #foo เป็นองค์ประกอบคอนเทนเนอร์ที่ระบบค้นหา แอตทริบิวต์ #foo[cq-XYZ] จะมีการเปลี่ยนแปลงอย่างไม่ถูกต้อง และจะใช้กฎ #foo อย่างไม่ถูกต้อง

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

ก่อน
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
หลัง
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

เนื่องจากคอนเทนเนอร์จะไม่ใช้แอตทริบิวต์แรก (cq-XYZ-A) กับตนเอง ตัวเลือกแรกจะจับคู่ก็ต่อเมื่อคอนเทนเนอร์หลักอื่นมีคุณสมบัติตรงตามเงื่อนไขของคอนเทนเนอร์และนำแอตทริบิวต์นั้นไปใช้

หน่วยที่สัมพันธ์กับคอนเทนเนอร์

คิวรีคอนเทนเนอร์ยังมีหน่วยใหม่ 2-3 หน่วยที่คุณใช้ใน CSS ได้ เช่น cqw และ cqh สำหรับ 1% ของความกว้างและความสูง (ตามลำดับ) ของคอนเทนเนอร์หลักที่เหมาะสมซึ่งอยู่ใกล้ที่สุด ระบบจะเปลี่ยนรูปแบบหน่วยเป็นนิพจน์ calc(...) โดยใช้พร็อพเพอร์ตี้ที่กำหนดเองของ CSS เพื่อรองรับรายการเหล่านี้ โพลีฟิลล์จะตั้งค่าค่าของคุณสมบัติเหล่านี้ผ่านรูปแบบอินไลน์ในองค์ประกอบคอนเทนเนอร์

ก่อน
.card {
  width: 10cqw;
  height: 10cqh;
}
หลัง
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

นอกจากนี้ยังมีหน่วยเชิงตรรกะ เช่น cqi และ cqb สำหรับขนาดในบรรทัดและขนาดบล็อก (ตามลำดับ) การดำเนินการเหล่านี้มีความซับซ้อนกว่าเล็กน้อย เนื่องจากแกนแนวนอนและแกนแนวตั้งจะกำหนดโดย writing-mode ขององค์ประกอบที่ใช้หน่วย ไม่ใช่องค์ประกอบที่ค้นหา เพื่อรองรับความต้องการนี้ Polyfill จะใช้รูปแบบอินไลน์กับองค์ประกอบที่ writing-mode แตกต่างจากองค์ประกอบหลัก

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

ตอนนี้คุณสามารถเปลี่ยนหน่วยเป็นพร็อพเพอร์ตี้ที่กำหนดเองของ CSS ที่เหมาะสมได้เช่นเดียวกับก่อนหน้านี้

พร็อพเพอร์ตี้

การค้นหาคอนเทนเนอร์ยังเพิ่มคุณสมบัติ CSS ใหม่ 2-3 อย่าง เช่น container-type และ container-name เนื่องจาก API เช่น getComputedStyle(...) ใช้กับพร็อพเพอร์ตี้ที่ไม่รู้จักหรือไม่ถูกต้องไม่ได้ ระบบจึงเปลี่ยน API เหล่านี้เป็นพร็อพเพอร์ตี้ที่กำหนดเองของ CSS หลังจากแยกวิเคราะห์ด้วย หากแยกวิเคราะห์พร็อพเพอร์ตี้ไม่ได้ (เช่น เนื่องจากมีค่าที่ไม่ถูกต้องหรือไม่รู้จัก) ระบบจะปล่อยพร็อพเพอร์ตี้นั้นไว้เพื่อให้เบราว์เซอร์จัดการ

ก่อน
.card {
  container-name: card-container;
  container-type: inline-size;
}
หลัง
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

ระบบจะเปลี่ยนรูปแบบคุณสมบัติเหล่านี้ทุกครั้งที่ค้นพบ ซึ่งช่วยให้ Polyfill ทำงานกับฟีเจอร์อื่นๆ ของ CSS เช่น @supports ได้อย่างลงตัว ฟังก์ชันการทำงานนี้เป็นพื้นฐานของแนวทางปฏิบัติแนะนำในการใช้ polyfill ตามที่อธิบายไว้ด้านล่าง

ก่อน
@supports (container-type: inline-size) {
  /* ... */
}
หลัง
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

โดยค่าเริ่มต้น ระบบจะรับค่าของพร็อพเพอร์ตี้ที่กำหนดเองของ CSS มาใช้ ซึ่งหมายความว่ารายการย่อยของ .card จะใช้ค่าของ --cq-XYZ-container-name และ --cq-XYZ-container-type ซึ่งไม่ใช่ลักษณะการทำงานของพร็อพเพอร์ตี้เนทีฟ ในการแก้ปัญหานี้ โพลีฟีลจะแทรกกฎต่อไปนี้ไว้ก่อนสไตล์ของผู้ใช้ เพื่อให้แน่ใจว่าองค์ประกอบทุกรายการจะได้รับค่าเริ่มต้น เว้นแต่ว่าจะมีกฎอื่นเขียนทับไว้

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

แนวทางปฏิบัติแนะนำ

แม้ว่าจะมีการคาดหวังว่าผู้เข้าชมส่วนใหญ่จะเรียกใช้เบราว์เซอร์ที่มีการรองรับการค้นหาคอนเทนเนอร์ในตัวเร็วกว่าในภายหลัง แต่ก็ยังคงจำเป็นที่จะต้องให้ประสบการณ์ที่ดีแก่ผู้เข้าชมที่เหลือของคุณ

ระหว่างการโหลดครั้งแรก ต้องดำเนินการหลายอย่างก่อนที่ Polyfill จะจัดเลย์เอาต์หน้าเว็บได้ ดังนี้

  • จำเป็นต้องโหลดและเริ่มต้น polyfill
  • จำเป็นต้องแยกวิเคราะห์และแปลงไฟล์สไตล์ เนื่องจากไม่มี API ใดที่เข้าถึงซอร์สไฟล์ดิบของสไตล์ชีตภายนอก ระบบจึงอาจต้องดึงข้อมูลใหม่แบบไม่พร้อมกัน แม้ว่าควรจะทำจากแคชของเบราว์เซอร์เท่านั้น

หาก polyfill ไม่จัดการข้อกังวลเหล่านี้อย่างละเอียด อาจทำให้ Core Web Vitals ลดลง

เราได้ออกแบบ polyfill ให้ให้ความสําคัญกับ First Input Delay (FID) และ Cumulative Layout Shift (CLS) เพื่อช่วยให้คุณสามารถมอบประสบการณ์การใช้งานที่น่าพึงพอใจแก่ผู้เข้าชมได้ง่ายขึ้น ซึ่งอาจทำให้ Largest Contentful Paint (LCP) ลดลง กล่าวโดยละเอียดคือ Polyfill ไม่ได้รับประกันว่าจะมีการประเมินการค้นหาคอนเทนเนอร์ก่อน First Paint ซึ่งหมายความว่าคุณต้องซ่อนเนื้อหาที่มีขนาดหรือตําแหน่งจะได้รับผลกระทบจากการใช้การค้นหาคอนเทนเนอร์ไว้จนกว่า polyfill จะโหลดและแปลง CSS ของคุณแล้ว เพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดีที่สุด โดยวิธีหนึ่งที่ทำได้คือการใช้กฎ @supports ดังนี้

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

เราขอแนะนำให้คุณรวมสิ่งนี้เข้ากับภาพเคลื่อนไหวการโหลด CSS ล้วนๆ ซึ่งวางไว้เหนือเนื้อหา (ที่ซ่อนอยู่) อย่างสมบูรณ์ เพื่อบอกผู้เข้าชมว่ากำลังมีบางอย่างเกิดขึ้น ดูการสาธิตวิธีการนี้แบบเต็มได้ที่นี่

เราแนะนำให้ใช้วิธีการนี้ด้วยเหตุผลหลายประการดังนี้

  • ตัวโหลด CSS ล้วนช่วยลดค่าใช้จ่ายสำหรับผู้ใช้ที่มีเบราว์เซอร์รุ่นใหม่ และให้ข้อเสนอแนะเพียงเล็กน้อยแก่ผู้ใช้เบราว์เซอร์รุ่นเก่าและเครือข่ายที่ช้ากว่า
  • การรวมตำแหน่งแบบสัมบูรณ์ของตัวโหลดกับ visibility: hidden จะช่วยให้คุณหลีกเลี่ยงการเปลี่ยนแปลงเลย์เอาต์
  • หลังจากโหลด polyfill แล้ว เงื่อนไข @supports นี้จะหยุดทำงาน และระบบจะแสดงเนื้อหาของคุณ
  • ในเบราว์เซอร์ที่รองรับการค้นหาคอนเทนเนอร์ในตัว เงื่อนไขจะไม่ผ่านเลย ดังนั้นหน้าเว็บจะแสดงใน First Paint ตามปกติ

บทสรุป

หากสนใจที่จะใช้การค้นหาคอนเทนเนอร์ในเบราว์เซอร์รุ่นเก่า ให้ลองใช้ polyfill โปรดแจ้งปัญหาหากพบปัญหา

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