Shadow DOM 301

แนวคิดขั้นสูงและ DOM API

บทความนี้กล่าวถึงสิ่งมหัศจรรย์อีกมากมายที่คุณสามารถทำได้ด้วย Shadow DOM ซึ่งจะต่อยอดจากแนวคิดที่กล่าวถึงใน Shadow DOM 101 และ Shadow DOM 201

การใช้รากแสงหลายราก

หากคุณจัดงานปาร์ตี้ ทุกคนจะอึดอัดหากต้องเบียดกันอยู่ในห้องเดียวกัน คุณต้องการตัวเลือกในการกระจายกลุ่มคนไปยังหลายห้อง องค์ประกอบที่โฮสต์ Shadow DOM สามารถทำเช่นนี้ได้เช่นกัน กล่าวคือ องค์ประกอบดังกล่าวสามารถโฮสต์ Shadow Root ได้มากกว่า 1 รายการพร้อมกัน

เรามาดูกันว่าจะเกิดอะไรขึ้นหากพยายามแนบรูทเงาหลายรายการกับโฮสต์

<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>

สิ่งที่แสดงผลคือ "Root 2 FTW" แม้ว่าเราจะแนบ Shadow Tree ไว้แล้วก็ตาม เนื่องจากระบบจะเลือก Shadow Tree รายการล่าสุดที่เพิ่มลงในโฮสต์ นี่เป็นกอง LIFO ในแง่ของการเรนเดอร์ ซึ่งการตรวจสอบเครื่องมือสำหรับนักพัฒนาเว็บจะยืนยันลักษณะการทำงานนี้

แล้วการใช้เงาหลายรายการจะมีประโยชน์อะไรหากมีเพียงเงาสุดท้ายเท่านั้นที่เชิญให้เข้าร่วมการเรนเดอร์ ป้อนจุดแทรกเงา

จุดแทรกเงา

"จุดแทรกเงา" (<shadow>) คล้ายกับจุดแทรก (<content>) ปกติตรงที่เป็นตัวยึดตำแหน่ง อย่างไรก็ตาม ไฟล์เหล่านี้ไม่ได้เป็นตัวยึดตําแหน่งสําหรับเนื้อหาของโฮสต์ แต่เป็นโฮสต์สําหรับต้นไม้เงาอื่นๆ พบกับ Shadow DOM Inception

ดังที่คุณอาจทราบแล้วว่ายิ่งเจาะลึกลงไปมากเท่าใด ทุกอย่างก็จะยิ่งซับซ้อนมากขึ้นเท่านั้น ด้วยเหตุนี้ ข้อกำหนดจึงระบุไว้อย่างชัดเจนว่าจะเกิดอะไรขึ้นเมื่อมีองค์ประกอบ <shadow> หลายรายการ

เมื่อมองกลับไปที่ตัวอย่างแรก root1 แรกไม่ได้อยู่ในรายการคำเชิญ การเพิ่มจุดแทรก <shadow> จะนำจุดแทรกกลับมา

<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>

มีสิ่งที่น่าสนใจ 2 อย่างเกี่ยวกับตัวอย่างนี้

  1. "Root 2 FTW" จะยังคงแสดงผลเหนือ "Root 1 FTW" สาเหตุคือตําแหน่งที่เราวางจุดแทรก <shadow> หากต้องการแบบย้อนกลับ ให้ย้ายจุดแทรก: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';
  2. โปรดสังเกตว่าตอนนี้มีจุดแทรก <content> ใน root1 ซึ่งจะทำให้โหนดข้อความ "Light DOM" แสดงผลด้วย

สิ่งที่แสดงผลที่ <shadow>

บางครั้งการทราบว่าแผนผังเงาที่เก่ากว่าแสดงใน <shadow> ก็มีประโยชน์ คุณดูข้อมูลอ้างอิงสำหรับแผนภูมิดังกล่าวผ่าน .olderShadowRoot ได้โดยทำดังนี้

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

การขอรูทเงาของโฮสต์

หากองค์ประกอบโฮสต์ Shadow DOM อยู่ คุณจะเข้าถึงรูท Shadow ที่อายุน้อยที่สุดได้โดยใช้ .shadowRoot ดังนี้

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

หากคุณกังวลว่าจะมีบุคคลเดินผ่านเงาของคุณ ให้กําหนดค่า .shadowRoot เป็น Null ดังนี้

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

เป็นการแฮ็กแบบเล็กน้อย แต่ได้ผล สุดท้ายนี้ โปรดทราบว่าแม้ Shadow DOM จะยอดเยี่ยมอย่างไม่น่าเชื่อ แต่ก็ไม่ได้ออกแบบมาเพื่อเป็นฟีเจอร์ด้านความปลอดภัย แต่อย่าพึ่งพาการแยกเนื้อหาอย่างสมบูรณ์

การสร้าง Shadow DOM ใน JS

หากต้องการสร้าง DOM ใน JS HTMLContentElement และ 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>

ตัวอย่างนี้เกือบจะเหมือนกับตัวอย่างในส่วนก่อนหน้า ความแตกต่างเพียงอย่างเดียวคือตอนนี้ฉันใช้ select เพื่อดึง <span> ที่เพิ่มเข้ามาใหม่

การทำงานกับจุดแทรก

โหนดที่เลือกจากองค์ประกอบโฮสต์และ "กระจาย" ไปยังต้นไม้เงานั้นเรียกกันว่าโหนดแบบกระจาย ข้อความจะข้ามขอบเขตเงาได้เมื่อจุดแทรกเชิญให้เข้ามา

แนวคิดที่แปลกไปมากเกี่ยวกับจุดแทรกคือ จุดเหล่านั้นไม่ได้ย้าย DOM โดยโหนดของโฮสต์จะยังคงอยู่เหมือนเดิม จุดแทรกเป็นเพียงการโปรเจ็กต์โหนดจากโฮสต์ไปยังต้นไม้เงาอีกครั้ง เป็นการนำเสนอ/การแสดงผล: "ย้ายโหนดเหล่านี้ไปที่นี่" "แสดงผลโหนดเหล่านี้ที่ตำแหน่งนี้"

เช่น

<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>

เรียบร้อย! h2 ไม่ใช่รายการย่อยของ Shadow DOM ซึ่งนำไปสู่เรื่องที่น่าสนใจอีกเรื่องหนึ่ง

Element.getDistributedNodes()

เราข้ามผ่านไปยัง <content> ไม่ได้ แต่ .getDistributedNodes() API ช่วยให้เราค้นหาโหนดแบบกระจายที่จุดแทรกได้

<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()

คุณสามารถตรวจสอบจุดแทรกที่โหนดกระจายอยู่ได้ด้วยการเรียก.getDestinationInsertionPoints()ของโหนด เช่นเดียวกับ.getDistributedNodes()

<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>

เครื่องมือ: เครื่องมือแสดงภาพ Shadow DOM

การทำความเข้าใจเวทมนตร์ดำที่เป็น Shadow DOM นั้นเป็นเรื่องยาก เราจำได้ว่าพยายามทำความเข้าใจเรื่องนี้เป็นครั้งแรก

เราได้สร้างเครื่องมือโดยใช้ d3.js เพื่อช่วยให้เห็นภาพว่าการแสดงผล Shadow DOM ทํางานอย่างไร ช่องมาร์กอัปทั้ง 2 ช่องทางด้านซ้ายจะแก้ไขได้ วางมาร์กอัปของคุณเองและลองดูว่าระบบทํางานอย่างไร และจุดแทรกจะเปลี่ยนโฮสต์โหนดเป็นต้นไม้เงาอย่างไร

เครื่องมือแสดงภาพ Shadow DOM
เปิดตัว Shadow DOM Visualizer

ลองใช้และบอกให้เราทราบว่าคุณคิดอย่างไร

โมเดลเหตุการณ์

เหตุการณ์บางรายการข้ามขอบเขตเงา แต่บางรายการไม่ข้าม ในกรณีที่เหตุการณ์ข้ามขอบเขต ระบบจะปรับเป้าหมายเหตุการณ์เพื่อรักษาการรวมกลุ่มที่ขอบเขตบนของรูทเงาให้อยู่ กล่าวคือ ระบบจะกําหนดเป้าหมายเหตุการณ์ใหม่ให้ดูเหมือนว่ามาจากองค์ประกอบโฮสต์ ไม่ใช่องค์ประกอบภายในของ Shadow DOM

เล่นการกระทำ 1

  • อันนี้น่าสนใจ คุณควรเห็น mouseout จากองค์ประกอบโฮสต์ (<div data-host>) ไปยังโหนดสีน้ำเงิน แม้ว่าจะเป็นโหนดแบบกระจาย แต่โหนดดังกล่าวจะยังคงอยู่ในโฮสต์ ไม่ใช่ ShadowDOM การเลื่อนเมาส์ลงไปจนเป็นสีเหลืองอีกครั้งจะทำให้เกิด mouseout บนโหนดสีน้ำเงิน

Play Action 2

  • มี mouseout 1 ตัวที่ปรากฏในโฮสต์ (ที่ส่วนท้ายสุด) โดยปกติแล้วคุณจะเห็นเหตุการณ์ mouseout ทริกเกอร์สำหรับบล็อกสีเหลืองทั้งหมด อย่างไรก็ตาม ในกรณีนี้ องค์ประกอบเหล่านี้อยู่ภายใน Shadow DOM และเหตุการณ์จะไม่ทวีไปตามขอบเขตด้านบน

เล่นการดําเนินการ 3

  • โปรดสังเกตว่าเมื่อคุณคลิกอินพุต focusin จะไม่ปรากฏในอินพุต แต่จะไม่ปรากฏในโหนดของโฮสต์ เปลี่ยนเป้าหมายแล้ว

เหตุการณ์ที่หยุดอยู่เสมอ

เหตุการณ์ต่อไปนี้จะไม่ข้ามขอบเขตเงา

  • ล้มเลิก
  • ข้อผิดพลาด
  • เลือก
  • เปลี่ยน
  • โหลด
  • รีเซ็ต
  • ปรับขนาด
  • scroll
  • selectstart

บทสรุป

เราหวังว่าคุณจะเห็นด้วยว่า Shadow DOM มีประสิทธิภาพอย่างมาก เรามีการบรรจุที่เหมาะสมเป็นครั้งแรกโดยไม่มี<iframe>หรือเทคนิคเก่าๆ อื่นๆ เพิ่มเติม

Shadow DOM เป็นเทคโนโลยีที่ซับซ้อน แต่ก็เป็นเทคโนโลยีที่ควรเพิ่มลงในแพลตฟอร์มเว็บ ลองใช้เวลาสักครู่ เรียนรู้ ถามคำถาม

หากต้องการดูข้อมูลเพิ่มเติม โปรดอ่านบทความแนะนำของ Dominic เรื่อง Shadow DOM 101 และบทความ Shadow DOM 201: CSS และการจัดสไตล์ของฉัน