Shadow DOM phiên bản 1 – Thành phần web tự chứa

Shadow DOM cho phép các nhà phát triển web tạo DOM và CSS phân vùng cho các thành phần web

Tóm tắt

Shadow DOM loại bỏ sự phiền hà của việc xây dựng ứng dụng web. Độ giòn đến từ bản chất chung của HTML, CSS và JS. Trong những năm qua, chúng tôi đã sáng chế ra số cắt cổ / công cụ để tránh né các vấn đề. Ví dụ: khi bạn sử dụng id/lớp HTML mới, thì không có dấu hiệu cho biết tên đó có xung đột với tên hiện có mà trang đó sử dụng hay không. Các lỗi nhỏ ngày càng xuất hiện, Tính cụ thể của CSS trở thành một vấn đề lớn (!important tất cả mọi thứ!), kiểu các bộ chọn vượt quá tầm kiểm soát và hiệu suất có thể bị ảnh hưởng. Danh sách tiếp tục.

Shadow DOM khắc phục CSS và DOM. Hướng dẫn này giới thiệu các kiểu có phạm vi cho web chủ. Nếu không có công cụ hoặc quy ước đặt tên, bạn có thể gói CSS với đánh dấu, ẩn chi tiết triển khai và tác giả độc lập các thành phần trong JavaScript vanilla.

Giới thiệu

Shadow DOM là một trong ba tiêu chuẩn của Thành phần web: Mẫu HTML, DOM tốiPhần tử tuỳ chỉnh. Nhập HTML từng là một phần của danh sách nhưng hiện được xem xét không dùng nữa.

Bạn không phải tạo các thành phần web sử dụng DOM bóng. Nhưng khi bạn thực hiện, bạn tận dụng các lợi ích của nó (xác định phạm vi CSS, đóng gói DOM, cấu trúc) và tạo bản dựng có thể tái sử dụng phần tử tuỳ chỉnh, bền bỉ, định cấu hình cao và cực kỳ dễ tái sử dụng. Nếu tuỳ chỉnh là cách để tạo HTML mới (với API JS), DOM bóng là bạn cung cấp HTML và CSS. Hai API kết hợp với nhau để tạo thành một thành phần có HTML, CSS và JavaScript độc lập.

Shadow DOM được thiết kế như một công cụ để tạo các ứng dụng dựa trên thành phần. Do đó, nó mang đến giải pháp cho các vấn đề phổ biến trong phát triển web:

  • DOM tách biệt: DOM của một thành phần là độc lập (ví dụ: document.querySelector() sẽ không trả về các nút trong DOM tối của thành phần).
  • CSS có giới hạn: CSS được xác định bên trong DOM bóng cũng thuộc phạm vi của CSS đó. Quy tắc về kiểu không bị rò rỉ và kiểu trang không bị tràn vào.
  • Cấu trúc: Thiết kế một API khai báo dựa trên mã đánh dấu cho thành phần của bạn.
  • Đơn giản hoá CSS – DOM có phạm vi nghĩa là bạn có thể sử dụng các bộ chọn CSS đơn giản, tên id/lớp chung và không phải lo lắng về xung đột khi đặt tên.
  • Năng suất – Hãy nghĩ về các ứng dụng trong các phần DOM thay vì một phần lớn (toàn cầu).

Bản minh hoạ fancy-tabs

Trong bài viết này, tôi sẽ đề cập đến thành phần minh hoạ (<fancy-tabs>) và tham chiếu các đoạn mã từ đó. Nếu trình duyệt của bạn hỗ trợ các API này, bạn bạn sẽ thấy bản minh hoạ trực tiếp ngay bên dưới. Nếu không, hãy xem nguồn đầy đủ trên GitHub.

Xem nguồn trên GitHub

DOM bóng là gì?

Nền trên DOM

HTML hỗ trợ web vì nó dễ làm việc. Bằng cách khai báo một vài thẻ, bạn có thể tạo một trang trong vài giây có cả cách trình bày và cấu trúc. Tuy nhiên, thực ra HTML không hề hữu ích. Mọi người rất dễ hiểu một văn bản dựa trên ngôn ngữ, nhưng công nghệ học máy cần thứ gì đó khác. Nhập đối tượng tài liệu Mô hình hoặc DOM.

Khi tải một trang web, trình duyệt sẽ thực hiện nhiều thao tác thú vị. Một trong số những tính năng của công cụ này là chuyển đổi HTML của tác giả thành một tài liệu hoạt động. Về cơ bản, để hiểu được cấu trúc của trang, trình duyệt phân tích cú pháp HTML (tệp tĩnh chuỗi văn bản) vào mô hình dữ liệu (đối tượng/nút). Trình duyệt sẽ giữ lại Hệ thống phân cấp của HTML bằng cách tạo cây các nút sau: DOM. Điều thú vị về DOM là vì nó đại diện trực tiếp cho trang của bạn. Không giống như mô hình tĩnh HTML chúng tôi tác giả, các nút do trình duyệt tạo chứa các thuộc tính, phương thức và nhất... có thể bị thao túng bởi các chương trình! Đó là lý do chúng tôi có thể tạo DOM trực tiếp bằng cách sử dụng JavaScript:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

sẽ tạo ra mã đánh dấu HTML sau:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

Mọi thứ đều ổn. Sau đó DOM tối là gì?

DOM... trong bóng tối

DOM bóng là DOM thông thường với hai điểm khác biệt: 1) cách nó được tạo ra/sử dụng và 2) cách công cụ hoạt động trong mối quan hệ với phần còn lại của trang. Thông thường, bạn tạo DOM các nút và thêm chúng dưới dạng phần tử con của một phần tử khác. Với DOM bóng, bạn tạo cây DOM có phạm vi được đính kèm vào phần tử, nhưng tách biệt với trẻ em thực sự. Cây con trong phạm vi này được gọi là cây bóng (shadow). Phần tử nó được đính kèm là máy chủ bóng. Mọi thứ bạn thêm vào bóng sẽ trở thành cục bộ cho phần tử lưu trữ, bao gồm <style>. Đây là cách DOM tối đạt được phạm vi kiểu CSS.

Tạo DOM bóng

Gốc đổ bóng là một mảnh tài liệu được đính kèm vào một phần tử "máy chủ lưu trữ". Hành động đính kèm gốc bóng là cách phần tử nhận được DOM bóng của mình. Người nhận tạo DOM bóng cho một phần tử, gọi element.attachShadow():

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

Tôi đang sử dụng .innerHTML để lấp đầy gốc bóng nhưng bạn cũng có thể sử dụng DOM khác API. Đây là web. Chúng ta có quyền lựa chọn.

Thông số kỹ thuật xác định danh sách các phần tử không thể lưu trữ cây bóng đổ. Có một số lý do có thể khiến một phần tử trên danh sách:

  • Trình duyệt đã lưu trữ DOM bóng nội bộ của riêng mình cho phần tử (<textarea>, <input>).
  • Việc phần tử lưu trữ DOM bóng (<img>) là không hợp lý.

Ví dụ: tính năng này không hoạt động:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

Tạo DOM bóng cho một phần tử tuỳ chỉnh

DOM bóng đặc biệt hữu ích khi tạo phần tử tuỳ chỉnh. Sử dụng DOM bóng để phân chia HTML, CSS và JS của một phần tử, do đó tạo một "thành phần web".

Ví dụ – một phần tử tuỳ chỉnh đính kèm DOM bóng vào chính nó, đóng gói DOM/CSS của nó:

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
        <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
        <div id="tabs">...</div>
        <div id="panels">...</div>
    `;
    }
    ...
});

Có một vài điều thú vị đang diễn ra ở đây. Đầu tiên là phần tử tuỳ chỉnh tạo DOM bóng của riêng mình khi một bản sao của <fancy-tabs> sẽ được tạo. Bạn có thể thực hiện việc này trong constructor(). Thứ hai, vì chúng tôi tạo ra gốc bóng, các quy tắc CSS bên trong <style> sẽ nằm trong phạm vi <fancy-tabs>.

Thành phần và khung giờ

Cấu trúc là một trong những tính năng ít được hiểu nhất của DOM tối, nhưng có lẽ là quan trọng nhất.

Trong thế giới phát triển web của chúng ta, thành phần là cách chúng ta tạo ứng dụng, thoát khỏi HTML. Các thành phần khác nhau (<div>, <header>, <form>, <input>) kết hợp với nhau để tạo thành các ứng dụng. Một số thẻ trong số này thậm chí còn hoạt động với nhau. Cấu trúc là lý do tại sao các phần tử gốc như <select>, <details>, <form><video> rất linh hoạt. Mỗi thẻ trong số đó chấp nhận HTML nhất định làm thành phần con và thực hiện điều gì đó đặc biệt với chúng. Ví dụ: <select> biết cách kết xuất <option><optgroup> trong trình đơn thả xuống và chọn nhiều tiện ích. Phần tử <details> kết xuất <summary> dưới dạng một mũi tên có thể mở rộng. Thậm chí <video> còn biết cách đối xử với một số trẻ em: Các phần tử <source> không được kết xuất nhưng có ảnh hưởng đến hoạt động của video. Thật kỳ diệu!

Thuật ngữ: DOM sáng so với DOM tối

Thành phần DOM tối giới thiệu một loạt các nguyên tắc cơ bản mới trong web phát triển ứng dụng. Trước khi đi vào cỏ dại, hãy chuẩn hoá một số chúng ta đang nói cùng một thuật ngữ.

DOM sáng

Mã đánh dấu mà người dùng trong thành phần của bạn viết. DOM này sống bên ngoài DOM bóng của thành phần. Đây là phần tử con thực sự của phần tử.

<better-button>
    <!-- the image and span are better-button's light DOM -->
    <img src="gear.svg" slot="icon">
    <span>Settings</span>
</better-button>

DOM tối

DOM mà tác giả thành phần viết. DOM bóng là cục bộ cho thành phần và xác định cấu trúc nội bộ, CSS có phạm vi và đóng gói phương thức triển khai của bạn chi tiết. Mã này cũng có thể xác định cách hiển thị mã đánh dấu do người dùng tạo trong thành phần của bạn.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

Cây DOM phẳng

Kết quả của việc trình duyệt phân phối DOM sáng của người dùng vào bóng của bạn DOM, hiển thị sản phẩm cuối cùng. Rốt cuộc thì bạn sẽ thấy cái cây đã được làm phẳng trong Công cụ cho nhà phát triển và nội dung hiển thị trên trang.

<better-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="gear.svg" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
        <span>Settings</span>
        </slot>
    </span>
</better-button>

Thẻ <slot> phần tử

DOM bóng kết hợp nhiều cây DOM lại với nhau bằng phần tử <slot>. Khe chứa phần giữ chỗ bên trong thành phần của bạn mà người dùng có thể điền vào mã đánh dấu riêng. Bằng việc xác định một hoặc nhiều vùng, bạn mời mã đánh dấu bên ngoài hiển thị trong DOM tối của thành phần. Về cơ bản, bạn muốn nói "Hiển thị đánh dấu ở đây".

Các phần tử được phép "kết hợp" ranh giới DOM tối khi <slot> mời đưa họ vào. Những phần tử này được gọi là nút phân phối. Về mặt lý thuyết, các nút được phân phối có thể hơi kỳ lạ. Các vị trí không di chuyển DOM; chúng hiển thị hình ảnh đó tại một vị trí khác bên trong DOM bóng.

Một thành phần có thể xác định số không hoặc nhiều ô trong DOM bóng đổ. Ô trống có thể trống hoặc cung cấp nội dung dự phòng. Nếu người dùng không cung cấp DOM sáng nội dung, vị trí sẽ hiển thị nội dung dự phòng.

<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>

<slot>fallback content</slot> <!-- default slot with fallback content -->

<slot> <!-- default slot entire DOM tree as fallback -->
    <h2>Title</h2>
    <summary>Description text</summary>
</slot>

Bạn cũng có thể tạo các vùng được đặt tên. Ô được đặt tên là các lỗ cụ thể trong DOM bóng mà người dùng tham chiếu theo tên.

Ví dụ – các vị trí trong DOM tối của <fancy-tabs>:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

Người dùng thành phần khai báo <fancy-tabs> như sau:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

Và nếu bạn muốn biết, cây đã dẹt sẽ trông giống như sau:

<fancy-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="panels">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</fancy-tabs>

Lưu ý rằng thành phần của chúng ta có thể xử lý nhiều cấu hình, nhưng cây DOM phẳng vẫn giữ nguyên. Chúng ta cũng có thể chuyển từ <button> sang <h2>. Thành phần này được tạo ra để xử lý nhiều kiểu trẻ em... chỉ giống như <select>!

Định kiểu

Có nhiều lựa chọn để tạo kiểu cho các thành phần web. Một thành phần sử dụng bóng DOM có thể được tạo kiểu theo trang chính, xác định kiểu của riêng nó hoặc cung cấp nội dung hấp dẫn (theo dưới dạng thuộc tính tuỳ chỉnh của CSS) để người dùng ghi đè lên các giá trị mặc định.

Kiểu do thành phần xác định

Tính năng hữu ích nhất của DOM tối là CSS có phạm vi:

  • Bộ chọn CSS từ trang bên ngoài không áp dụng trong thành phần của bạn.
  • Các kiểu được xác định bên trong sẽ không bị tràn. Các yêu cầu đó nằm trong phạm vi của phần tử máy chủ lưu trữ.

Bộ chọn CSS dùng bên trong DOM tối sẽ áp dụng cục bộ cho thành phần của bạn. Ngang bằng điều này nghĩa là chúng ta có thể sử dụng lại tên id/lớp chung mà không phải lo lắng về xung đột ở nơi khác trên trang. Tốt nhất là bạn nên dùng bộ chọn CSS đơn giản hơn bên trong Shadow DOM. Chúng cũng rất hữu ích cho hiệu suất.

Ví dụ – kiểu được xác định trong gốc của bóng là cục bộ

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Biểu định kiểu cũng nằm trong phạm vi cây bóng:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Bạn có từng thắc mắc cách phần tử <select> kết xuất tiện ích chọn nhiều mục không (thay vì một trình đơn thả xuống) khi bạn thêm thuộc tính multiple:

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

<select> có thể tạo kiểu chính khác nhau dựa trên các thuộc tính mà bạn tuyên bố về nội dung đó. Các thành phần web cũng có thể tự tạo kiểu, bằng cách sử dụng :host .

Ví dụ – chính việc định kiểu thành phần

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

Một vấn đề đáng chú ý với :host là các quy tắc trên trang mẹ có tính cụ thể cao hơn so với :host quy tắc đã xác định trong phần tử. Tức là kiểu bên ngoài chiến thắng. Chiến dịch này cho phép người dùng ghi đè kiểu cấp cao nhất từ bên ngoài. Ngoài ra, :host chỉ hoạt động trong ngữ cảnh gốc tối, vì vậy bạn không thể sử dụng nó bên ngoài DOM tối.

Dạng chức năng của :host(<selector>) cho phép bạn nhắm đến máy chủ nếu nó khớp với <selector>. Đây là một cách tuyệt vời để thành phần của bạn đóng gói các hành vi phản ứng với tương tác của người dùng hoặc trạng thái hoặc tạo kiểu cho các nút nội bộ dựa trên trên máy chủ.

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

Định kiểu dựa trên bối cảnh

:host-context(<selector>) khớp với thành phần nếu thành phần đó hoặc bất kỳ đối tượng cấp trên nào của thành phần đó khớp với <selector>. Một cách sử dụng phổ biến cho việc này là tuỳ chỉnh giao diện dựa trên môi trường xung quanh. Ví dụ: nhiều người thực hiện giao diện bằng cách áp dụng một lớp cho <html> hoặc <body>:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

:host-context(.darktheme) sẽ tạo kiểu cho <fancy-tabs> khi đó là thành phần con trong tổng số .darktheme:

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() có thể hữu ích cho việc tuỳ chỉnh giao diện, nhưng bạn cũng có thể dùng tạo hook cho kiểu bằng cách sử dụng thuộc tính tuỳ chỉnh CSS.

Tạo kiểu cho các nút được phân phối

::slotted(<compound-selector>) khớp với các nút được phân phối thành một <slot>.

Giả sử chúng ta đã tạo một thành phần huy hiệu tên:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

DOM bóng của thành phần có thể tạo kiểu cho <h2>.title của người dùng:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

Nếu bạn còn nhớ trước đó, <slot> không di chuyển DOM sáng của người dùng. Thời gian các nút được phân phối vào một <slot>, <slot> sẽ kết xuất DOM của chúng nhưng giữ nguyên vị trí của các nút. Những kiểu được áp dụng trước khi phân phối tiếp tục áp dụng sau khi phân phối. Tuy nhiên, khi DOM ánh sáng được phân phối, nó có thể sử dụng các kiểu khác (kiểu được xác định bởi DOM tối).

Một ví dụ khác chi tiết hơn từ <fancy-tabs>:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        border-radius: 3px;
        padding: 16px;
        height: 250px;
        overflow: auto;
    }
    #tabs {
        display: inline-flex;
        -webkit-user-select: none;
        user-select: none;
    }
    #tabsSlot::slotted(*) {
        font: 400 16px/22px 'Roboto';
        padding: 16px 8px;
        ...
    }
    #tabsSlot::slotted([aria-selected="true"]) {
        font-weight: 600;
        background: white;
        box-shadow: none;
    }
    #panelsSlot::slotted([aria-hidden="true"]) {
        display: none;
    }
    </style>
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
`;

Trong ví dụ này, có hai vị trí: một vị trí được đặt tên cho tiêu đề thẻ và một vị trí cho nội dung bảng điều khiển thẻ. Khi người dùng chọn một thẻ, chúng tôi sẽ in đậm lựa chọn của họ và hiện bảng điều khiển. Bạn có thể thực hiện việc này bằng cách chọn các nút đã phân phối có Thuộc tính selected. JS của phần tử tuỳ chỉnh (không hiển thị ở đây) bổ sung rằng vào đúng thời điểm.

Tạo kiểu cho một thành phần từ bên ngoài

Có một số cách để tạo kiểu cho một thành phần từ bên ngoài. Dễ nhất là sử dụng tên thẻ làm bộ chọn:

fancy-tabs {
    width: 500px;
    color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
    box-shadow: 0 3px 3px #ccc;
}

Kiểu bên ngoài luôn giành chiến thắng so với các kiểu được xác định trong DOM tối. Ví dụ: nếu người dùng viết bộ chọn fancy-tabs { width: 500px; }, thì bộ chọn đó sẽ chiếm ưu thế hơn quy tắc của thành phần: :host { width: 650px;}.

Cho đến nay, bạn chỉ có thể tạo kiểu cho thành phần này. Nhưng điều gì sẽ xảy ra nếu bạn bạn muốn tạo kiểu bên trong của một thành phần? Để làm được điều đó, chúng tôi cần CSS tuỳ chỉnh các thuộc tính.

Tạo hook cho kiểu bằng thuộc tính tuỳ chỉnh CSS

Người dùng có thể tinh chỉnh kiểu nội bộ nếu tác giả của thành phần cung cấp các hook tạo kiểu bằng cách sử dụng thuộc tính tuỳ chỉnh CSS. Về mặt lý thuyết, ý tưởng này tương tự với <slot>. Bạn tạo "phần giữ chỗ kiểu" để người dùng ghi đè.

Ví dụ<fancy-tabs> cho phép người dùng ghi đè màu nền:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

Bên trong DOM bóng:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

Trong trường hợp này, thành phần này sẽ sử dụng black làm giá trị nền vì do người dùng cung cấp. Nếu không, giá trị mặc định sẽ là #9E9E9E.

Chủ đề nâng cao

Tạo gốc đổ bóng khép kín (nên tránh)

Có một phiên bản khác của DOM bóng được gọi là "đóng" . Khi bạn tạo một cây bóng đổ, JavaScript bên ngoài sẽ không thể truy cập DOM nội bộ trong thành phần của bạn. Điều này tương tự như cách hoạt động của các phần tử gốc như <video>. JavaScript không thể truy cập DOM bóng của <video> vì trình duyệt triển khai bằng cách sử dụng gốc đổ bóng ở chế độ đóng.

Ví dụ – tạo cây bóng đổ kín:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

Các API khác cũng chịu ảnh hưởng của chế độ đóng:

  • Element.assignedSlot / TextNode.assignedSlot trả về null
  • Event.composedPath() cho các sự kiện liên kết với các phần tử bên trong bóng DOM, trả về []

Sau đây là phần tóm tắt của tôi về lý do bạn không bao giờ nên tạo các thành phần web bằng {mode: 'closed'}:

  1. Cảm giác an toàn giả tạo. Không có gì ngăn cản kẻ tấn công xâm nhập Element.prototype.attachShadow.

  2. Chế độ đóng ngăn mã phần tử tuỳ chỉnh truy cập vào chính nó DOM tối. Vậy là không thành công. Thay vào đó, bạn sẽ phải lưu trữ một tệp tham chiếu để dùng sau nếu bạn muốn sử dụng những thứ như querySelector(). Điều này hoàn toàn phá vỡ mục đích ban đầu của chế độ đóng!

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. Chế độ đóng khiến thành phần của bạn kém linh hoạt hơn đối với người dùng cuối. Khi bạn tạo thành phần web, sẽ có lúc bạn quên thêm của chúng tôi. Một lựa chọn cấu hình. Một trường hợp sử dụng mà người dùng muốn. Điểm chung Ví dụ: quên thêm các hook tạo kiểu phù hợp cho các nút nội bộ. Với chế độ đóng, người dùng không có cách nào để ghi đè các giá trị mặc định và tinh chỉnh kiểu. Khả năng truy cập vào nội dung bên trong thành phần này cực kỳ hữu ích. Cuối cùng, người dùng sẽ phát triển nhánh thành phần của bạn, tìm thành phần khác hoặc tạo thành phần sở hữu nếu nội dung đó không làm những gì họ muốn :(

Xử lý các vùng trong JS

shadow DOM API cung cấp các tiện ích để làm việc với các vị trí và phân phối nút. Các yếu tố này rất hữu ích khi bạn tạo một phần tử tuỳ chỉnh.

sự kiện thay đổi vị trí

Sự kiện slotchange sẽ kích hoạt khi các nút được phân phối của một vùng thay đổi. Cho ví dụ: nếu người dùng thêm/loại bỏ trẻ khỏi DOM ánh sáng.

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

Để theo dõi các loại thay đổi khác đối với DOM sáng, bạn có thể thiết lập MutationObserver trong hàm khởi tạo của phần tử.

Các phần tử nào đang được hiển thị trong một vị trí?

Đôi khi, sẽ rất hữu ích khi biết những phần tử nào được liên kết với một vùng. Gọi điện slot.assignedNodes() để tìm những phần tử mà khu vực đang hiển thị. Chiến lược phát hành đĩa đơn Tuỳ chọn {flatten: true} cũng sẽ trả về nội dung dự phòng của một vị trí (nếu không có nút nào) đang được phân phối).

Ví dụ: giả sử DOM tối của bạn có dạng như sau:

<slot><b>fallback content</b></slot>
Cách sử dụngGọiKết quả
<my-component>văn bản thành phần</my-component> slot.assignedNodes(); [component text]
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes(); []
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

Phần tử được gán cho vị trí nào?

Bạn cũng có thể trả lời câu hỏi ngược. element.assignedSlot cho biết cho bạn vị trí thành phần nào được gán vào.

Mô hình sự kiện Shadow DOM

Khi một sự kiện xuất hiện từ DOM tối, mục tiêu của sự kiện đó sẽ được điều chỉnh để duy trì đóng gói mà DOM tối cung cấp. Tức là các sự kiện được nhắm mục tiêu lại để xem giống như chúng đến từ thành phần chứ không phải các yếu tố nội bộ bên trong DOM tối. Một số sự kiện thậm chí không lan truyền ra khỏi DOM tối.

Các sự kiện vượt qua ranh giới bóng:

  • Sự kiện tiêu điểm: blur, focus, focusin, focusout
  • Sự kiện chuột: click, dblclick, mousedown, mouseenter, mousemove, v.v.
  • Sự kiện liên quan đến bánh xe: wheel
  • Đầu vào sự kiện: beforeinput, input
  • Sự kiện trên bàn phím: keydown, keyup
  • Sự kiện sáng tác: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop, v.v.

Mẹo

Nếu cây bóng đổ đang mở, lệnh gọi event.composedPath() sẽ trả về một mảng của các nút mà sự kiện đã đi qua.

Sử dụng sự kiện tuỳ chỉnh

Các sự kiện DOM tuỳ chỉnh được kích hoạt trên các nút nội bộ trong cây bóng đổ không bong bóng ra khỏi ranh giới bóng đổ trừ khi sự kiện được tạo bằng cách sử dụng Cờ composed: true:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

Nếu là composed: false (mặc định), người tiêu dùng sẽ không thể nghe sự kiện bên ngoài gốc bóng của bạn.

<fancy-tabs></fancy-tabs>
<script>
    const tabs = document.querySelector('fancy-tabs');
    tabs.addEventListener('tab-select', e => {
    // won't fire if `tab-select` wasn't created with `composed: true`.
    });
</script>

Xử lý tiêu điểm

Nếu bạn nhớ lại từ mô hình sự kiện của shadow DOM, các sự kiện được kích hoạt DOM bóng bên trong được điều chỉnh để trông giống như chúng đến từ phần tử lưu trữ. Ví dụ: giả sử bạn nhấp vào một <input> bên trong một gốc bóng:

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

Có vẻ như sự kiện focus này đến từ <x-focus>, không phải từ <input>. Tương tự, document.activeElement sẽ là <x-focus>. Nếu gốc bóng được tạo bằng mode:'open' (xem chế độ đóng), bạn cũng sẽ có thể truy cập nút nội bộ đã có được tiêu điểm:

document.activeElement.shadowRoot.activeElement // only works with open mode.

Nếu có nhiều cấp độ DOM bóng đang hoạt động (giả sử một phần tử tuỳ chỉnh trong một phần tử tuỳ chỉnh khác), bạn cần truy cập định kỳ vào gốc bóng để tìm activeElement:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

Một tuỳ chọn lấy tiêu điểm khác là tuỳ chọn delegatesFocus: true, giúp mở rộng hành vi tâm điểm của phần tử trong cây bóng đổ:

  • Nếu bạn nhấp vào một nút bên trong DOM bóng và nút đó không phải là khu vực có thể làm tâm điểm, khu vực có thể làm tâm điểm đầu tiên sẽ trở thành tiêu điểm.
  • Khi một nút bên trong DOM bóng nhận được tiêu điểm, :focus sẽ áp dụng cho máy chủ lưu trữ trong cho phần tử được đặt tiêu điểm.

Ví dụ – cách delegatesFocus: true thay đổi hành vi của tiêu điểm

<style>
    :focus {
    outline: 2px solid red;
    }
</style>

<x-focus></x-focus>

<script>
customElements.define('x-focus', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    const root = this.attachShadow({mode: 'open', delegatesFocus: true});
    root.innerHTML = `
        <style>
        :host {
            display: flex;
            border: 1px dotted black;
            padding: 16px;
        }
        :focus {
            outline: 2px solid blue;
        }
        </style>
        <div>Clickable Shadow DOM text</div>
        <input type="text" placeholder="Input inside shadow dom">`;

    // Know the focused element inside shadow DOM:
    this.addEventListener('focus', function(e) {
        console.log('Active element (inside shadow dom):',
                    this.shadowRoot.activeElement);
    });
    }
});
</script>

Kết quả

Ủy quyền tập trung: hành vi thực sự.

Trên đây là kết quả khi <x-focus> được lấy làm tiêu điểm (nhấp chuột của người dùng, được gắn thẻ vào, focus(), v.v.), "Văn bản DOM tối có thể nhấp" được nhấp vào hoặc quảng cáo nội bộ <input> được lấy tiêu điểm (bao gồm cả autofocus).

Nếu đặt delegatesFocus: false, bạn sẽ thấy màn hình sau đây:

đại biểuFocus: false và dữ liệu đầu vào nội bộ được đặt làm tâm điểm.
delegatesFocus: false<input> nội bộ được lấy tiêu điểm.
Ủy ban đại diện: false và x-focus
    lấy tiêu điểm (ví dụ: nó có tabindex=&#39;0&#39;).
delegatesFocus: false<x-focus> lấy được tiêu điểm (ví dụ: nó có tabindex="0").
Ủy ban đại biểu: false và &#39;Văn bản DOM bóng có thể nhấp&#39; là
    được nhấp vào (hoặc vùng trống khác trong DOM bóng của phần tử được nhấp vào).
delegatesFocus: false và "Văn bản DOM tối có thể nhấp vào" là được nhấp vào (hoặc vùng trống khác trong DOM bóng của phần tử được nhấp vào).

Mẹo và thủ thuật

Trong những năm qua, tôi đã học được đôi điều về việc tạo các thành phần web. T3 nghĩ rằng bạn sẽ thấy một vài mẹo trong số này hữu ích cho việc ghi nhận các thành phần và gỡ lỗi DOM bóng.

Sử dụng vùng chứa CSS

Thông thường, bố cục/kiểu/sơn của một thành phần web khá độc lập. Sử dụng Ngăn chặn CSS trong :host để đảm bảo hiệu suất thắng:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

Đặt lại kiểu có thể kế thừa

Các kiểu có thể kế thừa (background, color, font, line-height, v.v.) vẫn tiếp tục kế thừa trong DOM tối. Tức là chúng xuyên qua ranh giới DOM tối bằng cách mặc định. Nếu bạn muốn bắt đầu với một phương tiện chặn mới, hãy sử dụng all: initial; để đặt lại kiểu có thể kế thừa cho giá trị ban đầu khi vượt qua ranh giới bóng đổ.

<style>
    div {
    padding: 10px;
    background: red;
    font-size: 25px;
    text-transform: uppercase;
    color: white;
    }
</style>

<div>
    <p>I'm outside the element (big/white)</p>
    <my-element>Light DOM content is also affected.</my-element>
    <p>I'm outside the element (big/white)</p>
</div>

<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    :host {
        all: initial; /* 1st rule so subsequent properties are reset. */
        display: block;
        background: white;
    }
    </style>
    <p>my-element: all CSS properties are reset to their
        initial value using <code>all: initial</code>.</p>
    <slot></slot>
`;
</script>

Tìm tất cả phần tử tuỳ chỉnh mà một trang sử dụng

Đôi khi, bạn nên tìm các phần tử tuỳ chỉnh được sử dụng trên trang đó. Để làm như vậy, bạn cần di chuyển đệ quy qua DOM bóng của tất cả các phần tử được sử dụng trên trang.

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

Tạo phần tử từ <template>

Thay vì điền sẵn gốc đổ bóng bằng .innerHTML, chúng ta có thể sử dụng hàm khai báo <template>. Mẫu là phần giữ chỗ lý tưởng để khai báo cấu trúc của thành phần web.

Xem ví dụ trong "Phần tử tuỳ chỉnh: xây dựng các thành phần web có thể sử dụng lại".

Lịch sử và hỗ trợ trình duyệt

Nếu đã theo dõi các thành phần web trong vài năm qua, bạn sẽ biết rằng Chrome 35+/Opera đã vận chuyển phiên bản cũ hơn của DOM tối cho vào một thời điểm nào đó. Blink sẽ tiếp tục hỗ trợ song song cả hai phiên bản đối với một số bất cứ lúc nào. Thông số kỹ thuật v0 cung cấp một phương thức khác để tạo gốc đổ bóng (element.createShadowRoot thay vì element.attachShadow của phiên bản 1). Gọi phương thức cũ tiếp tục tạo gốc đổ bóng với ngữ nghĩa v0, vì vậy hiện có phiên bản v0 không làm hỏng mã.

Nếu bạn tình cờ quan tâm đến thông số kỹ thuật v0 cũ, hãy xem html5rock bài viết: 1! 2, 3. Còn có một so sánh tuyệt vời về điểm khác biệt giữa DOM tối phiên bản 0 và phiên bản 1.

Hỗ trợ trình duyệt

Shadow DOM phiên bản 1 được xuất bản trong Chrome 53 (trạng thái), Opera 40, Safari 10 và Firefox 63. Cạnh đã bắt đầu phát triển.

Để phát hiện DOM tối, hãy kiểm tra sự tồn tại của attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Ống polyfill

Cho đến khi hỗ trợ trình duyệt được cung cấp rộng rãi, kinh dị và Các polyfill shadycss cung cấp cho bạn phiên bản 1 của chúng tôi. DOM Shady bắt chước việc xác định phạm vi DOM của Shadow DOM và các đoạn polyfill của bóng râm Các thuộc tính tuỳ chỉnh CSS và phạm vi kiểu mà API gốc cung cấp.

Cài đặt các đoạn polyfill:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

Hãy sử dụng đoạn mã polyfill:

function loadScript(src) {
    return new Promise(function(resolve, reject) {
    const script = document.createElement('script');
    script.async = true;
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
    });
}

// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
    loadScript('/bower_components/shadydom/shadydom.min.js')
    .then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
    .then(e => {
        // Polyfills loaded.
    });
} else {
    // Native shadow dom v1 support. Go to go!
}

Hãy tham khảo bài viết https://github.com/webcomponents/shadycss#usage để biết hướng dẫn về cách chèn/giới hạn kiểu của bạn.

Kết luận

Lần đầu tiên chúng ta có một API gốc để xác định phạm vi CSS một cách thích hợp, Xác định phạm vi DOM và có bố cục thực. Kết hợp với các API thành phần web khác giống như các phần tử tuỳ chỉnh, shadow DOM cung cấp một cách để tác giả các thành phần không có kẻ tấn công hoặc sử dụng hành lý cũ như <iframe>.

Đừng hiểu lầm. DOM bóng chắc chắn là một mãnh thú phức tạp! Nhưng đó là quái vật đáng học hỏi. Hãy dành thời gian cho việc đó. Hãy tìm hiểu và đặt câu hỏi!

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

Câu hỏi thường gặp

Tôi có thể sử dụng Shadow DOM phiên bản 1 ngay bây giờ không?

Có. Hãy xem phần Hỗ trợ trình duyệt.

DOM tối cung cấp những tính năng bảo mật nào?

DOM tối không phải là một tính năng bảo mật. Đây là một công cụ gọn nhẹ để xác định phạm vi CSS và ẩn cây DOM trong thành phần. Nếu muốn có một ranh giới bảo mật thực sự, sử dụng <iframe>.

Thành phần web có phải sử dụng DOM tối không?

Không đâu. Bạn không phải tạo các thành phần web sử dụng DOM bóng. Tuy nhiên, việc tạo các phần tử tuỳ chỉnh sử dụng Shadow DOM có nghĩa là bạn có thể tận dụng các tính năng như xác định phạm vi CSS, đóng gói DOM và kết hợp.

Sự khác biệt giữa gốc đổ bóng mở và kín là gì?

Xem phần Gốc của bóng đã đóng.