แนวคิดขั้นสูงและ DOM API
บทความนี้จะพูดถึงสิ่งน่าทึ่งอื่นๆ ที่คุณทำได้ด้วย Shadow DOM โดยต่อยอดจากแนวคิดที่กล่าวถึงใน Shadow DOM 101 และ Shadow DOM 201
การใช้ Shadow Root หลายรายการ
หากคุณจัดปาร์ตี้ บรรยากาศจะอึดอัดหากทุกคนเบียดเสียดกันอยู่ในห้องเดียวกัน คุณต้องการตัวเลือกในการกระจายกลุ่มคนไปยังห้องต่างๆ องค์ประกอบที่โฮสต์ Shadow DOM ก็ทำเช่นนี้ได้เช่นกัน กล่าวคือสามารถโฮสต์ Shadow Root ได้มากกว่า 1 รายการพร้อมกัน
มาดูกันว่าจะเกิดอะไรขึ้นหากเราพยายามแนบ Shadow Root หลายรายการกับโฮสต์
<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 ในแง่ของการแสดงผล การตรวจสอบ DevTools จะยืนยันลักษณะการทำงานนี้
แล้วการใช้เงาหลายเงาจะมีประโยชน์อะไรหากมีเพียงเงาสุดท้ายเท่านั้นที่ได้รับเชิญให้เข้าร่วมปาร์ตี้การแสดงผล ป้อนจุดแทรกเงา
จุดแทรกเงา
"จุดแทรกเงา" (<shadow>) คล้ายกับจุดแทรกปกติ (<content>) ตรงที่เป็นตัวยึดตำแหน่ง แต่แทนที่จะเป็นตัวยึดตำแหน่งสำหรับเนื้อหาของโฮสต์ องค์ประกอบเหล่านี้จะเป็นโฮสต์สำหรับ Shadow Tree อื่นๆ
นี่คือ 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 ประการ ได้แก่
- "Root 2 FTW" จะยังคงแสดงผลเหนือ "Root 1 FTW" เนื่องจากเราวางจุดแทรก
<shadow>ไว้ตรงนั้น หากต้องการทำตรงกันข้าม ให้ย้ายจุดแทรก:root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>'; - โปรดสังเกตว่าตอนนี้มีจุดแทรก
<content>ใน root1 ซึ่งจะทำให้โหนดข้อความ "Light DOM" แสดงผลไปด้วย
สิ่งที่แสดงผลที่ <shadow>
บางครั้งการทราบว่า Shadow Tree ที่เก่ากว่ากำลังแสดงผลที่ <shadow> ก็มีประโยชน์ คุณสามารถรับการอ้างอิงไปยังโครงสร้างนั้นผ่าน .olderShadowRoot:
**root2.olderShadowRoot** === root1 //true
การรับ Shadow Root ของโฮสต์
หากองค์ประกอบโฮสต์ Shadow DOM คุณจะเข้าถึง Shadow Root ที่อายุน้อยที่สุดได้โดยใช้ .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> ที่เพิ่มใหม่
การทำงานกับจุดแทรก
โหนดที่เลือกจากองค์ประกอบโฮสต์และ "กระจาย" ไปยัง Shadow Tree เรียกว่า…รัวกลอง…โหนดแบบกระจาย องค์ประกอบเหล่านี้ได้รับอนุญาตให้ข้ามขอบเขต Shadow เมื่อจุดแทรกเชิญให้เข้ามา
สิ่งที่แปลกประหลาดในเชิงแนวคิดเกี่ยวกับจุดแทรกคือจุดแทรกไม่ได้ย้าย DOM จริงๆ โหนดของโฮสต์จะยังคงอยู่เหมือนเดิม จุดแทรกเป็นเพียงการฉายโหนดซ้ำ
จากโฮสต์ไปยัง Shadow Tree เป็นเรื่องของการนำเสนอ/การแสดงผล: "ย้ายโหนดเหล่านี้มาที่นี่" "แสดงผลโหนดเหล่านี้ที่ตำแหน่งนี้"
เช่น
<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 Visualizer
การทำความเข้าใจ Shadow DOM ซึ่งเป็นเรื่องที่ซับซ้อนนั้นเป็นเรื่องยาก ฉันจำได้ว่าพยายามทำความเข้าใจ เรื่องนี้เป็นครั้งแรก
ฉันได้สร้างเครื่องมือโดยใช้ d3.js เพื่อช่วยให้เห็นภาพการทำงานของการแสดงผล Shadow DOM ช่องมาร์กอัปทั้ง 2 ช่องทางด้านซ้ายมือแก้ไขได้ คุณสามารถวางมาร์กอัปของคุณเองและลองเล่นเพื่อดูว่าสิ่งต่างๆ ทำงานอย่างไร และจุดแทรกจะสลับโหนดโฮสต์เข้าไปใน Shadow Tree
ลองใช้แล้วบอกเราว่าคุณคิดอย่างไร
โมเดลเหตุการณ์
เหตุการณ์บางอย่างข้ามขอบเขต Shadow และบางเหตุการณ์ไม่ข้าม ในกรณีที่เหตุการณ์ข้ามขอบเขต เป้าหมายเหตุการณ์จะได้รับการปรับเพื่อรักษาการห่อหุ้มที่ขอบเขตบนของ Shadow Root มีให้ กล่าวคือ ระบบจะกำหนดเป้าหมายเหตุการณ์ใหม่ให้ดูเหมือนว่ามาจากองค์ประกอบโฮสต์แทนที่จะเป็นองค์ประกอบภายในของ Shadow DOM
Play Action 1
- อันนี้น่าสนใจ คุณควรเห็น
mouseoutจากองค์ประกอบโฮสต์ (<div data-host>) ไปยังโหนดสีน้ำเงิน แม้ว่าจะเป็นโหนดแบบกระจาย แต่ก็ยังอยู่ในโฮสต์ ไม่ใช่ ShadowDOM การเลื่อนเมาส์ลงไปในสีเหลืองอีกครั้งจะทำให้เกิดmouseoutบนโหนดสีน้ำเงิน
Play Action 2
- มี
mouseout1 รายการที่ปรากฏในโฮสต์ (ที่ส่วนท้ายสุด) โดยปกติแล้ว คุณจะเห็นเหตุการณ์mouseoutทริกเกอร์สำหรับบล็อกสีเหลืองทั้งหมด อย่างไรก็ตาม ในกรณีนี้องค์ประกอบเหล่านี้อยู่ภายใน Shadow DOM และเหตุการณ์จะไม่ส่งผลกระทบผ่านขอบเขตด้านบน
Play Action 3
- โปรดสังเกตว่าเมื่อคุณคลิกอินพุต
focusinจะไม่ปรากฏในอินพุต แต่จะปรากฏในโหนดโฮสต์เอง กำหนดเป้าหมายใหม่แล้ว
เหตุการณ์ที่หยุดเสมอ
เหตุการณ์ต่อไปนี้จะไม่ข้ามขอบเขต Shadow
- ล้มเลิก
- ข้อผิดพลาด
- เลือก
- เปลี่ยน
- คาร์ดิโอ
- รีเซ็ต
- ปรับขนาด
- scroll
- selectstart
บทสรุป
หวังว่าคุณจะเห็นด้วยว่า Shadow DOM มีประสิทธิภาพอย่างมาก เป็นครั้งแรกที่เรามีการห่อหุ้มที่เหมาะสมโดยไม่มีภาระเพิ่มเติมของ <iframe> หรือเทคนิคอื่นๆ ที่เก่ากว่า
Shadow DOM เป็นสิ่งที่ซับซ้อนอย่างแน่นอน แต่ก็เป็นสิ่งที่คุ้มค่าที่จะเพิ่มลงในแพลตฟอร์มเว็บ ใช้เวลากับเรื่องนี้สักหน่อย เรียนรู้เลย ตั้งคำถาม
หากต้องการดูข้อมูลเพิ่มเติม โปรดอ่านบทความแนะนำของ Dominic เรื่อง Shadow DOM 101 และบทความของฉันเรื่อง Shadow DOM 201: CSS และการจัดรูปแบบ