องค์ประกอบที่กำหนดเองคือคลาสที่ลงทะเบียนกับชื่อแท็กผ่าน customElements.define() การลงทะเบียนและวงจรพื้นฐานเป็นที่เข้าใจกันดีอยู่แล้ว กฎด้านล่างจึงกำหนดเป้าหมายไปยังส่วนที่มักจะมีการนำไปใช้อย่างไม่ถูกต้อง
วงจร: ส่วนที่ใช้งานไม่ถูกต้อง
Callback มีการบันทึกไว้อย่างดี แต่ความเข้าใจผิดต่อไปนี้อาจทำให้เกิดข้อบกพร่องได้
connectedCallbackสามารถทำงานได้มากกว่า 1 ครั้ง โดยจะเริ่มทำงานทุกครั้งที่มีการแทรก ดังนั้นการย้ายองค์ประกอบจะเรียกใช้โค้ดอีกครั้ง ทำให้การตั้งค่าเป็นแบบ Idempotent โดยอย่าคิดว่าการเรียกใช้ครั้งแรกจะเกิดขึ้นเพียงครั้งเดียวconnectedMoveCallbackจะข้ามการยกเลิกการเชื่อมต่อ/เชื่อมต่อใหม่เมื่อมีการย้าย เมื่อย้ายองค์ประกอบด้วยNode.moveBefore()ระบบจะเรียกใช้ Callback นี้แทนdisconnectedCallback→connectedCallbackดังนั้นสถานะ โฟกัส และการเล่น iframe/สื่อจะยังคงอยู่หลังจากการย้าย กำหนดเมื่อการตั้งค่าหรือการแยกส่วนมีค่าใช้จ่ายสูงหรือสังเกตเห็นได้ว่ามีการเรียกใช้ซ้ำ- รื้อถอนใน
disconnectedCallbackนำ Listener, ตัวจับเวลา และ Observer ส่วนกลางออก ไม่เช่นนั้นจะเกิดการรั่วไหล แต่องค์ประกอบอาจถูกแทรกซ้ำ ดังนั้นอย่าถือว่าการแยกส่วนเป็นแบบถาวร - เรียกใช้
super()ก่อนในคอนสตรัคเตอร์ และอย่าพึ่งพา DOM แบบเต็มที่มีอยู่ การเรียกใช้super()หลังจากแตะthisจะแสดงReferenceErrorโดยปกติแล้วแอตทริบิวต์จะแสดงอยู่ (แต่ไม่เสมอไป) รายการย่อยที่ยาวอาจยังแยกวิเคราะห์ไม่เสร็จ ใช้connectedCallbackสำหรับการเริ่มต้นแบบครั้งเดียวslotchangeเพื่อตรวจสอบองค์ประกอบย่อยโดยตรง และattributeChangedCallbackเพื่อตรวจสอบแอตทริบิวต์connectedCallbackอาจยังคงเริ่มทำงานก่อนที่จะแยกวิเคราะห์รายการย่อยทั้งหมด ดังนั้นอย่าละเว้นการเรียกslotchange/attributeChangedCallbackที่ตามมา attributeChangedCallbackจะเริ่มทำงานสำหรับobservedAttributesเท่านั้น และควรมีค่าใช้จ่ายต่ำสำหรับแอตทริบิวต์ที่ไม่ส่งผลต่อเอาต์พุต- ระบบจะจับภาพฮุกวงจรและ
observedAttributesไว้เมื่อลงทะเบียนcustomElements.define()จะอ่านทั้ง 2 อย่างเพียงครั้งเดียว การเพิ่มฮุกหรือขยายobservedAttributesในคลาสหลังจากการเรียกจะถูกละเว้นโดยไม่มีการแจ้งเตือน โดยเฉพาะอย่างยิ่งattributeChangedCallbackจะไม่เริ่มทำงานเว้นแต่observedAttributesจะแสดงอยู่แล้วในเวลาdefine()
ช่วงเวลาอัปเกรดและ:defined
องค์ประกอบอาจมีอยู่ใน DOM ก่อนที่จะมีการลงทะเบียนคลาส (ใน HTML ที่แสดงผลฝั่งเซิร์ฟเวอร์ หรือเมื่อสคริปต์โหลดช้า) จนกว่าจะมีการกำหนด HTMLElement ที่ไม่มีการทำงานใดๆ เมื่อ customElements.define() ทำงาน เบราว์เซอร์จะอัปเกรดองค์ประกอบที่ตรงกันทุกรายการที่มีอยู่ในเอกสาร โดยเรียกใช้คอนสตรัคเตอร์และ connectedCallback จากนั้น
ผลลัพธ์ 2 อย่างที่โมเดลมักจะพลาด ได้แก่
- แอตทริบิวต์และองค์ประกอบย่อยอาจมีอยู่แล้วเมื่อตัวสร้าง/
connectedCallbackทำงานในการอัปเกรด อ่านแอตทริบิวต์ที่มีอยู่ในconnectedCallbackเพื่อซิงค์สถานะเริ่มต้น และอย่าถือว่าองค์ประกอบเริ่มต้นด้วยค่าว่าง - องค์ประกอบย่อยอาจยังขาดหายไปแม้จะอยู่ใน
connectedCallbackเนื่องจากโปรแกรมแยกวิเคราะห์ HTML สามารถแสดงผลระหว่างการแยกวิเคราะห์รายการย่อยที่ยาวได้ ดังนั้น Callback จึงอาจทำงานก่อนที่จะแนบองค์ประกอบย่อยทั้งหมดในบางครั้ง ดูหัวข้อตัวสร้างในวงจรด้านบนสำหรับรูปแบบ (slotchange+attributeChangedCallback)
ซ่อนองค์ประกอบที่ไม่ได้กำหนดเพื่อหลีกเลี่ยงเนื้อหาที่ไม่มีการจัดรูปแบบแบบฉับพลัน แต่เปิดไม่สำเร็จ เนื้อหาจึงยังคงเข้าถึงได้หากสคริปต์ไม่โหลด สร้างภาพเคลื่อนไหว opacity โดยมีการหน่วงเวลาแทนที่จะซ่อนอย่างเข้มงวดด้วย visibility: hidden เพื่อให้องค์ประกอบแสดงตัวเองหลังจากหมดเวลาแม้ว่าจะไม่เคยอัปเกรดก็ตาม
my-element:not(:defined) {
opacity: 0;
/* Reveal anyway after 2s, so a failed or slow script never hides content for good. */
animation: defined-fallback 0.2s 2s forwards;
}
@keyframes defined-fallback { to { opacity: 1; } }
กำหนดขอบเขตของ Guard เหล่านี้ให้กับองค์ประกอบที่ต้องการ หรือกำหนดให้กับซับทรีที่เฉพาะเจาะจงผ่าน :has(:not(:defined)) แทนที่จะเป็น body ทั้งหมด วิดเจ็ตที่ไม่สำคัญและทำงานช้า (เช่น ปุ่มแชร์) ไม่ควรทำให้การแสดงผลทั้งหน้าล่าช้า
การแมปแอตทริบิวต์ ⇄ พร็อพเพอร์ตี้
ซิงค์แอตทริบิวต์ HTML และพร็อพเพอร์ตี้ JS เพื่อให้ใช้องค์ประกอบได้จากทั้งมาร์กอัปและสคริปต์ ค่าแอตทริบิวต์จะเป็นสตริงเสมอ ให้แปลงประเภท
class MyToggle extends HTMLElement {
static observedAttributes = ['label', 'count', 'disabled'];
// String attribute reflected via getter/setter.
get label() { return this.getAttribute('label') ?? ''; }
set label(v) {
v ? this.setAttribute('label', v) : this.removeAttribute('label');
}
// Numeric attribute: coerce from string on read, serialize on write.
get count() { return Number(this.getAttribute('count')) || 0; }
set count(v) { this.setAttribute('count', String(v)); }
// Boolean attribute: present = true, absent = false. Never set it to "false".
get disabled() { return this.hasAttribute('disabled'); }
set disabled(on) { this.toggleAttribute('disabled', !!on); }
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal !== newVal) this.render();
}
}
แหล่งที่มาของมูลค่า
ตัวอย่างด้านบนจะเก็บ DOM ไว้เป็นแหล่งข้อมูลที่ถูกต้องเพียงแหล่งเดียว โดย Getter แต่ละรายการจะอ่าน getAttribute ซึ่งเป็นค่าเริ่มต้นที่เหมาะสมสำหรับแอตทริบิวต์ที่เรียบง่ายและคล้ายสตริงจำนวนหนึ่ง เนื่องจากมีเพียงที่เดียวที่สถานะจะอยู่ได้ จึงไม่สามารถยกเลิกการซิงค์ได้
สำหรับคอมโพเนนต์ที่สมบูรณ์ยิ่งขึ้น ให้เก็บแหล่งข้อมูลที่เชื่อถือได้ไว้ในช่อง JS และแสดงออกไปภายนอกเป็นแอตทริบิวต์เฉพาะเมื่อมีสิ่งอื่นที่ต้องเห็น เช่น การกำหนดเป้าหมาย CSS หรือผู้บริโภคที่อ่านข้อมูลนั้น การอ่านฟิลด์มีค่าใช้จ่ายน้อยกว่าการแยกวิเคราะห์และการบังคับแอตทริบิวต์ในทุกการเข้าถึง และค่าที่ไม่ใช่สตริงจะไม่ไปกลับผ่านการแปลงเป็นอนุกรม นี่คือเหตุผลที่ไลบรารีอย่าง Lit เก็บสถานะไว้ใน JS และแสดงผลแบบเลือกแทนที่จะถือว่าแอตทริบิวต์เป็นพื้นที่เก็บข้อมูล โดยจะแสดงเฉพาะสิ่งที่โลกภายนอกต้องสังเกต ไม่ใช่พร็อพเพอร์ตี้ทั้งหมด
ข้อควรระวัง:
observedAttributesdiscipline: แสดงเฉพาะแอตทริบิวต์ที่การเปลี่ยนแปลงต้องแสดงผลอีกครั้งหรือทริกเกอร์ผลข้างเคียง การสังเกตทุกอย่างจะเพิ่ม Callback ให้กับการเปลี่ยนแปลงทุกรายการโดยไม่มีประโยชน์- ลูปการสะท้อน: Setter ที่เขียนแอตทริบิวต์จะเข้าสู่
attributeChangedCallbackอีกครั้ง ป้องกันด้วยการตรวจสอบoldVal !== newValตามที่อธิบายไว้ข้างต้น - สถานะเฉพาะสไตล์: สำหรับสถานะภายในที่มีอยู่เพื่อขับเคลื่อนการจัดสไตล์เท่านั้น (กำลังโหลด ทำงานอยู่ ขยาย) ให้เลือกสถานะที่กำหนดเองแทนการแสดงแอตทริบิวต์สังเคราะห์ โปรดดูสถานะที่กำหนดเองด้านล่าง แสดงผลเป็นแอตทริบิวต์เฉพาะเมื่อผู้บริโภคต้องตั้งค่าสถานะจากมาร์กอัปได้
สถานะที่กำหนดเอง
สถานะที่กำหนดเองคือแฟล็กบูลีนที่องค์ประกอบแสดงเกี่ยวกับตัวมันเองเพื่อให้ CSS กำหนดเป้าหมาย โดยตั้งค่าผ่าน ElementInternals และจับคู่กับคลาสจำลอง :state() ใช้สำหรับสถานะ UI รันไทม์ภายใน เช่น busy, expanded, invalid แทนที่จะแสดงแอตทริบิวต์สังเคราะห์ เนื่องจากจะไม่ขัดแย้งกับแอตทริบิวต์ของผู้ใช้ ไม่ปรากฏใน DOM และเป็นแบบอ่านอย่างเดียวจากภายนอก (ผู้ใช้สามารถจัดรูปแบบ :state() ได้ แต่จะปลอมแปลงไม่ได้)
class AsyncButton extends HTMLElement {
#internals;
#action = async () => {};
constructor() {
super();
this.#internals = this.attachInternals();
}
set action(fn) { this.#action = fn; } // host supplies the work to run
async run() {
if (this.#internals.states.has('busy')) return; // guard re-entry
this.#internals.states.add('busy');
this.#internals.states.delete('success');
try {
await this.#action();
this.#internals.states.add('success');
} catch {
this.#internals.states.add('error');
} finally {
this.#internals.states.delete('busy');
}
}
}
CustomStateSet เป็นชุด ดังนั้นสถานะอิสระจะประกอบกัน (busy บวก error) โดยไม่ต้องสร้าง enum data-status แบบรวม
สถานะที่กำหนดเองจะใช้ได้เฉพาะในรันไทม์เท่านั้น ระบบไม่ได้จัดรูปแบบเป็นอนุกรมไปยัง DOM ดังนั้นจึงไม่คงอยู่หลังจากการโหลดซ้ำ การสร้างองค์ประกอบใหม่ หรือการแสดงผลฝั่งเซิร์ฟเวอร์ หากต้องตั้งค่าสถานะอย่างชัดเจนในมาร์กอัปหรือคงอยู่ตลอดการโหลด นั่นเป็นหน้าที่ของแอตทริบิวต์ (ดูตำแหน่งที่ค่าอยู่) โดยให้กู้คืนใน connectedCallback จากแหล่งข้อมูลที่เชื่อถือได้ หลักการทั่วไป: สถานะที่กำหนดเองสำหรับสิ่งที่คอมโพเนนต์ได้รับและเป็นเจ้าของในรันไทม์ และจำเป็นต้องจัดสไตล์เท่านั้น แอตทริบิวต์สำหรับสิ่งที่ต้องตั้งค่าจากมาร์กอัปหรือทำให้เป็นอนุกรม
ดูวิธีจัดรูปแบบสถานะเหล่านี้ทั้งภายในคอมโพเนนต์และจากสไตล์ชีตของผู้ใช้ได้ที่การจัดรูปแบบคอมโพเนนต์เว็บ
รูปแบบการตั้งชื่อ
- สถานะส่วนตัวที่แท้จริง: ฟิลด์
#แบบเนทีฟ (#count) สำหรับการห่อหุ้มแบบเข้มงวด โดยใช้คำนำหน้า_เฉพาะเมื่อคลาสย่อยยังคงต้องเข้าถึงฟิลด์ดังกล่าว โปรดทราบว่าระบบจะไม่รับค่าช่อง#โดยคลาสย่อยจะมองไม่เห็นช่องส่วนตัวของคลาสพื้นฐาน - พร็อพเพอร์ตี้ที่มีคีย์เป็นสัญลักษณ์เป็นตัวกลาง โดยซ่อนจากการแจงนับปกติ แต่เข้าถึงได้ด้วยโค้ดใดก็ตามที่มีสัญลักษณ์ (ส่งออกหรือแสดงเป็นฟิลด์
staticในคลาส) เนื่องจากสัญลักษณ์แต่ละตัวไม่ซ้ำกัน สัญลักษณ์ 2 ตัวที่ใช้คำอธิบายร่วมกันจึงไม่ขัดแย้งกัน ซึ่งมีประโยชน์เมื่อคลาสย่อยหรือคลาสพี่น้องต้องการการเข้าถึงที่มีการควบคุมซึ่งฟิลด์#ไม่สามารถให้ได้ - อย่าใช้แอตทริบิวต์ส่วนกลาง (
style,class,id,slot,part,title,lang,dir) กับพร็อพเพอร์ตี้ของคุณเอง เนื่องจากจะทำให้ลักษณะการทำงานในตัวเสียหาย โปรดทราบว่าdisabledจะเป็นแบบส่วนกลางเฉพาะในตัวควบคุมแบบฟอร์มเท่านั้น ส่วนในองค์ประกอบอื่นๆ คุณต้องใช้เอฟเฟกต์ด้วยตนเอง
เมื่อตั้งชื่อแพลตฟอร์ม API ใหม่ ให้ทำตามแบบแผนของแพลตฟอร์มเอง โดยใช้ Kebab Case ตัวพิมพ์เล็กที่มีขีดกลางอย่างน้อย 1 ขีดสำหรับชื่อแท็กองค์ประกอบที่กำหนดเอง (เช่น my-toggle) lowerCamelCase สำหรับพร็อพเพอร์ตี้และเมธอด และชื่อแอตทริบิวต์ตัวพิมพ์เล็กที่ซิงค์กับพร็อพเพอร์ตี้ที่เกี่ยวข้อง