Shadow DOM แบบประกาศ

Declarative Shadow DOM เป็นฟีเจอร์แพลตฟอร์มเว็บมาตรฐานที่ Chrome รองรับตั้งแต่เวอร์ชัน 90 เป็นต้นไป โปรดทราบว่ามีการเปลี่ยนแปลงข้อกำหนดของฟีเจอร์นี้ในปี 2023 (รวมถึงการเปลี่ยนชื่อ shadowroot เป็น shadowrootmode) และทุกส่วนในเวอร์ชันมาตรฐานล่าสุดของทุกส่วนได้รับการเปิดตัวใน Chrome เวอร์ชัน 124

การรองรับเบราว์เซอร์

  • Chrome: 111.
  • ขอบ: 111
  • Firefox: 123
  • Safari: 16.4

แหล่งที่มา

Shadow DOM เป็น 1 ใน 3 มาตรฐานของคอมโพเนนต์เว็บที่ปัดเศษด้วยเทมเพลต HTML และองค์ประกอบที่กำหนดเอง Shadow DOM มอบวิธีในการกำหนดขอบเขตรูปแบบ CSS ไปยังแผนผังย่อย DOM ที่เฉพาะเจาะจงและแยกต้นไม้ย่อยนั้นออกจากส่วนที่เหลือของเอกสาร องค์ประกอบ <slot> ช่วยให้เราควบคุมตำแหน่งย่อยขององค์ประกอบที่กำหนดเองที่ควรแทรกภายในแผนผังเงา คุณลักษณะเหล่านี้รวมกันทำให้ระบบสามารถสร้างองค์ประกอบที่สมบูรณ์ในตัวและใช้ซ้ำได้ ซึ่งจะผสานรวมกับแอปพลิเคชันที่มีอยู่ได้อย่างราบรื่นเช่นเดียวกับองค์ประกอบ HTML ในตัว

จนถึงตอนนี้ วิธีเดียวที่จะใช้ Shadow DOM ได้คือการสร้างรากเงาโดยใช้ JavaScript

const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';

API ที่จำเป็นเช่นนี้จะทำงานได้ดีสำหรับการแสดงผลฝั่งไคลเอ็นต์ โมดูล JavaScript เดียวกับที่กำหนดองค์ประกอบที่กำหนดเองของเราก็จะสร้าง Shadow Roots และตั้งค่าเนื้อหาด้วยเช่นกัน อย่างไรก็ตาม เว็บแอปพลิเคชันจำนวนมากต้องแสดงผลเนื้อหาฝั่งเซิร์ฟเวอร์หรือเป็น HTML แบบคงที่ ณ เวลาบิลด์ นี่อาจเป็นส่วนสำคัญในการมอบประสบการณ์ที่สมเหตุสมผลแก่ผู้เข้าชมที่อาจเรียกใช้ JavaScript ไม่ได้

เหตุผลรองรับสำหรับการแสดงผลฝั่งเซิร์ฟเวอร์ (SSR) จะแตกต่างกันไปในแต่ละโปรเจ็กต์ บางเว็บไซต์ต้องใช้ HTML ที่แสดงผลโดยเซิร์ฟเวอร์ซึ่งทำงานได้อย่างสมบูรณ์เพื่อให้เป็นไปตามหลักเกณฑ์การช่วยเหลือพิเศษ บางเว็บไซต์เลือกที่จะให้ประสบการณ์การใช้งานพื้นฐานที่ไม่มี JavaScript เพื่อให้มั่นใจได้ว่าการเชื่อมต่อหรืออุปกรณ์ที่ช้าจะมีประสิทธิภาพดี

ที่ผ่านมา การใช้ Shadow DOM ร่วมกับการแสดงผลฝั่งเซิร์ฟเวอร์นั้นเป็นเรื่องยาก เนื่องจากไม่มีวิธีในตัวที่จะแสดง Shadow Root ใน HTML ที่เซิร์ฟเวอร์สร้างขึ้น นอกจากนี้ ยังมีผลกระทบด้านประสิทธิภาพเมื่อแนบ Shadow Roots ไปยังองค์ประกอบ DOM ที่แสดงผลแล้วโดยไม่มีองค์ประกอบดังกล่าว ซึ่งสามารถทำให้เกิดการเปลี่ยนเลย์เอาต์หลังจากหน้าเว็บโหลดแล้ว หรือแสดงแฟลชของเนื้อหาที่ไม่มีการจัดรูปแบบ ("FOUC") ชั่วคราวในขณะที่โหลดสไตล์ชีตของ Shadow Root

The Declarative Shadow DOM (DSD) จะนำข้อจำกัดนี้ออกและนำ Shadow DOM ไปยังเซิร์ฟเวอร์

วิธีสร้าง Declarative Shadow Root

รากเงาประกาศ ( Declarative Shadow Root) เป็นองค์ประกอบ <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 แบบสล็อต

ซึ่งทำให้เราได้รับประโยชน์จากการห่อหุ้ม DOM และการฉายภาพสล็อตใน HTML แบบคงที่ ไม่จำเป็นต้องใช้ JavaScript เพื่อสร้างทั้งโครงสร้าง รวมถึง Shadow Root

ส่วนประกอบของน้ำ

คุณสามารถใช้ Shadow DOM เชิงประกาศแบบเดี่ยวๆ เพื่อสรุปรูปแบบหรือปรับแต่งตำแหน่งย่อย แต่จะมีประสิทธิภาพมากที่สุดเมื่อใช้กับองค์ประกอบที่กำหนดเอง คอมโพเนนต์ที่สร้างขึ้นโดยใช้องค์ประกอบที่กำหนดเองจะได้รับการอัปเกรดจาก HTML แบบคงที่โดยอัตโนมัติ ด้วยการเปิดตัว Declarative Shadow DOM ตอนนี้ทำให้องค์ประกอบที่กำหนดเองมีรูทเงาก่อนที่จะอัปเกรดได้

องค์ประกอบที่กำหนดเองซึ่งกำลังอัปเกรดจาก HTML ที่มี Declarative Shadow Root จะมีรากเงาดังกล่าวแนบอยู่แล้ว ซึ่งหมายความว่าองค์ประกอบจะมีพร็อพเพอร์ตี้ shadowRoot อยู่แล้วเมื่อมีการสร้างอินสแตนซ์ โดยที่คุณไม่ต้องสร้างโค้ดอย่างชัดเจน ควรตรวจสอบ this.shadowRoot เพื่อหารากเงาที่มีอยู่ในเครื่องมือสร้างองค์ประกอบ หากมีค่าอยู่แล้ว HTML สำหรับคอมโพเนนต์นี้จะมี Declarative 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();

      // Detect whether we have SSR content already:
      if (this.shadowRoot) {
        // A Declarative Shadow Root exists!
        // wire up event listeners, references, etc.:
        const button = this.shadowRoot.firstElementChild;
        button.addEventListener('click', toggle);
      } else {
        // A Declarative Shadow Root doesn't exist.
        // Create a new shadow root and populate it:
        const shadow = this.attachShadow({mode: 'open'});
        shadow.innerHTML = `<button><slot></slot></button>`;
        shadow.firstChild.addEventListener('click', toggle);
      }
    }
  }

  customElements.define('menu-toggle', MenuToggle);
</script>

องค์ประกอบที่กำหนดเองมีการใช้งานมาระยะหนึ่งแล้ว และจนถึงตอนนี้ก็ยังไม่มีเหตุผลที่ควรตรวจสอบรูทเงาที่มีอยู่ก่อนสร้างด้วย attachShadow() Declarative Shadow DOM รวมการเปลี่ยนแปลงเล็กน้อยที่ช่วยให้คอมโพเนนต์ที่มีอยู่ทำงานได้แม้ว่าจะมีเหตุการณ์เช่นนี้ก็ตาม กล่าวคือการเรียกเมธอด attachShadow() ในองค์ประกอบที่มี Shadow Root ของ declarative ที่มีอยู่จะไม่แสดงข้อผิดพลาด แต่รูท Declarative Shadow Root จะถูกล้างและแสดงผล ซึ่งจะทำให้คอมโพเนนต์เก่าที่ไม่ได้สร้างสำหรับ Declarative Shadow DOM ทำงานต่อไปได้ เนื่องจากระบบจะเก็บรักษารูทแบบประกาศไว้จนกว่าจะมีการสร้างการแทนที่ที่จำเป็น

สำหรับองค์ประกอบที่กำหนดเองที่สร้างขึ้นใหม่ พร็อพเพอร์ตี้ ElementInternals.shadowRoot ใหม่จะระบุวิธีที่ชัดเจนในการรับการอ้างอิงไปยัง Declarative Shadow Root ที่มีอยู่ขององค์ประกอบทั้งแบบเปิดและปิด ตัวเลือกนี้ใช้เพื่อตรวจสอบและใช้ Declarative Shadow Root ใดก็ได้ ขณะที่ยังคงกลับไปใช้ attachShadow() ในกรณีที่ไม่ได้ระบุ

class MenuToggle extends HTMLElement {
  constructor() {
    super();

    const internals = this.attachInternals();

    // 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.firstChild.addEventListener('click', toggle);
  }
}

customElements.define('menu-toggle', MenuToggle);

1 เงาต่อราก

รากเงาประกาศที่เชื่อมโยงกับองค์ประกอบระดับบนสุดเท่านั้น ซึ่งหมายความว่ารากเงาจะอยู่ในตำแหน่งร่วมกับองค์ประกอบที่เกี่ยวข้องเสมอ การตัดสินใจออกแบบนี้ช่วยให้มั่นใจได้ว่ารากเงาสามารถสตรีมได้เช่นเดียวกับส่วนที่เหลือของเอกสาร HTML และยังสะดวกสำหรับการเขียนและการสร้าง เนื่องจากการเพิ่มรากเงาลงในองค์ประกอบไม่จำเป็นต้องรักษารีจิสทรีของรากเงาที่มีอยู่

ข้อดีข้อเสียของการเชื่อมโยงรากเงากับองค์ประกอบระดับบนสุดก็คือ องค์ประกอบหลายรายการในการเริ่มต้นจากรูทเงาเงา <template> เดียวกันไม่ได้ อย่างไรก็ตาม กรณีเช่นนี้มักจะไม่สำคัญในกรณีส่วนใหญ่ที่ใช้ Declarative Shadow DOM เนื่องจากเนื้อหาของรากเงาแต่ละรากแทบจะไม่ตรงกันเสียทีเดียว แม้ว่า HTML ที่แสดงโดยเซิร์ฟเวอร์มักจะมีโครงสร้างขององค์ประกอบที่ซ้ำกัน แต่เนื้อหามักจะแตกต่างกัน เช่น ข้อความหรือแอตทริบิวต์ที่มีความแตกต่างไปเล็กน้อย เนื่องจากเนื้อหาของ Declarative Shadow Root เป็นแบบคงที่ทั้งหมด การอัปเกรดองค์ประกอบหลายรายการจาก Declarative Shadow Root เดียวจะทำงานก็ต่อเมื่อองค์ประกอบเหมือนกันเท่านั้น สุดท้าย ผลกระทบของรากเงาที่คล้ายกันซ้ำๆ ต่อขนาดการโอนเครือข่ายค่อนข้างน้อยเนื่องจากผลกระทบของการบีบอัด

ในอนาคตคุณอาจกลับไปย้อนดูรูทของเงาที่แชร์อีกครั้งได้ หาก DOM ได้รับการสนับสนุนสำหรับการสร้างเทมเพลตในตัว รูทเงาเงาของประกาศสิทธิ์ ( Declarative Shadow Roots) อาจถือเป็นเทมเพลตที่มีการสร้างอินสแตนซ์ขึ้นเพื่อสร้างรากเงาสำหรับองค์ประกอบที่ระบุ การออกแบบ Shadow DOM แบบประกาศในปัจจุบันช่วยให้ความเป็นไปได้นี้จะเกิดขึ้นในอนาคตโดยการจำกัดการเชื่อมโยงรากของเงาไว้ที่องค์ประกอบเดียว

สตรีมมิงนั้นเจ๋งมาก

การเชื่อมโยงรากเงาประกาศ ระบบจะตรวจพบ Declarative Shadow Roots ระหว่างการแยกวิเคราะห์ HTML และแนบทันทีเมื่อพบแท็ก <template> ที่กำลังเปิด HTML ที่แยกวิเคราะห์ภายใน <template> จะได้รับการแยกวิเคราะห์ไปยังรากของเงาโดยตรงเพื่อให้สามารถ "สตรีม": แสดงผลขณะที่ได้รับ

<div id="el">
  <script>
    el.shadowRoot; // null
  </script>

  <template shadowrootmode="open">
    <!-- shadow realm -->
  </template>

  <script>
    el.shadowRoot; // ShadowRoot
  </script>
</div>

โปรแกรมแยกวิเคราะห์เท่านั้น

Declarative Shadow DOM เป็นฟีเจอร์ของโปรแกรมแยกวิเคราะห์ HTML ซึ่งหมายความว่าจะใช้ Declarative Shadow Root จะได้รับการแยกวิเคราะห์และแนบสำหรับแท็ก <template> ที่มีแอตทริบิวต์ shadowrootmode ที่มีอยู่ระหว่างการแยกวิเคราะห์ HTML เท่านั้น กล่าวอีกนัยหนึ่งคือ รูทเงาเงาประกาศ ( Declarative Shadow Roots) สามารถสร้างขึ้นระหว่างการแยกวิเคราะห์ 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

นอกจากนี้ คุณยังสร้าง Declarative Shadow Roots โดยใช้ API การแยกวิเคราะห์ Fragment เช่น innerHTML หรือ insertAdjacentHTML() ไม่ได้ เพื่อหลีกเลี่ยงการพิจารณาด้านความปลอดภัยที่สำคัญบางประการ วิธีเดียวในการแยกวิเคราะห์ HTML โดยใช้ Declarative Shadow Roots คือการใช้ 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>

การแสดงผลเซิร์ฟเวอร์อย่างมีสไตล์

สไตล์ชีตแบบแทรกในบรรทัดและภายนอกได้รับการสนับสนุนอย่างสมบูรณ์ภายใน Declarative Shadow Roots โดยใช้แท็ก <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>

รูปแบบที่ระบุด้วยวิธีนี้ยังได้รับการเพิ่มประสิทธิภาพสูงด้วย กล่าวคือหากมีสไตล์ชีตเดียวกันอยู่ใน Declarative Shadow Roots หลายรายการ สไตล์ชีตดังกล่าวจะโหลดและแยกวิเคราะห์เพียงครั้งเดียว เบราว์เซอร์จะใช้ CSSStyleSheet แบบสำรองเดี่ยวที่แชร์กับรูทเงาทั้งหมด ซึ่งช่วยขจัดส่วนเกินของหน่วยความจำที่ซ้ำกัน

สไตล์ชีตที่สร้างได้ไม่ได้รับการสนับสนุนใน Declarative Shadow DOM นั่นเป็นเพราะว่าในปัจจุบันยังไม่มีวิธีทำให้สไตล์ชีตที่สร้างได้มีรูปแบบเป็น HTML และไม่สามารถอ้างอิงไปยังสไตล์ชีตเมื่อป้อนข้อมูล adoptedStyleSheets ได้

วิธีหลีกเลี่ยงแฟลชของเนื้อหาที่ไม่ได้จัดรูปแบบ

ปัญหาหนึ่งที่อาจเกิดขึ้นในเบราว์เซอร์ที่ยังไม่รองรับ Declarative Shadow DOM คือการหลีกเลี่ยง "รูปแบบ Flash ของเนื้อหาที่ไม่ได้จัดรูปแบบ" (FOUC) ซึ่งแสดงเนื้อหาดิบขององค์ประกอบที่กำหนดเองที่ยังไม่ได้อัปเกรด ก่อนการสร้าง Declarative Shadow DOM เทคนิคทั่วไปอย่างหนึ่งในการหลีกเลี่ยง FOUC คือการใช้กฎรูปแบบ display:none กับองค์ประกอบที่กำหนดเองที่ยังไม่ได้โหลด เนื่องจากไม่ได้แนบรากของเงาและป้อนข้อมูลไว้ ด้วยวิธีนี้ เนื้อหาจะไม่แสดงจนกว่าจะ "พร้อม"

<style>
  x-foo:not(:defined) > * {
    display: none;
  }
</style>

ด้วยการเปิดตัว Declarative Shadow DOM ทำให้สามารถแสดงผลหรือเขียนองค์ประกอบที่กำหนดเองเป็น HTML เพื่อให้เนื้อหาเงาอยู่ในตำแหน่งและพร้อมใช้งานก่อนที่จะโหลดการใช้งานคอมโพเนนต์ฝั่งไคลเอ็นต์:

<x-foo>
  <template shadowrootmode="open">
    <style>h2 { color: blue; }</style>
    <h2>shadow content</h2>
  </template>
</x-foo>

ในกรณีนี้ display:none "FOUC" ป้องกันไม่ให้เนื้อหารากของเงาประกาศ อย่างไรก็ตาม การนำกฎดังกล่าวออกจะทำให้เบราว์เซอร์ที่ไม่มีการรองรับ Shadow DOM แสดงเนื้อหาที่ไม่ถูกต้องหรือไม่ได้จัดรูปแบบจนกว่า polyfill เงา DOM ของ Declarative Shadow จะโหลดและแปลงเทมเพลตรากของเงาเป็นรากของเงาจริง

ซึ่งคุณแก้ไขได้ใน CSS โดยการปรับเปลี่ยนกฎรูปแบบ FOUC ในเบราว์เซอร์ที่รองรับ Declarative 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

การตรวจหาฟีเจอร์และการสนับสนุนเบราว์เซอร์

Declarative Shadow DOM พร้อมใช้งานตั้งแต่ Chrome 90 และ Edge 91 แต่ใช้แอตทริบิวต์ที่ไม่ใช่มาตรฐานเวอร์ชันเก่าชื่อ shadowroot แทนแอตทริบิวต์ shadowrootmode มาตรฐาน แอตทริบิวต์ shadowrootmode ที่ใหม่กว่าและลักษณะสตรีมมิงมีอยู่ใน Chrome 111 และ Edge 111

Declarative Shadow DOM ยังไม่มีการสนับสนุนอย่างแพร่หลายในทุกเบราว์เซอร์ ซึ่งเป็น API แพลตฟอร์มเว็บใหม่ ระบบจะตรวจหาการรองรับเบราว์เซอร์ได้โดยตรวจหาการมีอยู่ของพร็อพเพอร์ตี้ shadowRootMode ในต้นแบบของ HTMLTemplateElement:

function supportsDeclarativeShadowDOM() {
  return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}

ใยโพลีเอสเตอร์

การสร้าง Polyfill แบบง่ายสำหรับ Declarative Shadow DOM นั้นค่อนข้างตรงไปตรงมา เนื่องจาก Polyfill ไม่จำเป็นต้องจำลองความหมายของช่วงเวลาหรือลักษณะของโปรแกรมแยกวิเคราะห์เฉพาะที่เกี่ยวข้องกับการใช้งานเบราว์เซอร์ ในการ polyfill Declarative Shadow DOM เราสามารถสแกน DOM เพื่อค้นหาองค์ประกอบ <template shadowrootmode> ทั้งหมด แล้วแปลงเป็น Shadow Roots ที่แนบในองค์ประกอบระดับบนสุด ขั้นตอนนี้สามารถทำได้เมื่อเอกสารพร้อมแล้ว หรือมีการทริกเกอร์โดยเหตุการณ์ที่เฉพาะเจาะจงมากขึ้น เช่น วงจรองค์ประกอบที่กำหนดเอง

(function attachShadowRoots(root) {
  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);

อ่านเพิ่มเติม