Shadow DOM khai báo là một tính năng tiêu chuẩn của nền tảng web, được hỗ trợ trong Chrome từ phiên bản 90. Xin lưu ý rằng thông số kỹ thuật của 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 hoá mới nhất của tất cả các phần của tính năng này đã ra mắt trong Chrome phiên bản 124.
Shadow DOM là một trong ba tiêu chuẩn Thành phần web, cùng với mẫu HTML và Phần tử tuỳ chỉnh. Shadow DOM cung cấp một cách để xác định phạm vi của các kiểu CSS cho một cây con DOM cụ thể và tách biệt cây con đó với phần còn lại của tài liệu. Phần tử <slot>
cho phép chúng ta kiểm soát vị trí chèn các phần tử con của một Phần tử tuỳ chỉnh trong Cây bóng. Khi kết hợp, các tính năng này cho phép hệ thống tạo các thành phần độc lập, có thể sử dụng lại và tích hợp liền mạch vào các ứng dụng hiện có, giống như một phần tử HTML tích hợp.
Cho đến nay, cách duy nhất để sử dụng Shadow DOM là tạo một 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 kết xuất phía máy khách: cùng một mô-đun JavaScript xác định Phần tử tuỳ chỉnh của chúng ta cũng tạo Shadow Root và đặt nội dung của chúng. Tuy nhiên, nhiều ứng dụng web cần hiển thị nội dung phía máy chủ hoặc HTML tĩnh tại thời điểm tạo bản dựng. Đây có thể là một phần quan trọng để mang lại 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 sử dụng tính năng Kết xuất phía máy chủ (SSR) sẽ khác nhau tuỳ theo dự án. Một số trang web phải cung cấp HTML do máy chủ hiển thị đầy đủ chức năng để đáp ứng các nguyên tắc hỗ trợ tiếp cận, còn một số trang web khác chọn cung cấp trải nghiệm cơ sở không có JavaScript để đảm bảo hiệu suất tốt trên các thiết bị hoặc kết nối chậm.
Trước đây, việc sử dụng Shadow DOM kết hợp với tính năng Hiển thị phía máy chủ rất khó khăn vì không có cách tích hợp sẵn để thể hiện Shadow Root trong HTML do máy chủ tạo. Việc đính kèm Shadow Root vào các phần tử DOM đã hiển thị mà không có Shadow Root cũng có thể ảnh hưởng đến hiệu suất. Đ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ị nội dung chưa được định kiểu ("FOUC") trong khi tải các tệp kiểu của Shadow Root.
Shadow DOM khai báo (DSD) sẽ loại bỏ giới hạn này, đưa Shadow DOM đến máy chủ.
Cách tạo một gốc bóng đổ khai báo
Gốc bóng đổ 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 sẽ phát hiện một phần tử mẫu có thuộc tính shadowrootmode
và áp dụng ngay phần tử đó 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ừ kết quả 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ẫu mã này tuân theo các quy ước của bảng điều khiển Elements (Thành phần) trong Chrome DevTools để hiển thị nội dung Shadow DOM. Ví dụ: ký tự ↳
đại diện cho nội dung Light DOM được đặt trong khe.
Điều này mang lại cho chúng ta các lợi ích của tính năng đóng gói và chiếu khe của Shadow DOM trong HTML tĩnh. Bạn không cần JavaScript để tạo toàn bộ cây, bao gồm cả Shadow Root.
Dữ liệu về lượng nước uống của thành phần
Bạn có thể sử dụng Shadow DOM khai báo riêng lẻ để đóng gói các kiểu hoặc tuỳ chỉnh vị trí con, nhưng tính năng này mạnh mẽ nhất khi được sử dụ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 việc ra mắt DOM tối 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 một Gốc bóng đổ khai báo sẽ được đính kèm 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 được tạo bản sao mà không cần mã của bạn tạo một thuộc tính. Tốt nhất là bạn nên kiểm tra this.shadowRoot
để tìm bất kỳ gốc bóng nào hiện có trong hàm khởi tạo của phần tử. Nếu đã có giá trị, HTML cho thành phần này sẽ bao gồm một Gốc bóng đổ khai báo. Nếu giá trị này là rỗng, thì không có Gốc bóng đổ khai báo nào trong HTML hoặc trình duyệt không hỗ trợ DOM bóng đổ khai báo.
<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>
Các phần tử tuỳ chỉnh đã xuất hiện được một thời gian và cho đến nay, không có lý do gì để kiểm tra xem có gốc bóng hiện có hay không trước khi tạo gốc bóng bằng attachShadow()
. Mặc dù vậy, Shadow DOM khai báo có một thay đổi nhỏ cho phép các thành phần hiện có hoạt động: việc gọi phương thức attachShadow()
trên một phần tử có Gốc bóng Khai báo hiện có sẽ không gửi lỗi. Thay vào đó, Gốc bóng khai báo sẽ được làm trống và trả về. Điều này cho phép các thành phần cũ không được tạo cho DOM bóng đổ khai báo tiếp tục hoạt động, vì các gốc khai báo được giữ nguyên cho đến khi tạo một thành phần thay thế bắt buộc.
Đối với các 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 rõ ràng để tham chiếu đến phần tử gốc bóng đổ khai báo hiện có của phần tử, cả mở và đóng. Bạn có thể dùng phương thức này để kiểm tra và sử dụng bất kỳ Shadow Root khai báo nào, đồng thời vẫn quay lại attachShadow()
trong trường hợp không có Shadow Root nào được cung cấp.
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 đổ cho mỗi gốc
Gốc bóng khi 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 kết. Quyết định thiết kế này đảm bảo rằng các gốc bóng có thể truyền trực tuyến 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 tạo và tạo bản sao vì việc thêm một gốc bóng vào một phần tử không yêu cầu duy trì sổ đăng ký của các gốc bóng hiện có.
Điểm đánh đổi khi liên kết gốc bóng với phần tử mẹ là không thể khởi chạy 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 có thể không quan trọng trong hầu hết các trường hợp sử dụng Shadow DOM khai báo, vì nội dung của mỗi gốc bóng hiếm 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 các cấu trúc này thường khác nhau – ví dụ: có sự khác biệt nhỏ về văn bản hoặc thuộc tính. Vì nội dung của một Gốc bóng đổ khai báo được chuyển đổi tuần tự hoàn toàn tĩnh, nên việc nâng cấp nhiều phần tử từ một Gốc bóng đổ khai báo 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 thư mục gốc bóng tương tự lặp lại đối với kích thước chuyển dữ liệu mạng tương đối nhỏ do hiệu ứng nén.
Trong tương lai, bạn có thể xem lại các gốc bóng được chia sẻ. Nếu DOM có hỗ trợ tạo mẫu tích hợp, thì Gốc bóng đổ khai báo có thể được coi là các mẫu được tạo bản sao để 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.
Phát trực tuyến thật tuyệt
Việc liên kết trực tiếp phần 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à đính kèm các phần tử đó vào phần tử mẹ. Phát hiện phần gốc bóng đổ khai báo trong quá trình phân tích cú pháp HTML và đính kèm ngay khi gặp thẻ <template>
mở. HTML được 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, vì vậy, HTML có thể được "truyền trực tuyến": hiển thị 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ỉ trình phân tích cú pháp
Shadow DOM khai báo là một tính năng của trình phân tích cú pháp HTML. Điều này có nghĩa là Gốc bóng khi 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, bạn có thể tạo Gốc bóng phản chiếu khai báo 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ố vấn đề bảo mật quan trọng, bạn cũng không thể tạo Gốc bóng phản chiếu khai báo bằng cách sử dụ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 phần 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ị phía máy chủ có kiểu
Các tệp định kiểu nội tuyến và bên ngoài được hỗ trợ đầy đủ bên trong phần gốc bóng đổ khai báo bằng cách sử dụng các thẻ <style>
và <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 trang kiểu xuất hiện trong nhiều Gốc bóng phản chiếu khai báo, thì trang 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 được chia sẻ bởi tất cả các gốc bóng, loại bỏ hao tổn bộ nhớ trùng lặp.
Trang kiểu có thể tạo không được hỗ trợ trong Shadow DOM 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 kiểu trang có thể tạo trong HTML và không có cách nào để tham chiếu đến các kiểu trang đó khi điền adoptedStyleSheets
.
Cách tránh hiện nội dung chưa được định kiểu
Một vấn đề tiềm ẩn trong các trình duyệt chưa hỗ trợ DOM tối khai báo là tránh "nội dung không theo kiểu" (FOUC), trong đó nội dung thô sẽ hiển thị cho các Phần tử tuỳ chỉnh chưa được nâng cấp. Trước khi 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 gốc bóng đổ. Bằng cách này, nội dung sẽ không hiển thị cho đến khi "sẵn sàng":
<style>
x-foo:not(:defined) > * {
display: none;
}
</style>
Với việc ra mắt DOM bóng đổ khai báo, các phần tử tuỳ chỉnh có thể được kết xuất hoặc tạo bằng HTML để nội dung bóng đổ của chúng được đặt đúng vị trí và sẵn sàng trước khi tải quá trình 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, quy tắc "FOUC" display:none
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ợ DOM tối khai báo hiển thị nội dung không chính xác hoặc không được định kiểu cho đến khi polyfill DOM tối khai báo tải và chuyển đổi mẫu gốc bóng thành gốc bóng thực.
Rất may, bạn có thể giải quyết vấn đề này 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>
sẽ được chuyển đổi ngay thành gốc bóng đổ, không để lại phần tử <template>
nào trong cây DOM. Những trình duyệt không hỗ trợ Shadow DOM khai báo sẽ giữ lại 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, quy tắc "FOUC" đã sửa đổi sẽ ẩn các phần tử con khi các phần tử đó tuân theo phần tử <template shadowrootmode>
. Sau khi bạn xác định Phần tử tuỳ chỉnh, quy tắc này sẽ không còn khớp nữa. Quy tắc này bị bỏ qua trong các trình duyệt hỗ trợ DOM bóng đổ khai báo vì phần tử con <template shadowrootmode>
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
Shadow DOM khai báo đã có từ Chrome 90 và Edge 91, nhưng sử dụng một thuộc tính không chuẩn cũ hơn có tên là shadowroot
thay vì thuộc tính shadowrootmode
đã chuẩn hoá. Thuộc tính shadowrootmode
và hành vi truyền trực tuyến mới hơn có trong Chrome 111 và Edge 111.
Là một API nền tảng web mới, Shadow DOM khai báo chưa được hỗ trợ rộng rãi trên tất cả trình duyệt. Bạn có thể phát hiện tính năng hỗ trợ trình duyệt bằng cách kiểm tra xem thuộc tính shadowRootMode
có tồn tại trên nguyên mẫu của HTMLTemplateElement
hay không:
function supportsDeclarativeShadowDOM() {
return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}
Polyfill
Việc tạo một polyfill đơn giản cho DOM tối khai báo tương đối đơn giản, vì polyfill không cần phải sao chép hoàn hảo ngữ nghĩa về thời gian hoặc các đặc điểm chỉ dành cho trình phân tích cú pháp mà việc triển khai trình duyệt quan tâm đến. Để polyfill DOM bóng đổ khai báo, chúng ta có thể quét DOM để tìm tất cả phần tử <template shadowrootmode>
, sau đó chuyển đổi các phần tử đó thành Shadow Root đính kèm trên phần tử mẹ. Bạn có thể thực hiện quy trình này sau 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);