DOM bóng 301

Khái niệm nâng cao và API DOM

Bài viết này thảo luận thêm về những điều tuyệt vời mà bạn có thể làm với Shadow DOM! Bài viết này dựa trên các khái niệm được thảo luận trong Shadow DOM 101 và Shadow DOM 201.

Sử dụng nhiều gốc bóng

Nếu bạn tổ chức một bữa tiệc, không gian sẽ trở nên ngột ngạt nếu mọi người đều chen chúc trong cùng một phòng. Bạn muốn có thể phân bổ các nhóm người vào nhiều phòng. Các phần tử lưu trữ Shadow DOM cũng có thể làm được điều này, tức là chúng có thể lưu trữ nhiều gốc bóng cùng một lúc.

Hãy xem điều gì sẽ xảy ra nếu chúng ta cố gắng đính kèm nhiều gốc bóng vào một máy chủ lưu trữ:

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

Nội dung hiển thị là "Root 2 FTW", mặc dù chúng ta đã đính kèm một cây bóng. Điều này là do cây bóng tối cuối cùng được thêm vào máy chủ sẽ thắng. Đây là ngăn xếp LIFO (vào sau ra trước) về mặt kết xuất. Việc kiểm tra Công cụ cho nhà phát triển sẽ xác minh hành vi này.

Vậy thì việc sử dụng nhiều bóng đổ có ý nghĩa gì nếu chỉ có bóng đổ cuối cùng được mời tham gia quá trình kết xuất? Nhập điểm chèn bóng.

Điểm chèn bóng

"Điểm chèn bóng" (<shadow>) tương tự như điểm chèn thông thường (<content>) ở chỗ chúng là phần giữ chỗ. Tuy nhiên, thay vì là phần giữ chỗ cho nội dung của máy chủ lưu trữ, chúng là máy chủ lưu trữ cho các cây bóng khác. Đó là Shadow DOM Inception!

Như bạn có thể hình dung, mọi thứ sẽ trở nên phức tạp hơn khi bạn tìm hiểu sâu hơn. Vì lý do này, thông số kỹ thuật nêu rất rõ điều gì sẽ xảy ra khi có nhiều phần tử <shadow>:

Nhìn lại ví dụ ban đầu, bóng đổ đầu tiên root1 đã bị loại khỏi danh sách mời. Thêm điểm chèn <shadow> sẽ đưa điểm đó trở lại:

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

Có một vài điều thú vị về ví dụ này:

  1. "Root 2 FTW" vẫn hiển thị phía trên "Root 1 FTW". Điều này là do vị trí chúng ta đặt điểm chèn <shadow>. Nếu bạn muốn làm ngược lại, hãy di chuyển điểm chèn: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Lưu ý rằng hiện có một điểm chèn <content> trong root1. Điều này khiến nút văn bản "Light DOM" xuất hiện trong quá trình kết xuất.

Nội dung nào được hiển thị tại <shadow>?

Đôi khi, bạn nên biết cây bóng cũ hơn đang được kết xuất tại <shadow>. Bạn có thể tham chiếu đến cây đó thông qua .olderShadowRoot:

**root2.olderShadowRoot** === root1 //true

Lấy gốc bóng của máy chủ lưu trữ

Nếu một phần tử đang lưu trữ Shadow DOM, bạn có thể truy cập vào gốc bóng tối mới nhất của phần tử đó bằng cách sử dụng .shadowRoot:

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

Nếu lo ngại về việc người dùng vượt qua bóng của bạn, hãy xác định lại .shadowRoot thành rỗng:

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

Hơi rườm rà nhưng hiệu quả. Cuối cùng, bạn cần nhớ rằng mặc dù rất tuyệt vời, nhưng Shadow DOM không được thiết kế để làm tính năng bảo mật. Đừng dựa vào tính năng này để tách biệt hoàn toàn nội dung.

Xây dựng DOM tối trong JS

Nếu muốn tạo DOM trong JS, bạn có thể dùng giao diện của HTMLContentElement và HTMLShadowElement.

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

Ví dụ này gần giống với ví dụ trong phần trước. Điểm khác biệt duy nhất là giờ đây, tôi đang sử dụng select để lấy <span> mới thêm.

Làm việc với điểm chèn

Các nút được chọn từ phần tử lưu trữ và "phân phối" vào cây bóng được gọi là…drumroll…nút phân phối! Các phần tử này được phép vượt qua ranh giới bóng khi các điểm chèn mời chúng vào.

Điều kỳ lạ về mặt khái niệm của các điểm chèn là chúng không di chuyển DOM về mặt vật lý. Các nút của máy chủ lưu trữ vẫn được giữ nguyên. Điểm chèn chỉ chiếu lại các nút từ máy chủ vào cây bóng. Đây là một vấn đề về trình bày/kết xuất: "Di chuyển các nút này đến đây" "Kết xuất các nút này tại vị trí này".

Ví dụ:

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

Như thế đấy! h2 không phải là phần tử con của DOM tối. Điều này dẫn đến một thông tin khác:

Element.getDistributedNodes()

Chúng ta không thể chuyển đổi thành <content>, nhưng API .getDistributedNodes() cho phép chúng ta truy vấn các nút được phân phối tại một điểm chèn:

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

Tương tự như .getDistributedNodes(), bạn có thể kiểm tra xem một nút được phân phối vào điểm chèn nào bằng cách gọi .getDestinationInsertionPoints():

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

Công cụ: Trình hiển thị DOM bóng

Thật khó để hiểu được "phép thuật hắc ám" của Shadow DOM. Tôi nhớ lần đầu tiên tôi cố gắng tìm hiểu về nó.

Để giúp bạn hình dung cách hoạt động của tính năng kết xuất Shadow DOM, tôi đã xây dựng một công cụ bằng d3.js. Bạn có thể chỉnh sửa cả hai hộp đánh dấu ở bên trái. Bạn có thể dán mã đánh dấu của riêng mình và thử nghiệm để xem cách mọi thứ hoạt động cũng như cách các điểm chèn chuyển đổi nút lưu trữ vào cây bóng.

Trình hiển thị DOM bóng
Chạy Trình hiển thị DOM bóng

Hãy dùng thử và cho tôi biết ý kiến của bạn nhé!

Mô hình sự kiện

Một số sự kiện vượt qua ranh giới bóng và một số sự kiện thì không. Trong trường hợp sự kiện vượt qua ranh giới, mục tiêu sự kiện sẽ được điều chỉnh để duy trì tính đóng gói mà ranh giới trên của gốc bóng cung cấp. Tức là các sự kiện được nhắm mục tiêu lại để trông giống như chúng đến từ phần tử lưu trữ thay vì các phần tử nội bộ đến Shadow DOM.

Thao tác phát 1

  • Câu này khá thú vị. Bạn sẽ thấy một mouseout từ phần tử lưu trữ (<div data-host>) đến nút màu xanh dương. Mặc dù là một nút phân phối, nhưng nút này vẫn nằm trong máy chủ lưu trữ chứ không phải ShadowDOM. Khi di chuột xuống sâu hơn vào vùng màu vàng, một sự kiện mouseout sẽ lại được kích hoạt trên nút màu xanh dương.

Play Action 2

  • Có một mouseout xuất hiện trên máy chủ (ở cuối). Thông thường, bạn sẽ thấy các sự kiện mouseout kích hoạt cho tất cả các khối màu vàng. Tuy nhiên, trong trường hợp này, các phần tử này nằm trong Shadow DOM và sự kiện không nổi lên qua ranh giới trên của nó.

Play Action 3

  • Lưu ý rằng khi bạn nhấp vào dữ liệu đầu vào, focusin sẽ không xuất hiện trên dữ liệu đầu vào mà xuất hiện trên chính nút lưu trữ. Đã nhắm mục tiêu lại!

Sự kiện luôn bị dừng

Các sự kiện sau đây không bao giờ vượt qua ranh giới bóng:

  • hủy đi
  • error
  • chọn
  • thay đổi
  • trọng tải
  • Khôi phục tuỳ chọn tìm kiếm
  • thay đổi kích thước
  • scroll
  • selectstart

Kết luận

Tôi hy vọng bạn sẽ đồng ý rằng Shadow DOM cực kỳ mạnh mẽ. Lần đầu tiên, chúng ta có tính năng đóng gói thích hợp mà không cần thêm <iframe> hoặc các kỹ thuật cũ khác.

Shadow DOM chắc chắn là một con quái vật phức tạp, nhưng là một con quái vật đáng để thêm vào nền tảng web. Hãy dành chút thời gian để tìm hiểu. Học hỏi. Đặt câu hỏi.

Nếu bạn muốn tìm hiểu thêm, hãy xem bài viết giới thiệu của Dominic Shadow DOM 101 và bài viết Shadow DOM 201: CSS & Styling của tôi.