polyfill truy vấn bên trong vùng chứa

Gerald Monaco
Gerald Monaco

Truy vấn vùng chứa là một tính năng CSS mới cho phép bạn viết logic định kiểu nhắm mục tiêu các tính năng của phần tử mẹ (ví dụ: chiều rộng hoặc chiều cao) để tạo kiểu cho phần tử con. Gần đây, một bản cập nhật lớn cho polyfill đã được phát hành, trùng với lúc hỗ trợ đích trong các trình duyệt.

Trong bài đăng này, bạn sẽ có thể nắm được cách thức hoạt động của polyfill, những thách thức mà công nghệ này vượt qua và các phương pháp hay nhất khi sử dụng công cụ này để cung cấp trải nghiệm người dùng tuyệt vời cho khách truy cập của bạn.

Tìm hiểu sâu

Chuyển đổi

Khi trình phân tích cú pháp CSS trong một trình duyệt gặp một quy tắc tại không xác định, chẳng hạn như quy tắc @container hoàn toàn mới, thì trình phân tích cú pháp sẽ loại bỏ quy tắc đó như thể chưa từng tồn tại. Do đó, điều đầu tiên và quan trọng nhất mà polyfill phải làm là chuyển đổi truy vấn @container thành một nội dung nào đó sẽ không bị loại bỏ.

Bước đầu tiên trong quá trình dịch là chuyển đổi quy tắc @container cấp cao nhất thành truy vấn @media. Điều này chủ yếu là đảm bảo nội dung vẫn được nhóm với nhau. Ví dụ: khi sử dụng API CSSOM và khi xem nguồn CSS.

Trước
@container (width > 300px) {
  /* content */
}
Sau
@media all {
  /* content */
}

Trước khi có truy vấn vùng chứa, CSS không có cách nào để tác giả tự ý bật hoặc vô hiệu các nhóm quy tắc. Để polyfill hành vi này, các quy tắc bên trong truy vấn vùng chứa cũng cần được biến đổi. Mỗi @container được cấp một mã nhận dạng duy nhất riêng (ví dụ: 123). Mã này dùng để biến đổi từng bộ chọn sao cho chỉ áp dụng khi phần tử có thuộc tính cq-XYZ bao gồm cả mã nhận dạng này. Thuộc tính này sẽ được polyfill thiết lập trong thời gian chạy.

Trước
@container (width > 300px) {
  .card {
    /* ... */
  }
}
Sau
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

Hãy lưu ý việc sử dụng lớp giả :where(...). Thông thường, việc thêm một bộ chọn thuộc tính bổ sung sẽ làm tăng đặc điểm của bộ chọn đó. Với lớp giả, bạn có thể áp dụng điều kiện bổ sung mà vẫn giữ nguyên tính đặc hiệu ban đầu. Để biết lý do tại sao điều này lại quan trọng, hãy xem xét ví dụ sau:

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

.card {
  color: red;
}

Với CSS này, một phần tử có lớp .card phải luôn có color: red, vì quy tắc sau này sẽ luôn ghi đè quy tắc trước đó bằng cùng một bộ chọn và tính cụ thể. Do đó, việc chuyển đổi quy tắc đầu tiên và thêm một bộ chọn thuộc tính bổ sung mà không có :where(...) sẽ làm tăng độ cụ thể và khiến color: blue được áp dụng nhầm.

Tuy nhiên, lớp giả :where(...) rất mới. Đối với những trình duyệt không hỗ trợ tính năng này, polyfill sẽ cung cấp một giải pháp an toàn và dễ dàng: bạn có thể cố ý tăng độ cụ thể của các quy tắc bằng cách thêm thủ công bộ chọn :not(.container-query-polyfill) giả vào các quy tắc @container của bạn:

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

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

.card {
  color: red;
}

Việc này có một số lợi ích:

  • Bộ chọn trong CSS nguồn đã thay đổi, vì vậy sự khác biệt về tính cụ thể được thể hiện rõ ràng. Đây cũng là tài liệu để bạn biết những vấn đề sẽ bị ảnh hưởng khi không cần hỗ trợ giải pháp hoặc đoạn mã polyfill nữa.
  • Tính cụ thể của các quy tắc sẽ luôn giống nhau, vì polyfill không thay đổi nó.

Trong quá trình dịch chuyển, đoạn mã polyfill sẽ thay thế giả này bằng bộ chọn thuộc tính có cùng đặc tính. Để tránh mọi sự bất ngờ, polyfill sử dụng cả hai bộ chọn: bộ chọn nguồn ban đầu dùng để xác định xem phần tử có nhận thuộc tính polyfill hay không và bộ chọn đã chuyển mã được dùng để tạo kiểu.

Phần tử giả

Có thể bạn sẽ tự hỏi: nếu polyfill đặt một số thuộc tính cq-XYZ trên một phần tử để bao gồm mã vùng chứa duy nhất 123, thì làm cách nào để hỗ trợ các phần tử giả (không thể đặt thuộc tính)?

Phần tử giả luôn liên kết với một phần tử có thực trong DOM, được gọi là phần tử gốc. Trong khi dịch, bộ chọn có điều kiện sẽ được áp dụng cho phần tử thực này:

Trước
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
Sau
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

Thay vì được chuyển đổi thành #foo::before:where([cq-XYZ~="123"]) (không hợp lệ), bộ chọn có điều kiện sẽ được chuyển đến cuối phần tử ban đầu #foo.

Tuy nhiên, đó không phải là tất cả những gì cần thiết. Vùng chứa không được phép sửa đổi bất kỳ nội dung nào không nằm bên trong vùng chứa đó (và vùng chứa không được ở bên trong chính vùng chứa đó), nhưng hãy nhớ rằng đó chính xác là những gì sẽ xảy ra nếu #foo chính là phần tử vùng chứa được truy vấn. Thuộc tính #foo[cq-XYZ] sẽ bị thay đổi nhầm và mọi quy tắc #foo sẽ bị áp dụng sai.

Để khắc phục vấn đề này, trên thực tế, polyfill sử dụng hai thuộc tính: một thuộc tính chỉ có thể được thành phần mẹ áp dụng cho một phần tử và một thuộc tính là một phần tử có thể áp dụng cho chính phần tử đó. Thuộc tính sau được sử dụng cho các bộ chọn nhắm mục tiêu các phần tử giả.

Trước
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
Sau
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

Vì một vùng chứa sẽ không bao giờ áp dụng thuộc tính đầu tiên (cq-XYZ-A) cho chính nó, nên bộ chọn đầu tiên sẽ chỉ khớp nếu một vùng chứa mẹ khác đáp ứng các điều kiện về vùng chứa và áp dụng thuộc tính đó.

Đơn vị tương đối của vùng chứa

Truy vấn vùng chứa cũng đi kèm với một số đơn vị mới mà bạn có thể sử dụng trong CSS, chẳng hạn như cqwcqh cho 1% chiều rộng và chiều cao (tương ứng) của vùng chứa mẹ phù hợp nhất. Để hỗ trợ các giá trị này, đơn vị được chuyển đổi thành biểu thức calc(...) bằng cách sử dụng Thuộc tính tuỳ chỉnh của CSS. Polyfill sẽ đặt giá trị cho các thuộc tính này thông qua kiểu nội tuyến trên phần tử vùng chứa.

Trước
.card {
  width: 10cqw;
  height: 10cqh;
}
Sau
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

Ngoài ra còn có các đơn vị logic, như cqicqb cho kích thước cùng dòng và kích thước khối (tương ứng). Các ví dụ này phức tạp hơn một chút, vì trục cùng dòng và trục khối được xác định bằng writing-mode của phần tử sử dụng đơn vị, chứ không phải phần tử đang được truy vấn. Để hỗ trợ việc này, polyfill áp dụng kiểu cùng dòng cho bất kỳ phần tử nào có writing-mode khác với phần tử mẹ.

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

Giờ đây, bạn có thể chuyển đổi các đơn vị thành Thuộc tính tuỳ chỉnh CSS thích hợp như trước đây.

Thuộc tính

Truy vấn vùng chứa cũng thêm một số thuộc tính CSS mới như container-typecontainer-name. Vì không thể dùng các API như getComputedStyle(...) với thuộc tính không xác định hoặc không hợp lệ, nên các API này cũng được chuyển đổi thành Thuộc tính tuỳ chỉnh CSS sau khi được phân tích cú pháp. Nếu không thể phân tích cú pháp một tài sản (ví dụ: do tài sản chứa giá trị không hợp lệ hoặc không xác định), thì tài sản đó chỉ còn để trình duyệt xử lý.

Trước
.card {
  container-name: card-container;
  container-type: inline-size;
}
Sau
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

Các thuộc tính này được biến đổi mỗi khi được phát hiện, cho phép polyfill phát dễ dàng cùng các tính năng CSS khác như @supports. Chức năng này là cơ sở của các phương pháp hay nhất khi sử dụng polyfill, như được trình bày bên dưới.

Trước
@supports (container-type: inline-size) {
  /* ... */
}
Sau
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

Theo mặc định, các Thuộc tính tuỳ chỉnh của CSS sẽ được kế thừa, có nghĩa là mọi thành phần con của .card đều sẽ nhận giá trị của --cq-XYZ-container-name--cq-XYZ-container-type. Chắc chắn rằng các tài sản gốc không hoạt động như vậy. Để giải quyết vấn đề này, polyfill sẽ chèn quy tắc sau đây trước bất kỳ kiểu nào của người dùng, nhằm đảm bảo rằng mọi phần tử đều nhận được giá trị ban đầu, trừ phi một quy tắc khác cố ý ghi đè.

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

Các phương pháp hay nhất

Mặc dù dự kiến hầu hết khách truy cập sẽ chạy trình duyệt có hỗ trợ truy vấn vùng chứa tích hợp sớm hơn, nhưng điều quan trọng vẫn là mang lại trải nghiệm tốt cho những khách truy cập còn lại.

Trong quá trình tải ban đầu, có rất nhiều việc cần phải xảy ra trước khi polyfill có thể bố cục trang:

  • Cần tải và khởi chạy polyfill.
  • Biểu định kiểu cần được phân tích cú pháp và dịch. Vì không có bất kỳ API nào để truy cập nguồn thô của biểu định kiểu bên ngoài, nó có thể cần được tìm nạp lại không đồng bộ, mặc dù lý tưởng là chỉ từ bộ nhớ đệm của trình duyệt.

Nếu bạn không xử lý cẩn thận những mối lo ngại này bằng đoạn mã polyfill, thì Các chỉ số quan trọng chính của trang web có thể sẽ mất dữ liệu.

Để giúp bạn dễ dàng mang đến cho khách truy cập một trải nghiệm thú vị, polyfill được thiết kế để ưu tiên Độ trễ đầu vào đầu tiên (FID)Điểm số tổng hợp về mức thay đổi bố cục (CLS), có thể đánh đổi bằng Thời gian hiển thị nội dung lớn nhất (LCP). Cụ thể, polyfill không đảm bảo rằng các truy vấn về vùng chứa của bạn sẽ được đánh giá trước lần sơn đầu tiên. Điều này có nghĩa là để mang lại trải nghiệm tốt nhất cho người dùng, bạn phải đảm bảo rằng mọi nội dung có kích thước hoặc vị trí sẽ bị ảnh hưởng khi sử dụng truy vấn vùng chứa đều bị ẩn cho đến khi đoạn mã polyfill đã tải và chuyển mã CSS của bạn. Bạn có thể thực hiện việc này bằng cách sử dụng quy tắc @supports:

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

Bạn nên kết hợp biểu ngữ này với ảnh động tải CSS thuần tuý, được đặt hoàn toàn trên nội dung (ẩn) của bạn để cho khách truy cập biết rằng có điều gì đó đang xảy ra. Bạn có thể xem bản minh hoạ đầy đủ về phương pháp này tại đây.

Bạn nên sử dụng phương pháp này vì một số lý do:

  • Trình tải CSS thuần tuý giảm thiểu mức hao tổn cho những người dùng sử dụng trình duyệt mới, đồng thời cung cấp phản hồi đơn giản cho những người dùng sử dụng trình duyệt cũ và mạng chậm.
  • Bằng cách kết hợp vị trí tuyệt đối của trình tải với visibility: hidden, bạn sẽ tránh được tình trạng thay đổi bố cục.
  • Sau khi tệp polyfill tải, điều kiện @supports này sẽ ngừng truyền và nội dung của bạn sẽ hiển thị.
  • Trên các trình duyệt có hỗ trợ tích hợp cho các truy vấn vùng chứa, điều kiện sẽ không bao giờ vượt qua, và do đó trang sẽ được hiển thị trong lần hiển thị đầu tiên như mong đợi.

Kết luận

Nếu bạn muốn sử dụng truy vấn vùng chứa trên các trình duyệt cũ hơn, hãy dùng thử polyfill. Đừng ngần ngại báo cáo sự cố nếu bạn gặp phải bất kỳ sự cố nào.

Chúng tôi rất mong được chứng kiến và trải nghiệm những điều tuyệt vời mà bạn sẽ xây dựng bằng công cụ này.

Xác nhận

Hình ảnh chính của Dan Cristian Pădureř trên Unsplash.