Shadow DOM แบบประกาศเป็นฟีเจอร์มาตรฐานของแพล��ฟอร์มบนเว็บ ซึ่ง Chrome รองรับตั้งแต่เวอร์ชัน 90 โปรดทราบว่าข้อกำหนดของฟีเจอร์นี้เปลี่ยนแปลงไปในปี 2023 (รวมถึงการเปลี่ยนชื่อ shadowroot
เป็น shadowrootmode
) และเวอร์ชันมาตรฐานล่าสุดของทุกส่วนของฟีเจอร์นี้พร้อมให้ใช้งานใน Chrome เวอร์ชัน 124
Shadow DOM เป็นหนึ่งในมาตรฐาน Web Components 3 รายการ ซึ่งประกอบไปด้วย เทมเพลต HTML และ องค์ประกอบที่กําหนดเอง Shadow DOM มีวิธีกำหนดขอบเขตสไตล์ CSS ให้กับซับต้นไม้ DOM ที่เฉพาะเจาะจง และแยกซับต้นไม้นั้นออกจากส่วนที่เหลือของเอกสาร องค์ประกอบ <slot>
ช่วยให้เราควบคุมตำแหน่งที่ควรแทรกองค์ประกอบย่อยขององค์ประกอบที่กำหนดเองภายใน Shadow Tree ได้ ฟีเจอร์เหล่านี้ช่วยให้ระบบสร้างคอมโพเนนต์แบบสแตนด์อโลนที่ใช้ซ้ำได้ ซึ่งผสานรวมเข้ากับแอปพลิเคชันที่มีอยู่ได้อย่างราบรื่น เช่นเดียวกับองค์ประกอบ HTML ในตัว
ก่อนหน้านี้ วิธีเดียวในการใช้ Shadow DOM คือการสร้างรูทเงาโด��ใ��้ JavaScript ดังนี้
const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';
API แบบบังคับเช่นนี้ทํางานได้ดีสําหรับการแสดงผลฝั่งไคลเอ็นต์ โมดูล JavaScript เดียวกันกับที่กําหนดองค์ประกอบที่กําหนดเองจะสร้างรูทเงาและตั้งค่าเนื้อหาด้วย อย่างไรก็ตาม เว็บแอปพลิเคชันจำนวนมากต้องแสดงผลเนื้อหาฝั่งเซิร์ฟเวอร์หรือเป็น HTML แบบคงที่ ณ เวลาที่สร้าง ซึ่งอาจเป็นส่วนสําคัญในการมอบประสบการณ์การใช้งานที่เหมาะสมแก่ผู้เข้าชมที่อาจไม่สามารถเรียกใช้ JavaScript ได้
เหตุผลที่ควรใช้การแสดงผลฝั่งเซิร์ฟเวอร์ (SSR) จะแตกต่างกันไปในแต่ละโปรเจ็กต์ เว็บไซต์บางแห่งต้องให้บริการ HTML ที่แสดงผลจากเซิร์ฟเวอร์ที่ใช้งานได้อย่างเต็มรูปแบบเพื่อให้เป็นไปตามหลักเกณฑ์การช่วยเหลือพิเศษ ส่วนเว็บไซต์อื่นๆ เลือกที่จะมอบประสบการณ์การใช้งานพื้นฐานแบบไม่มี JavaScript เพื่อประสิทธิภาพที่ดีในการเชื่อมต่อหรืออุปกรณ์ที่ช้า
ที่ผ่านมาการใช้ Shadow DOM ร่วมกับการแสดงผลฝั่งเซิร์ฟเวอร์นั้นเป็นเรื่องยาก เนื่องจากไม่มีวิธีในตัวในการแสดง Shadow Root ใน HTML ที่เซิร์ฟเวอร์สร้างขึ้น นอกจากนี้ ยังมีผลต่อประสิทธิภาพเมื่อแนบ Shadow Root กับองค์ประกอบ DOM ที่แสดงผลแล้วโดยไม่มี Shadow Root ซึ่งอาจทําให้เลย์เอาต์เปลี่��นหลังจากหน้าเว็บโหลด หรือแสดงเนื้อหาที่ไม่มีการจัดรูปแบบกะพริบชั่วคราว ("FOUC") ขณะโหลดสไตล์ชีตของ Shadow Root
Declarative Shadow DOM (DSD) จะนําข้อจํากัดนี้ออกและนํา Shadow DOM ไปยังเซิร์ฟเวอร์
วิธีสร้างรากเงาแบบประกาศ
รูทเงาแบบประกาศคือองค์ประกอบ <template>
มีแอตทริบิวต์ shadowrootmode
ดังนี้
<host-element>
<template shadowrootmode="open">
<slot></slot>
</template>
<h2>Light content</h2>
</host-element>
โปรแกรมแยกวิเคราะห์ HTML จะตรวจพบองค์ประกอบเทมเพลตที่มีแอตทริบิวต์ shadowrootmode
และใช้เป็นรูทเงาขององค์ประกอบหลักทันที การโหลดมาร์กอัป HTML ล้วนๆ จากตัวอย่างด้านบนจะแสดงผลเป็นต้นไม้ DOM ดังต่อไปนี้
<host-element>
#shadow-root (open)
<slot>
↳
<h2>Light content</h2>
</slot>
</host-element>
ตัวอย่างโค้ดนี้เป็นไปตามรูปแบบของแผงองค์ประกอบในเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ของ Chrome สำหรับการแสดงเนื้อหา Shadow DOM เช่น อักขระ ↳
แสดงถึงเนื้อหา Light DOM ในช่อง
ซึ่งทำให้เราได้รับประโยชน์จากการห่อหุ้มและการฉายสล็อตของ Shadow DOM ใน HTML แบบคงที่ คุณไม่จำเป็นต้องใช้ JavaScript เพื่อสร้างทั้งต้นไม้ ซึ่งรวมถึง Shadow Root
องค์ประกอบที่กําหนดเองและการตรวจหา Shadow Root ที่มีอยู่
Shadow DOM แบบประกาศใช้ได้ด้วยตัวเองเพื่อรวมสไตล์หรือปรับแต่งตำแหน่งขององค์ประกอบย่อย แต่��ีประสิทธิภาพสูงสุดเมื่อใช้กับองค์ประกอบที่กำหนดเอง คอมโพเนนต์ที่สร้างโดยใช้องค์ประกอบที่กำหนดเองจะได้รับการอัปเกรดจาก HTML แบบคงที่โดยอัตโนมัติ การใช้ Shadow DOM แบบประกาศทำให้เอลิเมนต์ที่กำหนดเองมี Shadow Root ได้ก่อนที่จะอัปเกรด
องค์ประกอบที่กําหนดเองมีมานานแล้ว แต่จนถึงตอนนี้ก็ยังไม่มีเหตุผลที่จะตรวจสอบรูทเงาที่มีอยู่ก่อนที่จะสร้างรูทเงาโดยใช้ attachShadow()
Shadow DOM แบบประกาศมีการเปลี่ยนแปลงเล็กน้อยที่ช่วยให้คอมโพเนนต์ที่มีอยู่ทำงานได้ นั่นคือการเรียกใช้เมธอด attachShadow()
ในองค์ประกอบที่มีรูท Shadow แบบประกาศที่มีอยู่จะไม่แสดงข้อผิดพลาด แต่ระบบจะล้างค่าในรากเงาแบบประกาศและแสดงผลแทน วิธีนี้ช่วยให้คอมโพเนนต์เก่าที่ไม่ได้สร้างสําหรับ Shadow DOM แบบประกาศทํางานต่อไปได้ เนื่องจากระบบจะเก็บรากแบบประกาศไว้จนกว่าจะสร้างรายการที่จะมาแทนที่
สําหรับองค์ประกอบที่กําหนดเองที่���ร้าง��ึ้นใหม่ พร็อพเพอร์ตี้ ElementInternals.shadowRoot ใหม่จะให้วิธีรับการอ้างอิงไปยังรากเงาแบบประกาศที่มีอยู่ขององค์ประกอบ ทั้งแบบเปิดและแบบปิด ซึ่งสามารถใช้เพื่อตรวจสอบและใช้ Shadow Root แบบประกาศได้ ในขณะที่ยังคงใช้ attachShadow()
ในกรณีที่ไม่ได้ระบุ Shadow Root
ปริมาณน้ำที่ดื่ม
องค์ประกอบที่กําหนดเองซึ่งอัปเกรดจาก HTML ที่มีรากเงาแบบประกาศจะมีรากเงานั้นแนบอยู่อยู่แล้ว ซึ่งหมายความว่า ElementInternals
ขององค์ประกอบจะมีพร็อพเพอร์ตี้ shadowRoot
อยู่แล้วเมื่อสร้างอินสแตนซ์ โดยไม่ต้องให้โค้ดของคุณสร้างพร็อพเพอร์ตี้ดังกล่าวอย่างชัดเจน คุณควรตรวจสอบ ElementInternals.shadowRoot
เพื่อหารูทเงาที่มีอยู่ในตัวสร้างขององค���ประกอบ หากมีค่าอยู่แล้ว HTML ของคอมโพเนนต์นี้จะมี Shadow Root แบบประกาศ หากค่าเป็น Null แสดงว่าไม่มี Declarative Shadow Root ใน HTML หรือเบราว์เซอร์ไม่รองรับ Declarative Shadow DOM
<menu-toggle>
<template shadowrootmode="open">
<button>
<slot></slot>
</button>
</template>
Open Menu
</menu-toggle>
<script>
class MenuToggle extends HTMLElement {
constructor() {
super();
const supportsDeclarative = HTMLElement.prototype.hasOwnProperty("attachInternals");
const internals = supportsDeclarative ? this.attachInternals() : undefined;
const toggle = () => {
console.log("menu toggled!");
};
// check for a Declarative Shadow Root.
let shadow = internals?.shadowRoot;
if (!shadow) {
// there wasn't one. create a new Shadow Root:
shadow = this.attachShadow({
mode: "open",
});
shadow.innerHTML = `<button><slot></slot></button>`;
}
// in either case, wire up our event listener:
shadow.firstElementChild.addEventListener("click", toggle);
}
}
customElements.define("menu-toggle", MenuToggle);
</script>
เงา 1 รายการต่อรูท
รูทเงาแบบประกาศจะเชื่อมโยงกับองค์ประกอบหลักเท่านั้น ซึ่งหมายความว่ารูทเงาจะอยู่ร่วมกับองค์ประกอบที่เชื่อมโยงอยู่เสมอ การตัดสินใจด้านการออกแบบนี้ช่วยให้สามารถสตรีมรูทเงาได้เช่นเดียวกับส่วนอื่นๆ ของเอกสาร HTML นอกจากนี้ ยังสะดวกสําหรับการเขียนและการสร้าง เนื่องจากการเพิ่ม Shadow Root ลงในองค์ประกอบไม่จําเป็นต้องดูแลรักษารีจิสทรีของ Shadow Root ที่มีอยู่
ข้อเสียของการเชื่อมโยงรูทเงากับองค์ประกอบหลักคือคุณจะไม่สามารถเริ่มต้นองค์ประกอบหลายรายการจากรูทเงาแบบประกาศ <template>
เดียวกันได้ อย่างไรก็ตาม กรณีนี้ไม่����า��ะ��������������นในส่วนใหญ่ที่ใช้ Declarative Shadow DOM เนื่องจากเนื้อหาของ Shadow Root แต่ละรายการแทบจะไม่เหมือนกัน แม้ว่า HTML ที่แสดงผลจากเซิร์ฟเวอร์มักจะมีโครงสร้างองค์ประกอบที่ซ้ำกัน แต่เนื้อหามักจะแตกต่างกัน เช่น ข้อความหรือแอตทริบิวต์ที่ต่างกันเล็กน้อย เนื่องจากเนื้อหาของ Declarative Shadow Root ที่แปลงเป็นอนุกรมเป็นแบบคงที่ทั้งหมด การอัปเกรดองค์ประกอบหลายรายการจาก Declarative Shadow Root รายการเดียวจะใช้งานได้ก็ต่อเมื่อองค์ประกอบเหล่านั้นเหมือนกันเท่านั้น สุดท้าย ผลกระทบของรูทเงาที่คล้ายกันซึ่งซ้ำกันต่อขนาดการโอนของเครือข่ายมีค่อนข้างน้อยเนื่องจากผลของการบีบอัด
ในอนาคต คุณอาจกลับมาดูรูทเงาที่แชร์ได้ หาก DOM รองรับเทมเพลตในตัว ระบบอาจถือว่ารูทเงาแบบประกาศเป็นเทมเพลตที่สร้างอินสแตนซ์เพื่อสร้างรูทเงาสําหรับองค์ประกอบหนึ่งๆ การออกแบบ Shadow DOM แบบประกาศในปัจจุบันเปิดโอกาสให้การดำเนินการนี้เกิดขึ้นได้ในอนาคตด้วยการจำกัดการเชื่อมโยงรูทเงาไว้กับองค์ประกอบเดียว
สตรีมมิงเจ๋ง
การเชื่อมโยงรากเงาแบบประกาศกับองค์ประกอบหลักโดยตรงจะทําให้กระบวนการอัปเกรดและแนบรากเงากับองค์ประกอบนั้นง่ายขึ้น ระบบจะตรวจหารากเงาแบบประกาศระหว่างการแยกวิเคราะห์ HTML และแนบทันทีที่พบแท็ก <template>
เปิด HTML ที่แยกวิเคราะห์ภายใน <template>
จะแยกวิเคราะห์ไปยังรูทเงาโดยตรง เพื่อให้ "สตรีม" ได้ ซึ่งก็คือแสดงผลเมื่อได้รับ
<div id="el">
<script>
el.shadowRoot; // null
</script>
<template shadowrootmode="open">
<!-- shadow realm -->
</template>
<script>
el.shadowRoot; // ShadowRoot
</script>
</div>
โปรแกรมแยกวิเคราะห์เท่านั้น
Shadow DOM แบบประกาศเป็นฟีเจอร์ของตัวแยกวิเคราะห์ HTML ซึ่งหมายความว่าระบบจะแยกวิเคราะห์และแนบรากเงาแบบประกาศสำหรับแท็ก <template>
ที่มีแอตทริบิวต์ shadowrootmode
เท่านั้นที่ปรากฏระหว่างการแยกวิเคราะห์ HTML กล่าวคือ รากเงาแบบประกาศสามารถสร้างได้ในระหว่างการแยกวิเคราะห์ HTML ครั้งแรก ดังนี้
<some-element>
<template shadowrootmode="open">
shadow root content for some-element
</template>
</some-element>
การตั้งค่าแอตทริบิวต์ shadowrootmode
ขององค์ประกอบ <template>
จะไม่ทําอะไรเลย และเทมเพลตจะยังคงเป็นองค์ประกอบเทมเพลตธรรมดา
const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null
นอกจากนี้ คุณยังสร้างรูทเงาแบบประกาศโดยใช้ API การแยกวิเคราะห์ข้อมูลโค้ดแบบเป็นกลุ่มไม่ได้ เช่น innerHTML
หรือ insertAdjacentHTML()
เพื่อหลีกเลี่ยงการพิจารณาด้านความปลอดภัยที่สำคัญบางอย่าง วิธีเดียวในการแยกวิเคราะห์ HTML ที่มีการใช้ราก Shadow แบบประกาศคือการใช้ setHTMLUnsafe()
หรือ parseHTMLUnsafe()
<script>
const html = `
<div>
<template shadowrootmode="open"></template>
</div>
`;
const div = document.createElement('div');
div.innerHTML = html; // No shadow root here
div.setHTMLUnsafe(html); // Shadow roots included
const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>
การแสดงผลฝั่งเซิร์ฟเวอร์ที่มีสไตล์
ระบบรองรับสไตล์ชีตในบรรทัดและภายนอกอย่างเต็มรูปแบบภายในรูทเงาแบบประกาศโดยใช้แท็ก <style>
และ <link>
มาตรฐาน ดังนี้
<nineties-button>
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<link rel="stylesheet" href="/comicsans.css" />
<button>
<slot></slot>
</button>
</template>
I'm Blue
</nineties-button>
สไตล์ที่ระบุด้วยวิธีนี้จะได้รับการเพิ่มประสิทธิภาพอย่างมากเช่นกัน หากมีชีตสไตล์เดียวกันในรากเงาแบบประกาศหลายรายการ ระบบจะโหลดและแยกวิเคราะห์ชีตสไตล์นั้นเพียงครั้งเดียว เบราว์เซอร์ใช้ CSSStyleSheet
สำรองรายการเดียวที่รูทเงาทั้งหมดใช้ร่วมกัน ซึ่งจะช่วยลดค่าใช้จ่ายหน่วยความจำที่ซ้ำกัน
สไตล์ชีตที่สร้างได้ไม่รองรับใน Shadow DOM แบบประกาศ เนื่องจากปัจจุบันยังไม่มีวิธีจัดรูปแบบข้อมูลสไตล์ชีตที่สร้างได้ใน HTML และไม่มีวิธีอ้างอิงสไตล์ชีตดังกล่าวเมื่อป้อนข้อมูล adoptedStyleSheets
วิธีหลีกเลี่ยงการแสดงเนื้อหาที่ไม่มีการจัดรูปแบบอย่างรวดเร็ว
ปัญหาที่อาจเกิดขึ้นอย่างหนึ่งในเบราว์เซอร์ที่ยังไม่รองรับ Declarative Shadow DOM คือ "การกะพริบของเนื้อหาที่ไม่มีการจัดรูปแบบ" (FOUC) ซึ่งระบบจะแสดงเนื้อหาดิบสำหรับองค์ประกอบที่กำหนดเองที่ยังไม่ได้อัปเกรด ก่อนที่จะมี Shadow DOM แบบประกาศ เทคนิคทั่วไปอย่างหนึ่งในการหลีกเลี่ยง FOUC คือการใช้กฎสไตล์ display:none
กับองค์ประกอบที่กำหนดเองซึ่งยังไม่ได้โหลด เนื่องจากองค์ประกอบเหล่านี้ยังไม่ได้แนบและสร้างรูท������� ��ิธีนี้จะทำให้เนื้อหาไม่แสดงจนกว่าจะ "พร้อม"
<style>
x-foo:not(:defined) > * {
display: none;
}
</style>
การใช้ Shadow DOM แบบประกาศทำให้องค์ประกอบที่กำหนดเองแสดงผลหรือเขียนได้ใน HTML เพื่อให้เนื้อหา Shadow อยู่ในตำแหน่งและพร้อมใช้งานก่อนที่จะโหลดการใช้งานคอมโพเนนต์ฝั่งไคลเอ็นต์
<x-foo>
<template shadowrootmode="open">
<style>h2 { color: blue; }</style>
<h2>shadow content</h2>
</template>
</x-foo>
ในกรณีนี้ กฎ display:none
"FOUC" จะป้องกันไม่ให้เนื้อหาของรากเงาแบบประกาศแสดง อย่างไรก็ตาม การนํากฎดังกล่าวออกจะทำให้เบราว์เซอร์ที่ไม่รองรับ Declarative Shadow DOM แสดงเนื้อหาที่ไม่ถูกต้องหรือไม่มีสไตล์จนกว่า Declarative Shadow DOM polyfill จะโหลดและแปลงเทมเพลต Shadow Root ให้เป็น Shadow Root จริง
แต่ปัญหานี้แก้ไขได้ใน CSS โดยการแก้ไขกฎรูปแบบ FOUC ในเบราว์เซอร์ที่รองรับ Shadow DOM แบบประกาศ ระบบจะแปลงองค์ประกอบ <template shadowrootmode>
เป็นรูทเงาทันที โดยไม่เหลือองค์ประกอบ <template>
ในต้นไม้ DOM เบราว์เซอร์ที่ไม่รองรับ Declarative Shadow DOM จะเก็บองค์ประกอบ <template>
ไว้ ซึ่งเราสามารถใช้เพื่อป้องกัน FOUC ได้ ดังนี้
<style>
x-foo:not(:defined) > template[shadowrootmode] ~ * {
display: none;
}
</style>
กฎ "FOUC" ที่แก้ไขแล้วจะไม่ซ่อนองค์ประกอบที่กำหนดเองที่ยังไม่ได้กำหนด แต่ซ่อนองค์ประกอบย่อยเมื่ออยู่หลังองค์ประกอบ <template shadowrootmode>
เมื่อกําหนดองค์ประกอบที่กําหนดเองแล้ว กฎจะไม่ตรงกันอีกต่อไป ระบบจะไม่สนใจกฎนี้ในเบราว์เซอร์ที่รองรับ Declarative Shadow DOM เนื่องจากระบบจะนำรายการย่อย <template shadowrootmode>
ออกระหว่างการแยกวิเคราะห์ HTML
การตรวจหาฟีเจอร์และการรองรับเบราว์เซอร์
Shadow DOM แบบประกาศใช้ได้ตั้งแต่ Chrome 90 และ Edge 91 แต่ใช้แอตทริบิวต์ที่ไม่เป็นไปตามมาตรฐานแบบเก่าที่������ยกว���า shadowroot
แทนแอตทริบิวต์ shadowrootmode
ที่เป็นมาตรฐาน แอตทริบิวต์ shadowrootmode
และลักษณะการสตรีมเวอร์ชันใหม่พร้อมใช้งานใน Chrome 111 และ Edge 111
เนื่องจากเป็น API แพลตฟอร์มเว็บใหม่ Declarative Shadow DOM จึงยังไม่ได้รับการรองรับอย่างแพร่หลายในเบราว์เซอร์ทุกรุ่น คุณสามารถตรวจหาการรองรับเบราว์เซอร์ได้โดยตรวจสอบว่ามีพร็อพเพอร์ตี้ shadowRootMode
ในโปรโตไทป์ของ HTMLTemplateElement
หรือไม่
function supportsDeclarativeShadowDOM() {
return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}
โพลีฟิลล์
การสร้าง polyfill ที่เรียบง่ายสำหรับ Declarative Shadow DOM นั้นค่อนข้างตรงไปตรงมา เนื่องจาก polyfill ไม่จำเป็นต้องจำลองความหมายเชิงเวลาหรือลักษณะเฉพาะของโปรแกรมแยกวิเคราะห์เท่านั้นที่การติดตั้งใช้งานเบราว์เซอร์ต้องคำนึงถึง หากต้องการใช้ polyfill สำหรับ Declarative Shadow DOM เราจะสแกน DOM เพื่อค้นหาองค์ประกอบ <template shadowrootmode>
ทั้งหมด จากนั้นแปลงเป็น Shadow Root ที่แนบมากับองค์ประกอบหลัก กระบวนการนี้สามารถดำเนินการได้เมื่อเอกสารพร้อมแล้ว หรือทริกเกอร์โดยเหตุการณ์ที่เฉพาะเจาะจงมากขึ้น เช่น วงจรชีวิตขององค์ประกอบที่กำหนดเอง
(function attachShadowRoots(root) {
if (supportsDeclarativeShadowDOM()) {
// Declarative Shadow DOM is supported, no need to polyfill.
return;
}
root.querySelectorAll("template[shadowrootmode]").forEach(template => {
const mode = template.getAttribute("shadowrootmode");
const shadowRoot = template.parentNode.attachShadow({ mode });
shadowRoot.appendChild(template.content);
template.remove();
attachShadowRoots(shadowRoot);
});
})(document);