Shadow DOM, เทมเพลต และสล็อต

ฟีเจอร์เว็บ: Shadow DOM <slot>

Shadow Root คือซับทรี DOM ที่แคปซูลไว้ซึ่งแนบอยู่กับองค์ประกอบโฮสต์ โดยสไตล์ภายในจะไม่รั่วไหลออกไป และสไตล์ของหน้าเว็บจะไม่รั่วไหลเข้ามา (ยกเว้นพร็อพเพอร์ตี้ที่รับค่ามา) เทมเพลตจะสร้างแผนผังอย่างมีประสิทธิภาพ ส่วนสล็อตจะฉายเนื้อหา Light DOM เข้าไปในแผนผัง

การแนบ Shadow Root

this.attachShadow({ mode: 'open', delegatesFocus: true });

delegatesFocus: true ทำให้การโฟกัสโฮสต์ย้ายโฟกัสไปยังองค์ประกอบแรกที่โฟกัสได้ภายใน Shadow Tree, กำหนดเส้นทางสไตล์ :focus ไปยังโฮสต์ และส่งต่อการคลิก <label> ไปยังการควบคุมภายในในองค์ประกอบที่กำหนดเองที่เชื่อมโยงกับแบบฟอร์ม (static formAssociated = true) ตั้งค่าเมื่อใดก็ตามที่คอมโพเนนต์รวมการควบคุมที่โฟกัสได้ ใช้ mode: 'open' ในเกือบทุกกรณีเพื่อให้เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ สคริปต์หน้าเว็บ และการไฮเดรตเซิร์ฟเวอร์เข้าถึง element.shadowRoot ได้ (ดูการวางแนวคอมโพเนนต์เว็บ)

เทมเพลต: สร้างโครงสร้างอย่างมีประสิทธิภาพ

กำหนด <template> ครั้งเดียว (ขอบเขตโมดูลหรือช่องแบบคงที่) และโคลนต่ออินสแตนซ์ การโคลนส่วนย่อยที่แยกวิเคราะห์แล้วจะเร็วกว่าการแยกวิเคราะห์สตริง innerHTML ซ้ำในการสร้างทุกครั้ง

const template = document.createElement('template');
template.innerHTML = `
  <div class="card">
    <slot name="header"></slot>
    <div class="body"><slot></slot></div>
    <slot name="footer"></slot>
  </div>
`;

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // MANDATORY: deep-clone the template content so each instance gets its own nodes.
    this.shadowRoot.append(template.content.cloneNode(true));
  }
}
customElements.define('my-card', MyCard);

ใช้เทมเพลตที่เรียบง่าย "div soup" ที่ซ้อนกันหลายชั้นและ Shadow Root ที่ซ้อนกันหลายระดับต่างก็เพิ่มต้นทุนการแก้ไขสไตล์และเลย์เอาต์ที่วัดได้ ซึ่งส่งผลต่อประสิทธิภาพอย่างมากในวงกว้าง เลือกโครงสร้างแบบแบนและองค์ประกอบเชิงความหมาย

สล็อต: ส่วนที่สร้างความประหลาดใจให้ผู้คน

สล็อตเริ่มต้นและสล็อตที่มีชื่อจะทำงานตามที่ระบุไว้ โดยลักษณะการทำงานที่อาจทำให้เกิดปัญหา ได้แก่

  • ช่องว่างมีความสำคัญ ระบบจะกำหนดโหนดข้อความระหว่างองค์ประกอบ (รวมถึงช่องว่างล้วนๆ) ให้กับช่องเริ่มต้น ดังนั้นช่องเริ่มต้นจึงแทบจะไม่ "ว่างเปล่า" จริงๆ
  • ระบบจะคงลำดับ DOM ไว้ ไม่ใช่ลำดับช่องโฆษณา โหนดที่ฉายจะคงลำดับ Light DOM เดิมไว้เพื่อวัตถุประสงค์ด้าน :first-child/:last-child ไม่ว่าจะเติมช่องใดก็ตาม กฎ ::slotted(*:first-child) กำหนดเป้าหมายเป็นรายการย่อย DOM รายการแรก ไม่ใช่รายการย่อยรายการแรกของช่องนั้น ดังนั้นจึงมักไม่ตรงกับสิ่งที่คุณคาดหวัง
  • slotchange คือสัญญาณเวลาอัปเกรด โดยจะเริ่มทำงานเมื่อโหนดที่กำหนดมีการเปลี่ยนแปลง (รวมถึงการกำหนดครั้งแรก) และเป็นวิธีที่เชื่อถือได้ในการทราบว่าโหนดบุตรหลานของ Light DOM มาถึงแล้วหลังจากการอัปเกรด ซึ่งอาจไม่มีอยู่ในระหว่าง connectedCallback
this.shadowRoot.querySelector('slot[name="header"]')
  .addEventListener('slotchange', (e) => {
    const nodes = e.target.assignedElements();
    this.toggleAttribute('has-header', nodes.length > 0);
  });

กำลังโหลดสไตล์ชีตลงใน Shadow Root

วิธีการ ข้อดี ข้อเสีย
<style> ในเทมเพลต ใช้งานง่าย มีการห่อหุ้มอย่างสมบูรณ์ และทำงานได้โดยไม่ต้องใช้ JS ทำซ้ำต่ออินสแตนซ์ ซึ่งทำให้ DOM มีขนาดใหญ่ขึ้นเมื่อปรับขนาด
<link rel="stylesheet"> ไฟล์ภายนอกที่แคชได้ คำขอเพิ่มเติม เสี่ยงต่อการเกิด Flash of Unstyled Content (FOUC)
adoptedStyleSheets CSSStyleSheet ที่แชร์ 1 รายการในทุกอินสแตนซ์ ปรับขนาดได้เร็วที่สุด ต้องใช้ JS; สไตล์ชีตที่สร้างขึ้นไม่รองรับ @import

สำหรับคอมโพเนนต์หนึ่งที่มีหลายอินสแตนซ์ ให้เลือกสไตล์ชีตที่สร้างขึ้นที่แชร์

const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // The same sheet object is reused by every instance; no per-instance parse.
    this.shadowRoot.adoptedStyleSheets = [sheet];
  }
}