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! Mô hình này dựa trên các khái niệm đã thảo luận trong Shadow DOM 101Shadow DOM 201.

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

Nếu bạn đang tổ chức một bữa tiệc, sẽ thật khó chịu nếu tất cả mọi người bị nhồi nhét vào cùng một phòng. Bạn muốn phân phối các nhóm người vào nhiều phòng. Các phần tử lưu trữ DOM bóng cũng có thể thực hiện việc này, tức là có thể lưu trữ nhiều gốc bóng tại một thời điểm.

Hãy xem điều gì xảy ra nếu chúng ta cố gắng đính kèm nhiều gốc bóng (shadow) 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>

Kết xuất 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 đổ cuối cùng được thêm vào máy chủ sẽ chiến thắng. Đó là ngăn xếp LIFO liên quan đến quá trình 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 điểm của việc sử dụng nhiều bóng là gì nếu chỉ có bóng cuối cùng được mời vào bên 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ì giữ vai trò phần giữ chỗ cho nội dung của máy chủ lưu trữ, chúng lại là máy chủ lưu trữ cho các cây bóng đổ khác. Chào mừng bạn đến với sự kiện Shadow DOM!

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

Hãy xem lại ví dụ ban đầu của chúng ta, bóng đầu tiên root1 bị rời khỏi danh sách mời. Việc thêm điểm chèn <shadow> sẽ đưa điểm chèn đó 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 chúng tôi đã đặt điểm chèn <shadow>. Nếu bạn muốn đảo ngược, 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 làm cho nút văn bản "Light DOM" xuất hiện trong quá trình kết xuất.

Nội dung hiển thị ở <shadow>?

Đôi khi, việc biết cây đổ bóng cũ hơn được kết xuất tại <shadow> sẽ rất hữu ích. Bạn có thể lấy thông tin 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ủ

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

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

Nếu bạn lo lắng về việc có người chuyển sang bóng đổ, hãy xác định lại .shadowRoot thành giá trị rỗng:

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

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

Xây dựng DOM bóng trong JS

Nếu bạn muốn xây dựng DOM trong JS, thì HTMLContentElementHTMLShadowElement sẽ có giao diện dành cho việc đó.

<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à hiện tại tôi đang sử dụng select để lấy <span> mới được thêm vào.

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

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

Về mặt lý thuyết, điểm chèn có điều gì kỳ lạ là chúng không thực sự di chuyển DOM. Các nút của máy chủ lưu trữ vẫn giữ nguyên. Các đ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à nội dung trình bày/hiển thị: "Di chuyển các nút này qua đây" "Hiển thị 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 bit tid khác:

Element.getDistributedNodes()

Chúng ta không thể truyền tải đến <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 đến điểm chèn nào bằng cách gọi .getDestinationInsertionPoints() của nút đó:

<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ụ: Shadow DOM Visualizer

Thật khó để hiểu được ma thuật của hắc ám là Shadow DOM. Tôi còn nhớ lần đầu tiên đã cố gắng bao quanh nó.

Để giúp hình dung cách hoạt động của tính năng hiển thị DOM bóng, tôi đã tạo một công cụ bằng d3.js. Cả hai hộp đánh dấu ở bên trái đều có thể chỉnh sửa. Bạn có thể dán mã đánh dấu của riêng mình và thử xem cách hoạt động của các điểm chèn và chuyển các nút máy chủ vào cây bóng đổ.

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

Hãy thử và cho tôi biết bạn nghĩ gì!

Mô hình sự kiện

Một số sự kiện vượt qua ranh giới bóng đổ và một số thì không. Trong trường hợp các sự kiện vượt quá ranh giới, mục tiêu sự kiện sẽ được điều chỉnh để duy trì hoạt động đó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ư các sự kiện đó đến từ phần tử lưu trữ thay vì các phần tử nội bộ đến DOM bóng.

Hành động 1

  • Câu này thú vị đấy. Bạn sẽ thấy mouseout từ phần tử lưu trữ (<div data-host>) đến nút màu xanh dương. Mặc dù đây là một nút được 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. Việc di chuột xuống sâu hơn vào màu vàng một lần nữa gây ra mouseout trên nút màu xanh.

Hành động 2

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

Hành động 3

  • Xin lưu ý rằng khi bạn nhấp vào đầu vào, focusin không xuất hiện trên đầu vào mà chỉ xuất hiện trên chính nút máy chủ. Đã nhắm mục tiêu lại!

Các 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
  • resize
  • scroll
  • chọn bắt đầu

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ó đóng gói phù hợp mà không có thêm <iframe> hoặc các kỹ thuật cũ khác.

Shadow DOM chắc chắn là một con thú phức tạp, nhưng nó là một con thú đáng để thêm vào nền tảng web. Hãy dành chút thời gian cho công cụ này. Học ngàn điều hay. Đặ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 Shadow DOM 101 của Dominic và bài viết Shadow DOM 201: CSS và Style của tôi.