Shadow DOM 301

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

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

การใช้ Shadow Root หลายรูท

หากคุณจะจัดปาร์ตี้อาจจะอัดแน่นไปด้วยเมื่อทุกคนอยู่ในห้องเดียวกัน คุณต้องการตัวเลือกสำหรับการกระจายกลุ่มคนในหลายๆ ห้อง องค์ประกอบที่โฮสต์ Shadow DOM ก็สามารถทำได้เช่นกัน กล่าวคือ องค์ประกอบสามารถโฮสต์ Shadow DOM ได้มากกว่า 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" แม้ว่าเราได้แนบเงาต้นไม้ไว้แล้วก็ตาม เนื่องจากเงาต้นไม้รายการสุดท้ายที่เพิ่มลงในโฮสต์ชนะ นี่เป็นสแต็ก 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-3 อย่างเกี่ยวกับตัวอย่างนี้

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

แสดงผลเป็นอย่างไรที่ <shadow>

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

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

การรับเงาของโฮสต์

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

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

หากกังวลว่าจะมีคนแอบมาอยู่ในเงามืด ให้กำหนด .shadowRoot ใหม่ให้เป็นค่าว่าง ดังนี้

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

เช่นเดียวกับ .getDistributedNodes() คุณจะตรวจสอบจุดแทรกที่กระจายโหนดได้โดยเรียกใช้ .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>

เครื่องมือ: Shadow DOM Visualizer

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

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

Shadow DOM Visualizer
เปิดตัว Shadow DOM Visualizer

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

โมเดลกิจกรรม

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

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

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

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

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

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

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

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

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

  • ล้มเลิก
  • error
  • เลือก
  • เปลี่ยน
  • โหลด
  • ตั้งค่าใหม่
  • resize
  • scroll
  • เลือกเริ่ม

บทสรุป

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

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

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