แนวคิดขั้นสูงและ 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 อย่างเกี่ยวกับตัวอย่างนี้
- "Root 2 FTW" ยังคงแสดงผลสูงกว่า "Root 1 FTW" เนื่องจากตำแหน่งที่เราวางจุดแทรก
<shadow>
ไว้ หากต้องการย้อนกลับ ให้เลื่อนจุดแทรกroot2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';
- โปรดทราบว่าตอนนี้มีจุดแทรก
<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
เล่นการกระทำ 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 ของฉัน