DOM bóng khai báo

DOM bóng khai báo là một tính năng nền tảng web tiêu chuẩn, đã được hỗ trợ trong Chrome từ phiên bản 90. Xin lưu ý rằng thông số kỹ thuật cho tính năng này đã thay đổi vào năm 2023 (bao gồm cả việc đổi tên shadowroot thành shadowrootmode) và các phiên bản chuẩn mới nhất của tất cả các phần của tính năng này đều đã có trong Chrome phiên bản 124.

Hỗ trợ trình duyệt

  • Chrome: 111.
  • Cạnh: 111.
  • Firefox: 123.
  • Safari: 16.4.

Nguồn

Shadow DOM là một trong 3 tiêu chuẩn của Thành phần web, được tổng hợp thành mẫu HTMLPhần tử tuỳ chỉnh. DOM tối cung cấp một cách xác định phạm vi các kiểu CSS cho một cây con DOM cụ thể và tách riêng cây con đó với phần còn lại của tài liệu. Phần tử <slot> cung cấp cho chúng ta một cách để kiểm soát vị trí chèn thành phần con của một Phần tử tuỳ chỉnh trong Cây bóng. Những tính năng này kết hợp lại cho phép một hệ thống xây dựng các thành phần độc lập, có thể tái sử dụng và tích hợp liền mạch vào các ứng dụng hiện có giống như phần tử HTML tích hợp sẵn.

Cho đến nay, cách duy nhất để sử dụng Shadow DOM là xây dựng gốc bóng đổ bằng JavaScript:

const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';

Một API bắt buộc như thế này hoạt động tốt cho việc hiển thị phía máy khách: các mô-đun JavaScript tương tự xác định Phần tử tuỳ chỉnh của chúng ta cũng tạo Shadow Roots và thiết lập nội dung. Tuy nhiên, nhiều ứng dụng web cần kết xuất nội dung phía máy chủ hoặc HTML tĩnh tại thời gian xây dựng. Đây có thể là một phần quan trọng trong việc cung cấp trải nghiệm hợp lý cho những khách truy cập có thể không chạy được JavaScript.

Lý do cho việc Hiển thị phía máy chủ (SSR) sẽ khác nhau theo từng dự án. Để đáp ứng các nguyên tắc về hỗ trợ tiếp cận, một số trang web phải cung cấp HTML do máy chủ hiển thị với đầy đủ chức năng. Trong khi đó, một số trang web khác chọn cung cấp trải nghiệm cơ sở không có JavaScript như một cách để đảm bảo hiệu suất tốt trên các kết nối hoặc thiết bị chậm.

Trước đây, rất khó để sử dụng Shadow DOM cùng với Kết xuất phía máy chủ vì không có cách tích hợp nào để thể hiện Shadow Roots trong HTML do máy chủ tạo. Ngoài ra còn có các hệ quả về hiệu suất khi đính kèm Shadow Root vào các phần tử DOM đã hiển thị mà không có các phần tử này. Điều này có thể khiến bố cục thay đổi sau khi trang tải hoặc tạm thời hiển thị flash của nội dung chưa định kiểu ("FOUC") trong khi tải biểu định kiểu của Shadow Root.

Declarative Shadow DOM (DSD) loại bỏ giới hạn này, đưa Shadow DOM đến máy chủ.

Cách tạo Root (Gốc) khai báo

Gốc khai báo là một phần tử <template> có thuộc tính shadowrootmode:

<host-element>
  <template shadowrootmode="open">
    <slot></slot>
  </template>
  <h2>Light content</h2>
</host-element>

Trình phân tích cú pháp HTML phát hiện một phần tử mẫu có thuộc tính shadowrootmode và ngay lập tức được áp dụng làm gốc bóng của phần tử mẹ. Việc tải mã đánh dấu HTML thuần tuý từ các mẫu ở trên sẽ dẫn đến cây DOM sau:

<host-element>
  #shadow-root (open)
  <slot>
    ↳
    <h2>Light content</h2>
  </slot>
</host-element>

Mã mẫu này tuân theo các quy ước của bảng điều khiển Phần tử Chrome Công cụ cho nhà phát triển khi hiển thị nội dung DOM bóng. Ví dụ: ký tự đại diện cho nội dung DOM sáng có rãnh.

Điều này mang lại cho chúng tôi những lợi ích của việc đóng gói và chiếu vùng của Shadow DOM trong HTML tĩnh. Không cần JavaScript để tạo toàn bộ cây, bao gồm cả Shadow Root.

Uống nước thành phần

DOM bóng khai báo có thể được sử dụng riêng như một cách để gói các kiểu hoặc tuỳ chỉnh vị trí con, nhưng cách này hiệu quả nhất khi được sử dụng cùng với Phần tử tuỳ chỉnh. Các thành phần được tạo bằng Phần tử tuỳ chỉnh sẽ tự động được nâng cấp từ HTML tĩnh. Với sự ra mắt của DOM bóng khai báo, giờ đây Phần tử tuỳ chỉnh có thể có gốc bóng trước khi được nâng cấp.

Một Phần tử tuỳ chỉnh đang được nâng cấp từ HTML có chứa Gốc khai báo sẽ có sẵn gốc đổ bóng đó. Điều này có nghĩa là phần tử sẽ có sẵn thuộc tính shadowRoot khi tạo thực thể mà không cần mã của bạn tạo rõ ràng một thuộc tính. Tốt nhất là bạn nên kiểm tra this.shadowRoot để tìm gốc đổ bóng hiện có trong hàm khởi tạo của phần tử. Nếu đã có giá trị thì HTML cho thành phần này sẽ bao gồm cả Dọc gốc khai báo (Khai báo). Nếu giá trị là rỗng, tức là không có Khai báo Shadow Root trong HTML hoặc trình duyệt không hỗ trợ Declarative Shadow DOM.

<menu-toggle>
  <template shadowrootmode="open">
    <button>
      <slot></slot>
    </button>
  </template>
  Open Menu
</menu-toggle>
<script>
  class MenuToggle extends HTMLElement {
    constructor() {
      super();

      // Detect whether we have SSR content already:
      if (this.shadowRoot) {
        // A Declarative Shadow Root exists!
        // wire up event listeners, references, etc.:
        const button = this.shadowRoot.firstElementChild;
        button.addEventListener('click', toggle);
      } else {
        // A Declarative Shadow Root doesn't exist.
        // Create a new shadow root and populate it:
        const shadow = this.attachShadow({mode: 'open'});
        shadow.innerHTML = `<button><slot></slot></button>`;
        shadow.firstChild.addEventListener('click', toggle);
      }
    }
  }

  customElements.define('menu-toggle', MenuToggle);
</script>

Phần tử tuỳ chỉnh đã xuất hiện được một thời gian và cho đến bây giờ, không có lý do gì để kiểm tra một gốc bóng (shadow) hiện có trước khi tạo một thành phần bằng attachShadow(). DOM bóng khai báo bao gồm một thay đổi nhỏ cho phép các thành phần hiện có hoạt động bất chấp điều này: gọi phương thức attachShadow() trên một phần tử có Gốc Khai báo hiện có sẽ không tạo ra lỗi. Thay vào đó, Gốc đổ bóng khai báo sẽ được làm trống và được trả về. Điều này cho phép các thành phần cũ không được tạo cho DOM tối khai báo tiếp tục hoạt động, vì gốc khai báo được bảo toàn cho đến khi cần tạo một thay thế bắt buộc.

Đối với Phần tử tuỳ chỉnh mới tạo, thuộc tính ElementInternals.shadowRoot mới cung cấp một cách thức rõ ràng để tham chiếu đến Gốc khai báo hiện có của một phần tử, cả mở và đóng. Bạn có thể dùng tính năng này để kiểm tra và sử dụng bất kỳ Gốc khai báo nào, đồng thời vẫn quay lại dùng attachShadow() trong trường hợp không cung cấp thuộc tính gốc.

class MenuToggle extends HTMLElement {
  constructor() {
    super();

    const internals = this.attachInternals();

    // check for a Declarative Shadow Root:
    let shadow = internals.shadowRoot;

    if (!shadow) {
      // there wasn't one. create a new Shadow Root:
      shadow = this.attachShadow({
        mode: 'open'
      });
      shadow.innerHTML = `<button><slot></slot></button>`;
    }

    // in either case, wire up our event listener:
    shadow.firstChild.addEventListener('click', toggle);
  }
}

customElements.define('menu-toggle', MenuToggle);

Một bóng trên mỗi gốc

Gốc khai báo chỉ liên kết với phần tử mẹ. Điều này có nghĩa là gốc bóng đổ luôn được đặt cùng với phần tử liên quan. Quyết định thiết kế này đảm bảo gốc bóng có thể truyền trực tuyến được như phần còn lại của tài liệu HTML. Việc này cũng thuận tiện cho việc biên soạn và tạo, vì việc thêm gốc bóng vào một phần tử không yêu cầu duy trì sổ đăng ký các gốc bóng hiện có.

Sự đánh đổi của việc liên kết gốc đổ bóng với phần tử mẹ là không thể khởi tạo nhiều phần tử từ cùng một gốc bóng khai báo <template>. Tuy nhiên, điều này khó có thể xảy ra trong hầu hết các trường hợp sử dụng DOM bóng khai báo, vì nội dung của mỗi gốc bóng ít khi giống nhau. Mặc dù HTML do máy chủ hiển thị thường chứa các cấu trúc phần tử lặp lại, nhưng nội dung của chúng thường khác nhau (ví dụ: văn bản hoặc thuộc tính có chút khác biệt). Vì nội dung của một Gốc khai báo tuần tự hoàn toàn là tĩnh, nên việc nâng cấp nhiều phần tử từ một Gốc khai báo duy nhất sẽ chỉ hoạt động nếu các phần tử đó giống hệt nhau. Cuối cùng, tác động của các gốc đổ bóng tương tự lặp lại đối với kích thước truyền mạng là tương đối nhỏ do ảnh hưởng của quá trình nén.

Trong tương lai, bạn có thể truy cập lại vào gốc bóng đổ chung. Nếu DOM được hỗ trợ cho việc tạo mẫu tích hợp sẵn, thì Gốc đổ bóng khai báo có thể được coi là các mẫu được tạo thực thể để tạo gốc đổ bóng cho một phần tử nhất định. Thiết kế DOM tối khai báo hiện tại cho phép khả năng này tồn tại trong tương lai bằng cách giới hạn liên kết gốc bóng với một phần tử duy nhất.

Thật thú vị khi phát trực tuyến

Việc liên kết trực tiếp Gốc đổ bóng khai báo với phần tử mẹ giúp đơn giản hoá quá trình nâng cấp và gắn chúng vào phần tử đó. Gốc bóng khai báo được phát hiện trong quá trình phân tích cú pháp HTML và được đính kèm ngay khi gặp thẻ <template> mở. HTML đã phân tích cú pháp trong <template> được phân tích cú pháp trực tiếp vào gốc bóng đổ, do đó có thể được "streamed": kết xuất khi nhận được.

<div id="el">
  <script>
    el.shadowRoot; // null
  </script>

  <template shadowrootmode="open">
    <!-- shadow realm -->
  </template>

  <script>
    el.shadowRoot; // ShadowRoot
  </script>
</div>

Chỉ có trình phân tích cú pháp

DOM bóng khai báo là một tính năng của trình phân tích cú pháp HTML. Tức là Gốc bóng khai báo sẽ chỉ được phân tích cú pháp và đính kèm cho các thẻ <template> có thuộc tính shadowrootmode xuất hiện trong quá trình phân tích cú pháp HTML. Nói cách khác, Gốc đổ bóng khai báo có thể được xây dựng trong quá trình phân tích cú pháp HTML ban đầu:

<some-element>
  <template shadowrootmode="open">
    shadow root content for some-element
  </template>
</some-element>

Việc đặt thuộc tính shadowrootmode của phần tử <template> sẽ không có tác dụng gì và mẫu vẫn là một phần tử mẫu thông thường:

const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null

Để tránh một số cân nhắc quan trọng về bảo mật, bạn cũng không thể tạo Gốc đổ bóng khai báo bằng các API phân tích cú pháp mảnh như innerHTML hoặc insertAdjacentHTML(). Cách duy nhất để phân tích cú pháp HTML có áp dụng Gốc bóng khai báo là sử dụng setHTMLUnsafe() hoặc parseHTMLUnsafe():

<script>
  const html = `
    <div>
      <template shadowrootmode="open"></template>
    </div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; // No shadow root here
  div.setHTMLUnsafe(html); // Shadow roots included
  const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>

Hiển thị trên máy chủ theo kiểu

Biểu định kiểu nội tuyến và bên ngoài được hỗ trợ đầy đủ trong Gốc khai báo gốc bằng cách sử dụng thẻ <style><link> tiêu chuẩn:

<nineties-button>
  <template shadowrootmode="open">
    <style>
      button {
        color: seagreen;
      }
    </style>
    <link rel="stylesheet" href="/comicsans.css" />
    <button>
      <slot></slot>
    </button>
  </template>
  I'm Blue
</nineties-button>

Các kiểu được chỉ định theo cách này cũng được tối ưu hoá cao: nếu cùng một biểu định kiểu hiện diện trong nhiều gốc bóng khai báo, thì biểu định kiểu đó chỉ được tải và phân tích cú pháp một lần. Trình duyệt sử dụng một CSSStyleSheet sao lưu duy nhất được chia sẻ bởi tất cả gốc bóng, giúp giảm mức hao tổn bộ nhớ trùng lặp.

Biểu định kiểu có thể tạo không được hỗ trợ trong DOM bóng khai báo. Lý do là hiện tại không có cách nào để chuyển đổi tuần tự các biểu định kiểu có thể tạo trong HTML, cũng như không có cách nào để tham chiếu đến các biểu định đó khi điền adoptedStyleSheets.

Cách tránh đăng tải nội dung không theo kiểu

Một vấn đề có thể xảy ra trong các trình duyệt chưa hỗ trợ Khai báo Shadow DOM là tránh tình trạng "flash của nội dung không được định kiểu" (FOUC), nơi nội dung thô được hiển thị cho các Phần tử tuỳ chỉnh chưa được nâng cấp. Trước DOM bóng khai báo, một kỹ thuật phổ biến để tránh FOUC là áp dụng quy tắc kiểu display:none cho các Phần tử tuỳ chỉnh chưa được tải, vì các phần tử này chưa được đính kèm và điền sẵn vào gốc bóng. Theo đó, nội dung sẽ không xuất hiện cho đến khi "sẵn sàng":

<style>
  x-foo:not(:defined) > * {
    display: none;
  }
</style>

Với sự ra mắt của DOM bóng khai báo, các Phần tử tuỳ chỉnh có thể được hiển thị hoặc chỉnh sửa trong HTML sao cho nội dung bóng của chúng được đặt đúng chỗ và sẵn sàng trước khi tải phương thức triển khai thành phần phía máy khách:

<x-foo>
  <template shadowrootmode="open">
    <style>h2 { color: blue; }</style>
    <h2>shadow content</h2>
  </template>
</x-foo>

Trong trường hợp này, display:none "FOUC" sẽ ngăn nội dung của gốc bóng khai báo hiển thị. Tuy nhiên, việc xoá quy tắc đó sẽ khiến các trình duyệt không hỗ trợ Khai báo Shadow DOM hiển thị nội dung không chính xác hoặc chưa định kiểu cho đến khi polyfill trong Khai báo Shadow DOM tải và chuyển đổi mẫu gốc bóng thành một gốc bóng đổ thực.

Rất may là việc này có thể được giải quyết trong CSS bằng cách sửa đổi quy tắc kiểu FOUC. Trong các trình duyệt hỗ trợ DOM bóng khai báo, phần tử <template shadowrootmode> ngay lập tức được chuyển đổi thành gốc bóng, không để lại phần tử <template> trong cây DOM. Những trình duyệt không hỗ trợ DOM tối khai báo giữ nguyên phần tử <template> mà chúng ta có thể dùng để ngăn chặn FOUC:

<style>
  x-foo:not(:defined) > template[shadowrootmode] ~ *  {
    display: none;
  }
</style>

Thay vì ẩn Phần tử tuỳ chỉnh chưa được xác định, "FOUC" đã sửa đổi quy tắc sẽ ẩn phần tử con khi chúng tuân theo phần tử <template shadowrootmode>. Khi Phần tử tùy chỉnh được xác định, quy tắc không còn phù hợp nữa. Quy tắc này sẽ bị bỏ qua trong các trình duyệt hỗ trợ DOM tối khai báo vì thành phần con <template shadowrootmode> sẽ bị xoá trong quá trình phân tích cú pháp HTML.

Phát hiện tính năng và hỗ trợ trình duyệt

DOM bóng khai báo đã có từ phiên bản Chrome 90 và Edge 91, nhưng nó sử dụng một thuộc tính cũ không theo chuẩn có tên là shadowroot thay vì thuộc tính shadowrootmode đã chuẩn hoá. Thuộc tính shadowrootmode mới hơn và hành vi truyền trực tuyến có trong Chrome 111 và Edge 111.

Là một API nền tảng web mới, Declarative Shadow DOM chưa được hỗ trợ rộng rãi trên tất cả các trình duyệt. Bạn có thể phát hiện sự hỗ trợ của trình duyệt bằng cách kiểm tra xem có thuộc tính shadowRootMode trên nguyên mẫu của HTMLTemplateElement hay không:

function supportsDeclarativeShadowDOM() {
  return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}

Ống polyfill

Việc xây dựng một polyfill đơn giản cho DOM bóng khai báo tương đối đơn giản, vì polyfill không cần phải tái tạo hoàn hảo ngữ nghĩa thời gian hoặc các đặc điểm chỉ dành cho trình phân tích cú pháp mà quá trình triển khai trình duyệt gặp phải. Để tạo polyfill Declarative Shadow DOM, chúng ta có thể quét DOM để tìm tất cả các phần tử <template shadowrootmode>, sau đó chuyển đổi các phần tử đó thành Shadow Roots đính kèm trên phần tử mẹ. Quá trình này có thể được thực hiện khi tài liệu đã sẵn sàng hoặc được kích hoạt bởi các sự kiện cụ thể hơn như vòng đời của Phần tử tuỳ chỉnh.

(function attachShadowRoots(root) {
  root.querySelectorAll("template[shadowrootmode]").forEach(template => {
    const mode = template.getAttribute("shadowrootmode");
    const shadowRoot = template.parentNode.attachShadow({ mode });

    shadowRoot.appendChild(template.content);
    template.remove();
    attachShadowRoots(shadowRoot);
  });
})(document);

Tài liệu đọc thêm