DOM bóng 101

Dominic Cooney
Dominic Cooney

Giới thiệu

Thành phần web là một bộ tiêu chuẩn tiên tiến giúp:

  1. Cho phép tạo tiện ích
  2. ...có thể được sử dụng lại theo cách đáng tin cậy
  3. ...và tính năng này sẽ không làm hỏng trang nếu phiên bản tiếp theo của thành phần thay đổi thông tin triển khai nội bộ.

Điều này có nghĩa là bạn phải quyết định thời điểm sử dụng HTML/JavaScript và khi nào sử dụng Thành phần web? Không đâu! HTML và JavaScript có thể giúp nội dung trực quan tương tác. Tiện ích là nội dung trực quan có tính tương tác. Nó để tận dụng các kỹ năng HTML và JavaScript của mình khi phát triển một tiện ích. Các tiêu chuẩn về Thành phần web được thiết kế nhằm giúp bạn làm điều đó.

Tuy nhiên, có một vấn đề cơ bản là các tiện ích được tạo từ HTML và JavaScript khó sử dụng: Cây DOM bên trong tiện ích không tách biệt khỏi phần còn lại của trang. Thiếu tính đóng gói nghĩa là biểu định kiểu tài liệu của bạn có thể vô tình áp dụng cho các phần bên trong tiện ích; JavaScript của bạn có thể vô tình sửa đổi các phần bên trong tiện ích; ID của bạn có thể trùng với ID bên trong tiện ích; và cứ tiếp tục như vậy.

Thành phần web bao gồm ba phần:

  1. Mẫu
  2. DOM tối
  3. Phần tử tuỳ chỉnh

Shadow DOM giải quyết vấn đề đóng gói cây DOM. Chiến lược phát hành đĩa đơn bốn phần của Thành phần web được thiết kế để hoạt động cùng nhau, nhưng bạn cũng có thể chọn phần nào của Thành phần web để sử dụng. Chiến dịch này hướng dẫn cho bạn cách sử dụng Shadow DOM.

Xin chào, Thế giới bóng tối

Với Shadow DOM, các phần tử có thể có một loại nút mới liên kết với chúng. Loại nút mới này được gọi là gốc bóng. Phần tử có gốc bóng liên kết với phần tử đó được gọi là bóng máy chủ lưu trữ. Nội dung của máy chủ bóng không được hiển thị; nội dung của thay vào đó, hiệu ứng đổ bóng sẽ được kết xuất.

Ví dụ: nếu bạn có mã đánh dấu như sau:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

thì thay vì

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

trang của bạn trông giống như thế nào

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

Không chỉ vậy, nếu JavaScript trên trang hỏi nút textContent thì sẽ không nhận được "こんんたちん影の世界!" nhưng là "Xin chào thế giới!" do cây con DOM dưới gốc bóng đổ được đóng gói.

Tách nội dung khỏi bản trình bày

Giờ chúng ta sẽ xem xét việc sử dụng Shadow DOM để tách nội dung khỏi bản trình bày. Giả sử chúng ta có thẻ tên sau:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Đây là phần đánh dấu. Đây là những gì bạn sẽ viết hôm nay. Không sử dụng Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Do cây DOM không đóng gói, toàn bộ cấu trúc của thẻ tên được hiển thị với tài liệu. Nếu các thành phần khác trên trang vô tình sử dụng cùng một tên lớp để tạo kiểu hoặc viết tập lệnh, chúng ta sẽ có khoảng thời gian không vui.

Chúng ta có thể tránh gặp phải khoảng thời gian không vui.

Bước 1: Ẩn thông tin chi tiết về bản trình bày

Về mặt ngữ nghĩa, có lẽ chúng ta chỉ quan tâm đến việc:

  • Đó là thẻ tên.
  • Tên là "Bob".

Trước tiên, chúng ta viết mã đánh dấu gần với ngữ nghĩa thực mà chúng ta muốn:

<div id="nameTag">Bob</div>

Sau đó, chúng ta đặt tất cả các kiểu và div được dùng để trình bày vào phần tử <template>:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

Lúc này, "Bob" là thành phần duy nhất được kết xuất. Vì chúng tôi đã di chuyển các phần tử DOM trình bày vào trong phần tử <template>, chúng sẽ không được hiển thị, nhưng bạn có thể truy cập vào các mã này qua JavaScript. Chúng tôi làm việc đó ngay bây giờ để điền sẵn vào gốc đổ bóng:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

Bây giờ, chúng ta đã thiết lập gốc đổ bóng, thẻ tên sẽ được kết xuất một lần nữa. Nếu bạn nhấp chuột phải vào thẻ tên và kiểm tra bạn thấy rằng nó được đánh dấu có ngữ nghĩa ngọt ngào:

<div id="nameTag">Bob</div>

Điều này chứng minh rằng bằng cách sử dụng Shadow DOM, chúng tôi đã ẩn các chi tiết trình bày của thẻ tên trong tài liệu. Chiến lược phát hành đĩa đơn các chi tiết về bản trình bày được đóng gói trong Shadow DOM.

Bước 2: Tách riêng nội dung khỏi bản trình bày

Giờ đây, thẻ tên của chúng ta ẩn thông tin chi tiết về bản trình bày khỏi trang, không thực sự tách bản trình bày khỏi nội dung, vì mặc dù nội dung (tên "Bob") có trên trang, tên hiển thị là mã mà chúng ta đã sao chép vào thư mục gốc. Nếu chúng ta muốn thay đổi tên trên thẻ tên, chúng tôi cần thực hiện việc đó ở hai vị trí và chúng có thể không đồng bộ.

Các phần tử HTML mang tính cấu trúc — bạn có thể đặt một nút bên trong bảng, ví dụ: Cấu trúc là những gì chúng ta cần ở đây: Thẻ tên phải là bố cục nền đỏ, câu "Xin chào!" văn bản và nội dung trên thẻ tên.

Bạn, tác giả thành phần, xác định cách sáng tác hoạt động với bằng cách sử dụng phần tử mới có tên là <content>. Chiến dịch này tạo một điểm chèn trong bản trình bày tiện ích và điểm chèn chọn nội dung từ máy chủ bóng để hiển thị tại thời điểm đó.

Nếu chúng ta thay đổi mã đánh dấu trong Shadow DOM thành:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

Khi thẻ tên được hiển thị, nội dung của máy chủ bóng sẽ chiếu vào điểm mà phần tử <content> sẽ xuất hiện.

Giờ đây, cấu trúc của tài liệu đơn giản hơn vì chỉ có tên ở cùng một nơi — tài liệu. Nếu trang của bạn cần cập nhật tên của người dùng, bạn chỉ cần viết:

document.querySelector('#nameTag').textContent = 'Shellie';

và thế là xong. Quá trình hiển thị thẻ tên được cập nhật tự động bởi trình duyệt vì chúng tôi đang chiếu nội dung của vào vị trí bằng <content>.

<div id="ex2b">

Giờ đây, chúng ta đã tách được nội dung và cách trình bày. Các nội dung nằm trong tài liệu; bản trình bày nằm trong Shadow DOM. Chúng sẽ được trình duyệt tự động đồng bộ hóa khi đến lúc để kết xuất nội dung nào đó.

Bước 3: Lợi nhuận

Bằng cách tách riêng nội dung và cách trình bày, chúng tôi có thể đơn giản hoá mã thao túng nội dung — trong ví dụ về thẻ tên, đoạn mã đó mã chỉ cần xử lý một cấu trúc đơn giản chứa một <div> thay vì một vài.

Bây giờ, nếu thay đổi cách trình bày, chúng ta không cần thay đổi bất kỳ mã!

Ví dụ: giả sử chúng ta muốn bản địa hoá thẻ tên của mình. Đó vẫn là một tên để nội dung ngữ nghĩa trong tài liệu không thay đổi:

<div id="nameTag">Bob</div>

Mã thiết lập gốc đổ bóng vẫn giữ nguyên. Chỉ cần đưa những gì được đưa vào thay đổi gốc của bóng:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

Đây là một cải tiến lớn so với tình hình trên web ngày nay, bởi vì mã cập nhật tên của bạn có thể phụ thuộc vào cấu trúc của thành phần đơn giản và nhất quán. Tên của bạn mã cập nhật không cần biết cấu trúc được dùng cho kết xuất hình ảnh. Nếu chúng ta xem xét nội dung hiển thị thì tên đó sẽ xuất hiện thứ hai bằng tiếng Anh (sau “Hi! Tôi tên là”) nhưng đầu tiên bằng tiếng Nhật (trước “χ申子ます”). Sự khác biệt đó vô nghĩa về mặt ngữ nghĩa từ góc nhìn cập nhật tên đang được hiển thị, nên mã cập nhật tên không cần biết về chi tiết đó.

Tín dụng bổ sung: Dự đoán nâng cao

Trong ví dụ trên, phần tử <content> quả anh đào chọn tất cả nội dung từ máy chủ bóng. Bằng cách sử dụng select, bạn có thể kiểm soát việc dự án phần tử nội dung. Bạn cũng có thể sử dụng nhiều nội dung phần tử.

Ví dụ: nếu bạn có tài liệu chứa thông tin sau:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

và gốc bóng sử dụng bộ chọn CSS để chọn nội dung cụ thể:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

Phần tử <div class="email"> khớp với cả hai phần tử <content select="div"><content select=".email">. Email của Bob bao nhiêu lần địa chỉ của bạn xuất hiện và có màu gì?

Câu trả lời là địa chỉ email của Bob xuất hiện một lần và có màu vàng.

Lý do là, như những người đột nhập vào Shadow DOM biết, việc xây dựng cây nội dung thực sự kết xuất trên màn hình giống như bữa tiệc lớn. Phần tử nội dung là lời mời cho phép nội dung từ tài liệu vào quá trình kết xuất Shadow DOM bên thứ ba. Những lời mời này được gửi theo thứ tự; người nhận được thư lời mời phụ thuộc vào người gửi thư (tức là thuộc tính select.) Nội dung, một lần được mời, luôn chấp nhận lời mời (ai không chấp nhận?!) và không chấp nhận lời mời đó sẽ đi. Nếu lời mời tiếp theo được gửi lại đến địa chỉ đó thì không có ai ở nhà và không đến bữa tiệc của bạn.

Trong ví dụ trên, <div class="email"> khớp với cả bộ chọn div.email bộ chọn khác, nhưng vì phần tử nội dung có div xuất hiện trước đó trong tài liệu, <div class="email"> tham gia bữa tiệc màu vàng và không ai có mặt để tham gia bữa tiệc màu xanh dương. (Điều đó có thể hãy tại sao màu xanh lam của bạn lại có màu xanh dương, mặc dù đau khổ thích công việc, vì vậy bạn không bao giờ biết.)

Nếu một hoạt động nào đó được mời không tham gia bữa tiệc nào, thì hoạt động đó sẽ không được hiển thị hoàn toàn. Đó là những gì đã xảy ra với dòng chữ "Hello, world" trong trong ví dụ đầu tiên. Điều này rất hữu ích khi bạn muốn có kết xuất khác hoàn toàn: Viết mô hình ngữ nghĩa trong tài liệu mà tập lệnh có thể truy cập được trong trang, nhưng ẩn cho mục đích kết xuất và kết nối nó với một mô hình kết xuất trong Shadow DOM bằng JavaScript.

Ví dụ: HTML có một bộ chọn ngày đẹp mắt. Nếu bạn viết <input type="date">, bạn sẽ có một lịch bật lên gọn gàng. Nhưng nếu bạn muốn cho người dùng chọn một phạm vi ngày cho món tráng miệng của họ kỳ nghỉ trên đảo (bạn biết đấy... với võng làm từ Cây nho đỏ.) Bạn thiết lập tài liệu của bạn theo cách sau:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

nhưng tạo Shadow DOM sử dụng bảng để tạo ra một lịch đẹp để đánh dấu phạm vi ngày và các mục khác. Khi người dùng nhấp vào các ngày trong lịch, thành phần cập nhật trạng thái trong Mục nhập startDate và endDate; khi người dùng gửi biểu mẫu, từ các phần tử đầu vào đó được gửi đi.

Tại sao tôi lại đưa các nhãn vào tài liệu nếu chúng không thích hợp đã hiển thị không? Lý do là nếu người dùng xem biểu mẫu bằng trình duyệt không hỗ trợ Shadow DOM, biểu mẫu này vẫn sử dụng được, nhưng không giống như khá đẹp. Người dùng thấy một số thông tin như:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

Bạn vượt qua Shadow DOM 101

Đó là những thông tin cơ bản của Shadow DOM — bạn vượt qua Shadow DOM 101! Bạn có thể làm được nhiều việc hơn với DOM tối, ví dụ: bạn có thể sử dụng nhiều bóng trên một máy chủ bóng đổ, hoặc bóng lồng nhau để đóng gói hoặc cấu trúc trang của bạn bằng Chế độ xem dựa trên mô hình (MDV) và DOM bóng. Và web Các thành phần không chỉ là DOM bóng.

Chúng tôi sẽ giải thích những điều này trong các bài đăng sau.