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 của phần tử mẹ) để tạo kiểu cho phần tử con. Gần đây, chúng tôi đã phát hành một bản cập nhật lớn cho polyfill, trùng với thời điểm ra mắt tính năng hỗ trợ trong trình duyệt.

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

Tìm hiểu sâu

Dịch thuật

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

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

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

Trước truy vấn vùng chứa, CSS không có cách nào để tác giả tự ý bật hoặc tắt các nhóm quy tắc. Để polyfill hành vi này, các quy tắc bên trong một truy vấn vùng chứa cũng cần được chuyển đổi. Mỗi @container được cấp mã nhận dạng duy nhất (ví dụ: 123) dùng để biến đổi từng bộ chọn sao cho bộ chọn chỉ được á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 đặt bởi polyfill trong thời gian chạy.

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

Hãy lưu ý đến 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 tính cụ thể của bộ chọn đó. Với lớp giả, bạn có thể áp dụng điều kiện bổ sung trong khi vẫn giữ nguyên tính đặc trưng ban đầu. Để biết tại sao điều này 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, phần tử có lớp .card phải luôn có color: red, vì quy tắc sau 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 dịch mã 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 tính đặc trưng này và khiến color: blue bị áp dụng không chính xác.

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

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, bạn có thể thấy rõ sự khác biệt về mức độ cụ thể. Thông tin này cũng đóng vai trò là tài liệu để bạn biết nội dung nào bị ảnh hưởng khi bạn không cần hỗ trợ giải pháp thay thế hoặc polyfill nữa.
  • Đặc điểm cụ thể của các quy tắc sẽ luôn giống nhau, vì polyfill là gì?

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

Yếu tố giả

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

Phần tử giả luôn được liên kết với một phần tử thực trong DOM, được gọi là phần tử gốc. Trong quá trình 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ì chuyển đổi thành #foo::before:where([cq-XYZ~="123"]) (giá trị này không hợp lệ), bộ chọn có điều kiện đượ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 chứa bên trong nó (và vùng chứa không thể ở bên trong chính nó), nhưng hãy xem xét chính xác điều đó 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 không chính xác và mọi quy tắc #foo đều sẽ bị áp dụng nhầm.

Để khắc phục vấn đề này, polyfill thực sự sử dụng hai thuộc tính: một thuộc tính chỉ có thể áp dụng cho một phần tử do phần tử mẹ có thể áp dụng và một thuộc tính mà một phần tử có thể áp dụng cho chính nó. 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ì 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 vùng chứa đó, nên bộ chọn đầu tiên sẽ chỉ khớp nếu vùng chứa gốc khác đáp ứng các điều kiện của 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 vài đơn vị mới mà bạn có thể sử dụng trong CSS của mình, 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 gốc thích 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. Thẻ polyfill sẽ đặt giá trị cho các thuộc tính này thông qua kiểu nội dòng 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 phép tính 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ởi writing-mode của phần tử sử dụng đơn vị, chứ không phải phần tử được truy vấn. Để hỗ trợ việc này, polyfill sẽ áp dụng một kiểu cùng dòng cho mọi phần tử 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 giống 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ì bạn không thể sử dụng các API như getComputedStyle(...) với các 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 của CSS sau khi được phân tích cú pháp. Nếu hệ thống không thể phân tích cú pháp một thuộc tính (ví dụ: do thuộc tính đó chứa giá trị không hợp lệ hoặc không xác định), thì trình duyệt sẽ tự xử lý thông tin đó.

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 bất cứ khi nào chúng được phát hiện, cho phép polyfill hoạt động hiệu quả với 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 để sử dụng polyfill, như trình bày dưới đây.

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

* {
  --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 thay vì sau đó, cung cấp cho khách truy cập còn lại trải nghiệm tốt.

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

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

Nếu bạn không giải quyết những mối lo ngại này một cách cẩn thận bằng chiến dịch polyfill, thì chỉ số Các chỉ số quan trọng chính của trang web có thể sẽ giảm tải.

Để giúp bạn dễ dàng mang đến trải nghiệm thoải mái cho khách truy cập, thẻ polyfill được thiết kế để ưu tiên Thời gian phản hồi lần tương tác đầu tiên (FID)Điểm số tổng hợp về mức thay đổi bố cục (CLS), có khả năng sẽ gây ra tổn hại về Thời gian hiển thị nội dung lớn nhất (LCP). Cụ thể, polyfill này không đảm bảo rằng các truy vấn 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à để có trải nghiệm người dùng tốt nhất, 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 do việc sử dụng các truy vấn vùng chứa đều bị ẩn cho đến khi polyfill đã được 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 ảnh động này với một ảnh động tải CSS thuần tuý, nằm ở vị trí tuyệt đối phía trên nội dung (ẩn) của bạn, để cho khách truy cập biết rằng đã có chuyện 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.

Phương pháp này được đề xuất vì một số lý do:

  • Trình tải CSS thuần tuý giúp giảm thiểu chi phí cho người dùng sử dụng trình duyệt mới, trong khi vẫn cung cấp ý kiến phản hồi ngắn gọ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 có thể tránh được tình trạng thay đổi bố cục.
  • Sau khi polyfill được 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ó tính năng hỗ trợ tích hợp sẵn cho truy vấn vùng chứa, điều kiện sẽ không bao giờ vượt qua, và vì vậy trang sẽ hiển thị trong nội dung hiển thị đầu tiên như dự kiến.

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 gửi vấn đề nếu bạn gặp bất kỳ vấn đề nào.

Chúng tôi rất nóng lòng được chiêm ngưỡng và trải nghiệm những điều tuyệt vời mà bạn sẽ tạo dựng bằng công cụ này.

Xác nhận

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