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 phần Shadow DOM 101Shadow DOM 201.

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

Nếu bạn tổ chức tiệc, mọi người sẽ cảm thấy ngột ngạt nếu phải chen chúc trong cùng một phòng. Bạn muốn có lựa chọn phân bổ các nhóm người trên nhiều phòng. Các phần tử lưu trữ Shadow DOM cũng có thể thực hiện việc 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 thư mục 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 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ủ lưu trữ sẽ thắng. Đây là ngăn xếp LIFO (về mặt kết xuất). Việc kiểm tra Công cụ của Chrome cho nhà phát triển sẽ xác minh hành vi này.

Vậy thì lợi ích của việc sử dụng nhiều bóng nếu chỉ bóng đổ cuối cùng được mời đến nhóm hiển thị là gì? 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ủ, các tệp này là máy chủ cho các cây bóng khác. Đây là phần khởi đầu của Shadow DOM!

Như bạn có thể tưởng tượng, mọi thứ sẽ trở nên phức tạp hơn khi bạn đi sâu hơn vào hố thỏ. 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> đang phát:

Quay lại ví dụ ban đầu, root1 bóng đầu tiên đã bị bỏ qua trong danh sách mời. Bạn có thể thêm điểm chèn <shadow> để khôi phục:

<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 đảo ngược, hãy di chuyển điểm chèn: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Lưu ý hiện có một điểm chèn <content> trong root1. Điều này giúp nút văn bản "Light DOM" đi cùng 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ũ đ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 thư mục 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 trẻ nhấ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 bạn lo lắng về việc người khác đi vào bóng của bạn, hãy xác định lại .shadowRoot là rỗng:

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

Một chút hack, nhưng nó hoạt động. Cuối cùng, điều quan trọng cần nhớ là mặc dù rất tuyệt vời, nhưng Shadow DOM không được thiết kế để trở thành một 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.

Tạo Shadow DOM trong JS

Nếu bạn muốn tạo DOM trong JS, HTMLContentElementHTMLShadowElement có giao diện 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à giờ đây, tôi đang sử dụng select để lấy <span> mới thêm.

Làm việc với các đ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à…đổ trống…các nút được phân phối! 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.

Điều kỳ lạ về khái niệm điểm chèn là các điểm này không thực sự di chuyển DOM. Các nút của máy chủ lưu trữ vẫn còn nguyên vẹ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ề bản 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 bóng. Điều này dẫn đến một thông tin khác:

Element.getDistributedNodes()

Chúng ta không thể truy cập vào <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 các điểm chèn mà một nút được phân phối và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ụ: Công cụ hiển thị tối DOM

Rất khó để hiểu được phép thuật đen là Shadow DOM. Tôi nhớ lần đầu tiên mình 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 đã 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 được. Bạn có thể dán mã đánh dấu của riêng mình và thử nghiệm để xem mọi thứ hoạt động như thế nào và các điểm chèn sẽ chuyển đổi các nút lưu trữ vào cây bóng.

Trình xem Shadow DOM
Chạy Trình trực quan hoá Shadow DOM

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

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 qua ranh giới, mục tiêu sự kiện sẽ được điều chỉnh để duy trì tính bao bọc 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.

Play Action 1

  • Câu này 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 màu vàng một lần nữa, mouseout sẽ xuất hiện 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 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 bật lên qua ranh giới trên của Shadow DOM.

Chơi 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à sẽ xuất hiện trên chính nút lưu trữ. Mục tiêu đã được đặt 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
  • đổi kích thước
  • scroll
  • selectstart

Kết luận

Tôi hy vọng bạn sẽ đồng ý rằng Shadow DOM rất mạnh mẽ. Lần đầu tiên, chúng ta có được tính năng đóng gói phù 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 công nghệ phức tạp, nhưng đây là một công nghệ đáng để thêm vào nền tảng web. Hãy dành thời gian cho bản thảo đó. 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 của Dominic Shadow DOM 101 và bài viết Shadow DOM 201: CSS & Styling của tôi.