Shadow DOM แบบประกาศ

วิธีใหม่ในการติดตั้งและใช้ Shadow DOM ใน HTML โดยตรง

Declarative Shadow DOM คือฟีเจอร์แพลตฟอร์มเว็บ ขณะนี้อยู่ในกระบวนการกําหนดมาตรฐาน และเปิดใช้อยู่โดยค่าเริ่มต้นใน Chrome เวอร์ชัน 111

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

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

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

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

การสร้าง Shadow Root แบบประกาศ

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 DevTools สำหรับการแสดงเนื้อหา Shadow DOM เช่น อักขระ ↳ จะแสดงเนื้อหา Light DOM ของช่อง

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

การเติมน้ำคอมโพเนนต์

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

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

      // 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>

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

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

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

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

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

สตรีมมิงดีเลย

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

<script>
  const html = `
    <div>
      <template shadowrootmode="open"></template>
    </div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; // No shadow root here
  const fragment = new DOMParser().parseFromString(html, 'text/html', {
    includeShadowRoots: true
  }); // Shadow root 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>

รูปแบบที่ระบุด้วยวิธีนี้ก็ได้รับการเพิ่มประสิทธิภาพอย่างสูงเช่นกัน หากมีสไตล์ชีตเดียวกันอยู่ใน 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>

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

โชคดีที่ปัญหานี้แก้ไขได้ใน 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> เมื่อกำหนดองค์ประกอบที่กำหนดเองแล้ว กฎจะไม่มีการจับคู่อีกต่อไป ระบบจะไม่สนใจกฎนี้ในเบราว์เซอร์ที่รองรับ 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 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);

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