Shadow DOM แบบประกาศ

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

Browser Support

Source

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 ไปยังเซิร์ฟเวอร์

วิธีสร้างรากเงาแบบประกาศ

Image for: วิธีสร้างรากเงาแบบประกาศ

รูทเงาแบบประกาศคือองค์ประกอบ <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 ที่มีอยู่

Image for: องค์ประกอบที่กําหนดเองและการตรวจหา Shadow Root ที่มีอยู่

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

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

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

Browser Support

Source

ปริมาณน้ำที่ดื่ม

Image for: ปริมาณน้ำที่ดื่ม

องค์ประกอบที่กําหนดเองซึ่งอัปเกรดจาก 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 รายการต่อรูท

Image for: เงา 1 รายการต่อรูท

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

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

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

สตรีมมิงเจ๋ง

Image for: สตรีมมิงเจ๋ง

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

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

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

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

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

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

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>

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

Image for: การแสดงผลฝั่งเซิร์ฟเวอร์ที่มีสไตล์

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

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

Image for: วิธีหลีกเลี่ยงการแสดงเนื้อหาที่ไม่มีการจัดรูปแบบอย่างรวดเร็ว

ปัญหาที่อาจเกิดขึ้นอย่างหนึ่งในเบราว์เซอร์ที่ยังไม่รองรับ 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

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

Image for: การตรวจหาฟีเจอร์และการรองรับเบราว์เซอร์

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');
}

โพลีฟิลล์

Image for: โพลีฟิลล์

การสร้าง 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);

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

Image for: อ่านเพิ่มเติม