Shadow DOM ช่วยให้นักพัฒนาเว็บสร้าง DOM และ CSS ที่แบ่งออกเป็นส่วนๆ สําหรับคอมโพเนนต์เว็บได้
สรุป
Shadow DOM ช่วยขจัดข้อจำกัดของการสร้างเว็บแอป ความเปราะบางนี้มาจากลักษณะแบบทั่วโลกของ HTML, CSS และ JS ตลอดหลายปีที่ผ่านมา เราได้คิดค้นเครื่องมือ
จำนวน
มากมายเพื่อหลีกเลี่ยงปัญหา ตัวอย่างเช่น เมื่อคุณใช้รหัส/คลาส HTML ใหม่ คุณจะไม่ทราบเลยว่ารหัส/คลาสดังกล่าวจะขัดแย้งกับชื่อที่มีอยู่ซึ่งหน้าเว็บใช้อยู่หรือไม่
ข้อบกพร่องเล็กๆ น้อยๆ เกิดขึ้น ความเป็นเฉพาะเจาะจงของ CSS กลายเป็นปัญหาใหญ่ (!important ทุกอย่าง) ตัวเลือกสไตล์ควบคุมไม่ได้ และประสิทธิภาพอาจลดลง รายการนี้ยังมีอีกมากมาย
Shadow DOM แก้ไข CSS และ DOM มาตรฐานนี้นําสไตล์ที่มีขอบเขตมาสู่แพลตฟอร์มเว็บ หากไม่มีเครื่องมือหรือรูปแบบการตั้งชื่อ คุณจะรวม CSS กับมาร์กอัป ซ่อนรายละเอียดการใช้งาน และคอมโพเนนต์แบบสําเร็จรูปของผู้เขียนใน JavaScript เวอร์ชันมาตรฐานได้
บทนำ
Shadow DOM เป็นหนึ่งในมาตรฐาน Web Components 3 รายการ ได้แก่ เทมเพลต HTML, Shadow DOM และองค์ประกอบที่กําหนดเอง การนําเข้า HTML เคยอยู่ในรายการนี้ แต่ตอนนี้ถือว่าเลิกใช้งานแล้ว
คุณไม่จำเป็นต้องเขียนคอมโพเนนต์เว็บที่ใช้ Shadow DOM แต่เมื่อใช้ คุณจะใช้ประโยชน์จากข้อดีต่างๆ (การกำหนดขอบเขต CSS, การรวม DOM, การจัดองค์ประกอบ) และสร้างองค์ประกอบที่กำหนดเองซึ่งนำมาใช้ซ้ำได้ ยืดหยุ่น กำหนดค่าได้สูง และนํามาใช้ซ้ำได้อย่างมาก หากองค์ประกอบที่กําหนดเองเป็นวิธีสร้าง HTML ใหม่ (ด้วย JS API) Shadow DOM คือวิธีระบุ HTML และ CSS API 2 รายการนี้รวมกันเพื่อสร้างคอมโพเนนต์ที่มี HTML, CSS และ JavaScript ในตัว
Shadow DOM ออกแบบมาเพื่อเป็นเครื่องมือในการสร้างแอปที่อิงตามคอมโพเนนต์ ดังนั้น แพลตฟอร์มนี้จึงช่วยแก้ปัญหาที่พบได้ทั่วไปในการพัฒนาเว็บ ดังนี้
- DOM ที่แยกออกมา: DOM ของคอมโพเนนต์จะทำงานได้ด้วยตัวเอง (เช่น
document.querySelector()จะไม่แสดงผลโหนดใน Shadow DOM ของคอมโพเนนต์) - CSS ที่มีขอบเขต: CSS ที่กําหนดภายใน Shadow DOM จะมีขอบเขตอยู่ใน Shadow DOM กฎสไตล์จะไม่แสดงในหน้าอื่นๆ และสไตล์หน้าเว็บจะไม่แสดงในหน้าอื่นๆ
- การคอมโพสิชัน: ออกแบบ API แบบประกาศที่ใช้มาร์กอัปสำหรับคอมโพเนนต์
- ลดความซับซ้อนของ CSS - DOM ที่มีขอบเขตช่วยให้คุณใช้ตัวเลือก CSS ง่ายๆ, ชื่อรหัส/คลาสทั่วไปได้มากขึ้น และไม่ต้องกังวลว่าจะเกิดการทับซ้อนของชื่อ
- ประสิทธิภาพการทำงาน - นึกถึงแอปเป็นกลุ่ม DOM แทนที่จะเป็นหน้าเว็บขนาดใหญ่หน้าเดียว (ส่วนกลาง)
fancy-tabs demo
ตลอดทั้งบทความนี้ เราจะอ้างอิงถึงคอมโพเนนต์สาธิต (<fancy-tabs>) และข้อมูลโค้ดที่อ้างอิงจากคอมโพเนนต์ดังกล่าว หากเบราว์เซอร์ของคุณรองรับ API คุณจะเห็นการสาธิตการใช้งานจริงของ API ด้านล่าง หรือดูซอร์สโค้ดแบบเต็มใน GitHub
Shadow DOM คืออะไร
ข้อมูลเบื้องต้นเกี่ยวกับ DOM
HTML เป็นหัวใจสำคัญของเว็บเนื่องจากใช้งานง่าย การประกาศแท็ก 2-3 รายการจะช่วยให้คุณเขียนหน้าเว็บที่มีทั้งการแสดงผลและโครงสร้างได้ในไม่กี่วินาที อย่างไรก็ตาม HTML เพียงอย่างเดียวนั้นไม่ค่อยมีประโยชน์ มนุษย์เข้าใจภาษาที่เป็นข้อความได้ง่ายๆ แต่เครื่องจักรต้องการมากกว่านั้น ป้อน Document Object Model หรือ DOM
เมื่อเบราว์เซอร์โหลดหน้าเว็บ จะมีการดําเนินการที่น่าสนใจหลายอย่าง หนึ่งในสิ่งที่เครื่องมือนี้ทําคือเปลี่ยน HTML ของผู้เขียนให้เป็นเอกสารแบบเรียลไทม์ โดยพื้นฐานแล้ว เบราว์เซอร์จะแยกวิเคราะห์ HTML (สตริงข้อความแบบคงที่) เป็นโมเดลข้อมูล (ออบเจ็กต์/โหนด) เพื่อให้เข้าใจโครงสร้างของหน้า เบราว์เซอร์จะเก็บลําดับชั้นของ HTML ไว้ด้วยการสร้างแผนผังโหนดเหล่านี้ ซึ่งก็คือ DOM สิ่งที่ยอดเยี่ยมเกี่ยวกับ DOM คือการแสดงหน้าเว็บแบบเรียลไทม์ โหนดที่เบราว์เซอร์สร้างขึ้นจะมีพร็อพเพอร์ตี้ เมธอด และที่สำคัญที่สุดคือสามารถควบคุมโดยโปรแกรมได้ ซึ่งแตกต่างจาก HTML แบบคงที่ที่เราเขียน ด้วยเหตุนี้ เราจึงสร้างองค์ประกอบ DOM ได้โดยตรงโดยใช้ JavaScript
const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);
แสดงผลมาร์กอัป HTML ต่อไปนี้
<body>
<header>
<h1>Hello DOM</h1>
</header>
</body>
เยี่ยมไปเลย แล้วShadow DOM คืออะไร
DOM… ในส่วนมืด
Shadow DOM คือ DOM ปกติที่มีความแตกต่างกัน 2 อย่าง ได้แก่ 1) วิธีสร้าง/ใช้งาน และ 2) ลักษณะการทํางานเมื่อเทียบกับส่วนที่เหลือของหน้า โดยปกติแล้ว คุณจะต้องสร้างโหนด DOM และเพิ่มโหนดเหล่านั้นเป็นองค์ประกอบย่อยขององค์ประกอบอื่น เมื่อใช้ Shadow DOM คุณจะสร้างแผนผัง DOM ที่มีขอบเขตซึ่งแนบอยู่กับองค์ประกอบ แต่แยกจากองค์ประกอบย่อยจริง ต้นไม้ย่อยที่มีขอบเขตนี้เรียกว่าต้นไม้เงา องค์ประกอบที่เกาะอยู่คือโฮสต์เงา ทุกอย่างที่คุณเพิ่มในเงาจะกลายเป็นข้อมูลในเครื่องขององค์ประกอบโฮสติ้ง ซึ่งรวมถึง <style> นี่คือวิธีที่ Shadow DOM กำหนดขอบเขตสไตล์ CSS
การสร้าง Shadow DOM
รูทเงาคือชิ้นส่วนเอกสารที่แนบอยู่กับองค์ประกอบ "โฮสต์"
การแนบรูทเงาเป็นวิธีที่องค์ประกอบได้รับ Shadow DOM หากต้องการสร้าง Shadow DOM สําหรับองค์ประกอบ ให้เรียกใช้ element.attachShadow() ดังนี้
const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().
// header.shadowRoot === shadowRoot
// shadowRoot.host === header
เราใช้ .innerHTML เพื่อกรอกข้อมูลรูทเงา แต่คุณก็ใช้ DOM API อื่นๆ ได้ด้วย นี่คือเว็บ เราเลือกได้
ข้อกําหนดจะกําหนดรายการองค์ประกอบที่โฮสต์ต้นไม้เงาไม่ได้ องค์ประกอบอาจอยู่ในรายการเนื่องจากสาเหตุหลายประการ ดังนี้
- เบราว์เซอร์โฮสต์ Shadow DOM ภายในของตัวเองสําหรับองค์ประกอบนั้นอยู่แล้ว (
<textarea>,<input>) - องค์ประกอบไม่ควรโฮสต์ Shadow DOM (
<img>)
ตัวอย่างเช่น ข้อความต่อไปนี้ใช้ไม่ได้
document.createElement('input').attachShadow({mode: 'open'});
// Error. `<input>` cannot host shadow dom.
การสร้าง Shadow DOM สําหรับองค์ประกอบที่กําหนดเอง
Shadow DOM มีประโยชน์อย่างยิ่งเมื่อสร้างองค์ประกอบที่กําหนดเอง ใช้ Shadow DOM เพื่อแบ่งส่วน HTML, CSS และ JS ขององค์ประกอบ ซึ่งจะทำให้เกิด "คอมโพเนนต์เว็บ"
ตัวอย่าง - เอลิเมนต์ที่กําหนดเองแนบ Shadow DOM กับตนเอง โดยรวม DOM/CSS ไว้ดังนี้
// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// Attach a shadow root to <fancy-tabs>.
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
<div id="tabs">...</div>
<div id="panels">...</div>
`;
}
...
});
มีสิ่งที่น่าสนใจ 2-3 อย่างเกิดขึ้น ประการแรกคือ องค์ประกอบที่กําหนดเองจะสร้าง Shadow DOM ของตัวเองเมื่อสร้างอินสแตนซ์ของ <fancy-tabs> ซึ่งทำได้ใน constructor() ประการที่ 2 เนื่องจากเรากําลังสร้างรูทเงา กฎ CSS ภายใน <style> จะมีขอบเขตเป็น <fancy-tabs>
การประพันธ์เพลงและช่อง
การคอมโพสิชันเป็นหนึ่งในฟีเจอร์ของ Shadow DOM ที่เข้าใจยากที่สุด แต่เป็นหนึ่งในฟีเจอร์ที่สำคัญที่สุด
ในโลกของการพัฒนาเว็บ องค์ประกอบคือวิธีที่เราสร้างแอปจาก HTML องค์ประกอบต่างๆ (<div>, <header>,
<form>, <input>) มารวมกันเป็นแอป แท็กบางรายการยังทำงานร่วมกันได้ด้วย องค์ประกอบแบบเนทีฟอย่าง <select>, <details>, <form> และ <video> จึงมีความยืดหยุ่นมาก แท็กแต่ละรายการยอมรับ HTML บางรายการเป็นองค์ประกอบย่อยและทำสิ่งพิเศษกับองค์ประกอบย่อยเหล่านั้น ตัวอย่างเช่น <select> รู้วิธีแสดงผล <option> และ <optgroup> เป็นวิดเจ็ตเมนูแบบเลื่อนลงและวิดเจ็ตแบบเลือกหลายรายการ องค์ประกอบ <details> จะแสดงผล <summary> เป็นลูกศรที่ขยายได้ แม้แต่ <video> ก็ยังรู้วิธีจัดการกับองค์ประกอบย่อยบางรายการ นั่นคือ ระบบจะไม่แสดงผลองค์ประกอบ <source> แต่องค์ประกอบดังกล่าวจะส่งผลต่อลักษณะการทํางานของวิดีโอ
สุดยอดไปเลย
คําศัพท์: Light DOM กับ Shadow DOM
การคอมโพสิชัน Shadow DOM นำเสนอพื้นฐานใหม่ๆ มากมายในการพัฒนาเว็บ ก่อนจะลงรายละเอียด เรามากำหนดมาตรฐานคำศัพท์กันก่อนเพื่อให้เข้าใจตรงกัน
Light DOM
มาร์กอัปที่ผู้ใช้คอมโพเนนต์เขียน DOM นี้อยู่นอก Shadow DOM ของคอมโพเนนต์ นั่นคือองค์ประกอบย่อยจริงขององค์ประกอบ
<better-button>
<!-- the image and span are better-button's light DOM -->
<img src="gear.svg" slot="icon">
<span>Settings</span>
</better-button>
Shadow DOM
DOM ที่ผู้เขียนคอมโพเนนต์เขียน Shadow DOM อยู่ภายในคอมโพเนนต์และกำหนดโครงสร้างภายใน CSS ที่มีขอบเขต และรวมรายละเอียดการใช้งาน นอกจากนี้ยังกำหนดวิธีแสดงผลมาร์กอัปที่เขียนโดยผู้ใช้คอมโพเนนต์ของคุณได้ด้วย
#shadow-root
<style>...</style>
<slot name="icon"></slot>
<span id="wrapper">
<slot>Button</slot>
</span>
แผนผัง DOM ที่ผสาน
ผลลัพธ์ของเบราว์เซอร์ที่กระจาย Light DOM ของผู้ใช้ไปยัง Shadow DOM ของคุณ ซึ่งจะแสดงผลลัพธ์สุดท้าย แผนภูมิแบบแบนคือสิ่งที่คุณเห็นในท้ายที่สุดในเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์และสิ่งที่แสดงผลในหน้าเว็บ
<better-button>
#shadow-root
<style>...</style>
<slot name="icon">
<img src="gear.svg" slot="icon">
</slot>
<span id="wrapper">
<slot>
<span>Settings</span>
</slot>
</span>
</better-button>
องค์ประกอบ <slot>
Shadow DOM จะประกอบทรี DOM ต่างๆ เข้าด้วยกันโดยใช้องค์ประกอบ <slot>
สล็อตคือตัวยึดตําแหน่งในคอมโพเนนต์ที่ผู้ใช้สามารถกรอกมาร์กอัปของตนเอง การกําหนดช่องอย่างน้อย 1 ช่องเป็นการเชิญให้มาร์กอัปภายนอกแสดงผลใน Shadow DOM ของคอมโพเนนต์ พูดง่ายๆ คือคุณกําลังพูดว่า "แสดงผลมาร์กอัปของผู้ใช้ที่นี่"
ระบบอนุญาตให้องค์ประกอบ "ข้าม" ขอบเขต Shadow DOM ได้เมื่อ <slot> เชิญให้เข้ามา องค์ประกอบเหล่านี้เรียกว่าโหนดที่กระจายอยู่ แนวคิดของโหนดแบบกระจายอาจดูแปลกไปสักหน่อย สล็อตไม่ได้ย้าย DOM ไปทางกายภาพ แต่แสดงผล DOM นั้นในตำแหน่งอื่นภายใน Shadow DOM
คอมโพเนนต์สามารถกำหนดช่องได้ตั้งแต่ 0 ช่องขึ้นไปใน Shadow DOM ช่องอาจเป็นค่าว่างหรือมีเนื้อหาสำรองก็ได้ หากผู้ใช้ไม่ได้ระบุเนื้อหา Light DOM สล็อตจะแสดงผลเนื้อหาสำรอง
<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>
<slot>fallback content</slot> <!-- default slot with fallback content -->
<slot> <!-- default slot entire DOM tree as fallback -->
<h2>Title</h2>
<summary>Description text</summary>
</slot>
นอกจากนี้ คุณยังสร้างช่วงเวลาที่ตั้งชื่อได้ด้วย ช่องที่มีชื่อคือช่องเฉพาะใน Shadow DOM ที่ผู้ใช้อ้างอิงตามชื่อ
ตัวอย่าง - ช่องใน Shadow DOM ของ <fancy-tabs>
#shadow-root
<div id="tabs">
<slot id="tabsSlot" name="title"></slot> <!-- named slot -->
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
ผู้ใช้คอมโพเนนต์จะประกาศ <fancy-tabs> ดังนี้
<fancy-tabs>
<button slot="title">Title</button>
<button slot="title" selected>Title 2</button>
<button slot="title">Title 3</button>
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</fancy-tabs>
<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
<h2 slot="title">Title</h2>
<section>content panel 1</section>
<h2 slot="title" selected>Title 2</h2>
<section>content panel 2</section>
<h2 slot="title">Title 3</h2>
<section>content panel 3</section>
</fancy-tabs>
และหากคุณสงสัยว่าแผนภูมิต้นไม้แบบแบนจะมีลักษณะเป็นอย่างไร โปรดดูภาพต่อไปนี้
<fancy-tabs>
#shadow-root
<div id="tabs">
<slot id="tabsSlot" name="title">
<button slot="title">Title</button>
<button slot="title" selected>Title 2</button>
<button slot="title">Title 3</button>
</slot>
</div>
<div id="panels">
<slot id="panelsSlot">
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</slot>
</div>
</fancy-tabs>
โปรดทราบว่าคอมโพเนนต์ของเราจัดการการกำหนดค่าต่างๆ ได้ แต่ต้นไม้ DOM ที่ยุบแล้วจะยังคงเหมือนเดิม เราเปลี่ยนจาก <button> เป็น <h2> ได้ด้วย คอมโพเนนต์นี้เขียนขึ้นเพื่อจัดการกับเด็กประเภทต่างๆ เช่นเดียวกับที่ <select> ทำ
การจัดรูปแบบ
การจัดสไตล์คอมโพเนนต์เว็บทำได้หลายวิธี คอมโพเนนต์ที่ใช้ Shadow DOM สามารถกำหนดสไตล์โดยหน้าหลัก กำหนดสไตล์ของตัวเอง หรือระบุฮุก (ในรูปแบบพร็อพเพอร์ตี้ที่กำหนดเองของ CSS) เพื่อให้ผู้ใช้ลบล้างค่าเริ่มต้นได้
สไตล์ที่คอมโพเนนต์กําหนด
ฟีเจอร์ที่มีประโยชน์ที่สุดของ Shadow DOM คือ CSS แบบจำกัดขอบเขต
- ตัวเลือก CSS จากหน้าด้านนอกจะไม่มีผลกับภายในคอมโพเนนต์
- สไตล์ที่กําหนดไว้ภายในจะไม่ตัดขอบ แต่จะมีผลกับองค์ประกอบโฮสต์
ตัวเลือก CSS ที่ใช้ภายใน Shadow DOM จะมีผลกับคอมโพเนนต์ของคุณในเครื่อง ในทางปฏิบัติแล้ว หมายความว่าเราสามารถใช้รหัส/ชื่อคลาสทั่วไปซ้ำได้โดยไม่ต้องกังวลว่าจะทับซ้อนกับส่วนอื่นๆ ในหน้า ตัวเลือก CSS ที่เรียบง่ายขึ้นเป็นแนวทางปฏิบัติแนะนำภายใน Shadow DOM และยังส่งผลดีต่อประสิทธิภาพด้วย
ตัวอย่าง - สไตล์ที่กําหนดในรูทเงาจะเป็นสไตล์ภายใน
#shadow-root
<style>
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
...
}
#tabs {
display: inline-flex;
...
}
</style>
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
สไตล์ชีตยังมีขอบเขตที่ต้นไม้เงาด้วย
#shadow-root
<link rel="stylesheet" href="styles.css">
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
เคยสงสัยไหมว่าองค์ประกอบ <select> แสดงผลวิดเจ็ตแบบเลือกหลายรายการ (แทนที่จะเป็นเมนูแบบเลื่อนลง) ได้อย่างไรเมื่อคุณเพิ่มแอตทริบิวต์ multiple
<select multiple>
<option>Do</option>
<option selected>Re</option>
<option>Mi</option>
<option>Fa</option>
<option>So</option>
</select>
<select> สามารถจัดรูปแบบตัวเองให้แตกต่างกันไปตามแอตทริบิวต์ที่คุณประกาศ คอมโพเนนต์เว็บยังกำหนดสไตล์ของตนเองได้ด้วยโดยใช้ตัวเลือก :host
ตัวอย่าง - การจัดสไตล์คอมโพเนนต์เอง
<style>
:host {
display: block; /* by default, custom elements are display: inline */
contain: content; /* CSS containment FTW. */
}
</style>
ข้อควรระวังอย่างหนึ่งเกี่ยวกับ :host คือกฎในหน้าหลักมีความเฉพาะเจาะจงสูงกว่ากฎ :host ที่กําหนดไว้ในองค์ประกอบ กล่าวคือ รูปแบบภายนอกจะชนะ ซึ่งจะช่วยให้ผู้ใช้ลบล้างการจัดรูปแบบระดับบนสุดจากภายนอกได้ นอกจากนี้ :host จะใช้งานได้ในบริบทของ Shadow Root เท่านั้น คุณจึงใช้ :host นอก Shadow DOM ไม่ได้
รูปแบบฟังก์ชันของ :host(<selector>) ช่วยให้คุณกําหนดเป้าหมายไปยังโฮสต์ได้หากตรงกับ <selector> วิธีนี้เป็นวิธีที่ยอดเยี่ยมในการทำให้คอมโพเนนต์รวมพฤติกรรมที่ตอบสนองต่อการโต้ตอบของผู้ใช้ หรือสถานะ หรือสไตล์ของโหนดภายในตามโฮสต์
<style>
:host {
opacity: 0.4;
will-change: opacity;
transition: opacity 300ms ease-in-out;
}
:host(:hover) {
opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
background: grey;
pointer-events: none;
opacity: 0.4;
}
:host(.blue) {
color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>
การจัดรูปแบบตามบริบท
:host-context(<selector>) จะจับคู่กับคอมโพเนนต์หากคอมโพเนนต์นั้นหรือบรรพบุรุษของคอมโพเนนต์ตรงกับ <selector> การใช้งานทั่วไปคือการกำหนดธีมตามบริบทของคอมโพเนนต์ ตัวอย่างเช่น ผู้ใช้จํานวนมากใช้ธีมโดยการใช้คลาสกับ <html> หรือ <body>
<body class="darktheme">
<fancy-tabs>
...
</fancy-tabs>
</body>
:host-context(.darktheme) จะจัดรูปแบบ <fancy-tabs> เมื่อเป็นรายการที่สืบทอดมาจาก .darktheme ดังนี้
:host-context(.darktheme) {
color: white;
background: black;
}
:host-context() อาจมีประโยชน์สำหรับธีม แต่วิธีที่ดีกว่าคือสร้างฮุกรูปแบบโดยใช้พร็อพเพอร์ตี้ที่กำหนดเองของ CSS
จัดสไตล์โหนดที่กระจาย
::slotted(<compound-selector>) จับคู่โหนดที่กระจายอยู่ใน
<slot>
สมมติว่าเราสร้างคอมโพเนนต์ป้ายชื่อแล้ว
<name-badge>
<h2>Eric Bidelman</h2>
<span class="title">
Digital Jedi, <span class="company">Google</span>
</span>
</name-badge>
Shadow DOM ของคอมโพเนนต์สามารถกำหนดสไตล์ <h2> และ .title ของผู้ใช้ได้ ดังนี้
<style>
::slotted(h2) {
margin: 0;
font-weight: 300;
color: red;
}
::slotted(.title) {
color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
text-transform: uppercase;
}
*/
</style>
<slot></slot>
ดังที่ทราบกันก่อนหน้านี้ <slot> จะไม่ย้าย DOM ของแสงของผู้ใช้ เมื่อมีการกระจายโหนดไปยัง <slot> <slot> จะแสดงผล DOM แต่โหนดจะยังคงอยู่ที่เดิม สไตล์ที่ใช้ก่อนการเผยแพร่จะยังคงมีผลหลังจากการเผยแพร่ อย่างไรก็ตาม เมื่อมีการเผยแพร่ Light DOM นั้น สามารถรับรูปแบบเพิ่มเติมได้ (รูปแบบที่ Shadow DOM กำหนด)
ตัวอย่างที่ละเอียดยิ่งขึ้นอีกตัวอย่างหนึ่งจาก <fancy-tabs>
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
border-radius: 3px;
padding: 16px;
height: 250px;
overflow: auto;
}
#tabs {
display: inline-flex;
-webkit-user-select: none;
user-select: none;
}
#tabsSlot::slotted(*) {
font: 400 16px/22px 'Roboto';
padding: 16px 8px;
...
}
#tabsSlot::slotted([aria-selected="true"]) {
font-weight: 600;
background: white;
box-shadow: none;
}
#panelsSlot::slotted([aria-hidden="true"]) {
display: none;
}
</style>
<div id="tabs">
<slot id="tabsSlot" name="title"></slot>
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
`;
ในตัวอย่างนี้ มี 2 ช่อง ได้แก่ ช่องที่มีชื่อสำหรับชื่อแท็บ และช่องสำหรับเนื้อหาของแผงแท็บ เมื่อผู้ใช้เลือกแท็บ เราจะไฮไลต์การเลือกของผู้ใช้เป็นตัวหนาและแสดงแผงแท็บนั้น ซึ่งทำได้โดยการเลือกโหนดที่กระจายซึ่งมีแอตทริบิวต์ selected JS ขององค์ประกอบที่กําหนดเอง (ไม่ได้แสดงที่นี่) จะเพิ่มแอตทริบิวต์นั้นในเวลาที่เหมาะสม
จัดแต่งคอมโพเนนต์จากภายนอก
การจัดสไตล์คอมโพเนนต์จากภายนอกทำได้ 2 วิธี วิธีที่ง่ายที่สุดคือการใช้ชื่อแท็กเป็นตัวเลือก
fancy-tabs {
width: 500px;
color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
box-shadow: 0 3px 3px #ccc;
}
สไตล์ภายนอกจะมีความสำคัญเหนือกว่าสไตล์ที่กําหนดใน Shadow DOM เสมอ ตัวอย่างเช่น หากผู้ใช้เขียนตัวเลือก fancy-tabs { width: 500px; } ตัวเลือกนี้จะลบล้างกฎของคอมโพเนนต์ :host { width: 650px;}
การจัดสไตล์คอมโพเนนต์เพียงอย่างเดียวนั้นไม่เพียงพอ แต่จะเกิดอะไรขึ้นหากคุณต้องการจัดสไตล์ภายในของคอมโพเนนต์ ซึ่งต้องใช้พร็อพเพอร์ตี้ที่กำหนดเองของ CSS
การสร้างฮุกสไตล์โดยใช้พร็อพเพอร์ตี้ที่กำหนดเองของ CSS
ผู้ใช้สามารถปรับแต่งสไตล์ภายในได้หากผู้เขียนคอมโพเนนต์ระบุฮุกการจัดรูปแบบโดยใช้พร็อพเพอร์ตี้ที่กำหนดเองของ CSS แนวคิดนี้คล้ายกับ <slot> คุณสร้าง "ตัวยึดตําแหน่งสไตล์" เพื่อให้ผู้ใช้ลบล้าง
ตัวอย่าง - <fancy-tabs> อนุญาตให้ผู้ใช้ลบล้างสีพื้นหลังได้
<!-- main page -->
<style>
fancy-tabs {
margin-bottom: 32px;
--fancy-tabs-bg: black;
}
</style>
<fancy-tabs background>...</fancy-tabs>
ภายใน Shadow DOM
:host([background]) {
background: var(--fancy-tabs-bg, #9E9E9E);
border-radius: 10px;
padding: 10px;
}
ในกรณีนี้ คอมโพเนนต์จะใช้ black เป็นค่าพื้นหลังเนื่องจากผู้ใช้ระบุไว้ มิเช่นนั้นค่าเริ่มต้นจะเป็น #9E9E9E
หัวข้อขั้นสูง
การสร้างรูทเงาแบบปิด (ควรหลีกเลี่ยง)
Shadow DOM ยังมีอีกรูปแบบหนึ่งที่เรียกว่าโหมด "ปิด" เมื่อคุณสร้างทรี Shadow แบบปิด JavaScript ภายนอกจะเข้าถึง DOM ภายในของคอมโพเนนต์ไม่ได้ ซึ่งคล้ายกับวิธีการทำงานขององค์ประกอบเนทีฟ เช่น <video>
JavaScript เข้าถึง Shadow DOM ของ <video> ไม่ได้เนื่องจากเบราว์เซอร์ใช้ Shadow Root แบบโหมดปิด
ตัวอย่าง - การสร้างต้นไม้ที่มีเงาปิด
const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div
API อื่นๆ ยังได้รับผลกระทบจากโหมดปิดด้วย
Element.assignedSlot/TextNode.assignedSlotแสดงผลnullEvent.composedPath()สำหรับเหตุการณ์ที่เชื่อมโยงกับองค์ประกอบภายใน Shadow DOM จะแสดงผลเป็น []
สรุปเหตุผลที่คุณไม่ควรสร้างคอมโพเนนต์เว็บด้วย {mode: 'closed'} มีดังนี้
ความรู้สึกปลอดภัยที่เกิดจากการหลอกลวง ไม่มีอะไรหยุดผู้โจมตีจากการลักลอบใช้
Element.prototype.attachShadowได้โหมดปิดจะป้องกันไม่ให้โค้ดองค์ประกอบที่กําหนดเองเข้าถึง Shadow DOM ของตัวเอง ไม่ได้ผลเลย แต่คุณจะต้องเก็บข้อมูลอ้างอิงไว้ใช้ภายหลังหากต้องการใช้สิ่งต่างๆ เช่น
querySelector()ซึ่งทำให้โหมดปิดเสียจุดประสงค์เดิมไปโดยสิ้นเชิงcustomElements.define('x-element', class extends HTMLElement { constructor() { super(); // always call super() first in the constructor. this._shadowRoot = this.attachShadow({mode: 'closed'}); this._shadowRoot.innerHTML = '<div class="wrapper"></div>'; } connectedCallback() { // When creating closed shadow trees, you'll need to stash the shadow root // for later if you want to use it again. Kinda pointless. const wrapper = this._shadowRoot.querySelector('.wrapper'); } ... });โหมดปิดจะทำให้คอมโพเนนต์ยืดหยุ่นน้อยลงสำหรับผู้ใช้ปลายทาง เมื่อคุณสร้างคอมโพเนนต์เว็บ บางครั้งคุณอาจลืมเพิ่มฟีเจอร์ ตัวเลือกการกําหนดค่า กรณีการใช้งานที่ผู้ใช้ต้องการ ตัวอย่างที่พบบ่อยคือ การลืมใส่ฮุกการจัดสไตล์ที่เพียงพอสําหรับโหนดภายใน เมื่อใช้โหมดปิด ผู้ใช้จะลบล้างค่าเริ่มต้นและปรับแต่งสไตล์ไม่ได้ การเข้าถึงข้อมูลภายในของคอมโพเนนต์มีประโยชน์มาก ท้ายที่สุดแล้ว ผู้ใช้จะแยกคอมโพเนนต์ของคุณ ค้นหาคอมโพเนนต์อื่น หรือสร้างคอมโพเนนต์ของตนเองหากคอมโพเนนต์ของคุณไม่ทําในสิ่งที่ต้องการ :(
การทำงานกับสล็อตใน JS
Shadow DOM API มียูทิลิตีสำหรับการทำงานกับช่องและโหนดที่กระจาย ซึ่งจะมีประโยชน์เมื่อเขียนองค์ประกอบที่กําหนดเอง
เหตุการณ์ slotchange
เหตุการณ์ slotchange จะเริ่มต้นเมื่อโหนดที่กระจายของช่องมีการเปลี่ยนแปลง เช่น หากผู้ใช้เพิ่ม/นํารายการย่อยออกจาก DOM เบา
const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
console.log('light dom children changed!');
});
หากต้องการตรวจสอบการเปลี่ยนแปลงประเภทอื่นๆ ใน DOM แบบเบา คุณสามารถตั้งค่า MutationObserver ในคอนสตรคเตอร์ขององค์ประกอบ
องค์ประกอบใดบ้างที่แสดงผลในช่อง
บางครั้งการทราบว่าองค์ประกอบใดเชื่อมโยงกับช่องหนึ่งๆ นั้นมีประโยชน์ โทรไปที่
slot.assignedNodes() เพื่อดูว่าองค์ประกอบใดที่ช่องแสดงผล ตัวเลือก {flatten: true} จะแสดงเนื้อหาสำรองของช่องด้วย (หากไม่มีการจัดจำหน่ายโหนด)
ตัวอย่างเช่น สมมติว่า Shadow DOM มีลักษณะดังนี้
<slot><b>fallback content</b></slot>
| การใช้งาน | โทร | ผลลัพธ์ |
|---|---|---|
| <my-component>component text</my-component> | slot.assignedNodes(); |
[component text] |
| <my-component></my-component> | slot.assignedNodes(); |
[] |
| <my-component></my-component> | slot.assignedNodes({flatten: true}); |
[<b>fallback content</b>] |
องค์ประกอบได้รับการกําหนดให้กับช่องใด
คุณสามารถตอบคำถามย้อนกลับได้ด้วย element.assignedSlot จะบอกคุณว่าองค์ประกอบของคุณกำหนดให้กับช่องคอมโพเนนต์ใด
รูปแบบเหตุการณ์ Shadow DOM
เมื่อเหตุการณ์ปรากฏขึ้นจาก Shadow DOM ระบบจะปรับเป้าหมายของเหตุการณ์เพื่อรักษาการรวมที่ Shadow DOM มีให้ กล่าวคือ ระบบจะกําหนดเป้าหมายเหตุการณ์ใหม่ให้ดูเหมือนว่ามาจากคอมโพเนนต์ ไม่ใช่องค์ประกอบภายในภายใน Shadow DOM เหตุการณ์บางรายการไม่ได้นำไปใช้นอก Shadow DOM เลย
เหตุการณ์ที่ข้ามขอบเขตเงา ได้แก่
- กิจกรรมโฟกัส:
blur,focus,focusin,focusout - เหตุการณ์เมาส์:
click,dblclick,mousedown,mouseenter,mousemoveฯลฯ - เหตุการณ์การหมุน:
wheel - เหตุการณ์อินพุต:
beforeinput,input - เหตุการณ์แป้นพิมพ์:
keydown,keyup - กิจกรรมการเขียน:
compositionstart,compositionupdate,compositionend - DragEvent:
dragstart,drag,dragend,dropฯลฯ
เคล็ดลับ
หากทรีเงาเปิดอยู่ การเรียกใช้ event.composedPath() จะแสดงผลอาร์เรย์ของโหนดที่เหตุการณ์เดินทางผ่าน
การใช้เหตุการณ์ที่กําหนดเอง
เหตุการณ์ DOM ที่กําหนดเองซึ่งเริ่มทํางานบนโหนดภายในในต้นไม้เงาจะไม่แสดงนอกขอบเขตเงา เว้นแต่ว่าเหตุการณ์จะสร้างขึ้นโดยใช้ Flag composed: true ดังนี้
// Inside <fancy-tab> custom element class definition:
selectTab() {
const tabs = this.shadowRoot.querySelector('#tabs');
tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}
หากเป็น composed: false (ค่าเริ่มต้น) ผู้บริโภคจะไม่สามารถฟังเหตุการณ์นอกรูทเงาได้
<fancy-tabs></fancy-tabs>
<script>
const tabs = document.querySelector('fancy-tabs');
tabs.addEventListener('tab-select', e => {
// won't fire if `tab-select` wasn't created with `composed: true`.
});
</script>
การจัดการโฟกัส
ดังที่ทราบจากรูปแบบเหตุการณ์ของ Shadow DOM เหตุการณ์ที่เริ่มทํางานภายใน Shadow DOM จะได้รับการปรับให้ดูเหมือนว่ามาจากองค์ประกอบโฮสติ้ง
ตัวอย่างเช่น สมมติว่าคุณคลิก <input> ภายในรูทเงา
<x-focus>
#shadow-root
<input type="text" placeholder="Input inside shadow dom">
เหตุการณ์ focus จะดูเหมือนว่ามาจาก <x-focus> ไม่ใช่ <input>
ในทํานองเดียวกัน document.activeElement จะกลายเป็น <x-focus> หากสร้างรูทเงาด้วย mode:'open' (ดูโหมดปิด) คุณจะเข้าถึงโหนดภายในที่ได้รับโฟกัสได้ด้วย โดยทำดังนี้
document.activeElement.shadowRoot.activeElement // only works with open mode.
หากมี Shadow DOM หลายระดับ (เช่น เอลิเมนต์ที่กําหนดเองภายในเอลิเมนต์ที่กําหนดเองอีกรายการหนึ่ง) คุณต้องเจาะลึกรูทเงาแบบย้อนกลับเพื่อค้นหา activeElement
function deepActiveElement() {
let a = document.activeElement;
while (a && a.shadowRoot && a.shadowRoot.activeElement) {
a = a.shadowRoot.activeElement;
}
return a;
}
อีกตัวเลือกสําหรับโฟกัสคือตัวเลือก delegatesFocus: true ซึ่งจะขยายลักษณะการโฟกัสขององค์ประกอบภายในทรีเงา
- หากคุณคลิกโหนดภายใน Shadow DOM และโหนดนั้นไม่ใช่พื้นที่ที่โฟกัสได้ ระบบจะโฟกัสที่พื้นที่ที่โฟกัสได้รายการแรก
- เมื่อโหนดภายใน Shadow DOM ได้รับโฟกัส
:focusจะมีผลกับโฮสต์นอกเหนือจากองค์ประกอบที่มีโฟกัส
ตัวอย่าง - วิธีที่ delegatesFocus: true เปลี่ยนลักษณะการโฟกัส
<style>
:focus {
outline: 2px solid red;
}
</style>
<x-focus></x-focus>
<script>
customElements.define('x-focus', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
const root = this.attachShadow({mode: 'open', delegatesFocus: true});
root.innerHTML = `
<style>
:host {
display: flex;
border: 1px dotted black;
padding: 16px;
}
:focus {
outline: 2px solid blue;
}
</style>
<div>Clickable Shadow DOM text</div>
<input type="text" placeholder="Input inside shadow dom">`;
// Know the focused element inside shadow DOM:
this.addEventListener('focus', function(e) {
console.log('Active element (inside shadow dom):',
this.shadowRoot.activeElement);
});
}
});
</script>
ผลลัพธ์