ข้ามไปยังเนื้อหา

การเรนเดอร์เนื้อหา

class ของ custom element มี constructor เหมือน ES class อื่น ๆ แต่ constructor เป็นที่ที่ผิดสำหรับการทำงานกับ DOM เมื่อ browser เรียก constructor element จะมีอยู่ในรูปอ็อบเจกต์ JavaScript แต่ยังไม่ถูกเพิ่มเข้าไปในเอกสาร — ยังไม่มี parent, attribute อาจยัง parse ไม่ครบ และ children ก็ยังไม่ปรากฏ

ให้ใช้ connectedCallback แทน จะถูกเรียกทุกครั้งที่ element ถูก แทรกเข้าไปในเอกสารที่เชื่อมต่อแล้ว ซึ่ง ณ จุดนั้น attribute และ light DOM children ของ element พร้อมใช้งานแล้ว:

class MyCard extends HTMLElement {
constructor() {
super(); // always required
// DO NOT touch this.innerHTML, this.children, or attributes here
}
connectedCallback() {
// Safe to read attributes and manipulate DOM here
const title = this.getAttribute('title') || 'Card';
this.innerHTML = '<h2>' + title + '</h2>';
}
}

innerHTML คือวิธีที่เร็วที่สุดในการสร้างเทมเพลตออกมา:

connectedCallback() {
this.innerHTML = '<p>Hello from <strong>innerHTML</strong></p>';
}

สำหรับค่าที่เปลี่ยนแปลงได้หรือค่าที่ผู้ใช้ป้อนเข้ามา ให้ sanitize ก่อนเสมอ หรือใช้ DOM API แทนเพื่อหลีกเลี่ยง XSS:

connectedCallback() {
const p = document.createElement('p');
const userInput = this.getAttribute('label') || '';
p.textContent = userInput; // textContent never parses HTML
this.appendChild(p);
}

this.getAttribute(name) คืนค่า attribute เป็นสตริง หรือคืน null หาก attribute ไม่มีอยู่ รูปแบบ this.getAttribute('x') || 'default' ปลอดภัยเพราะ null || 'default' ให้ผลเป็น 'default':

connectedCallback() {
const color = this.getAttribute('color') || 'blue';
const label = this.getAttribute('label') || 'Button';
this.innerHTML =
'<button style="background:' + color + '">' + label + '</button>';
}

ลองเพิ่ม tag <color-card> ตัวที่สองโดยใช้ color และ label ที่ต่างกันในแผง HTML

ตัวเลือกBenefitCost
innerHTMLเขียนเทมเพลตได้เร็วและกระชับเสี่ยง XSS ถ้าใส่ค่าที่ผู้ใช้ป้อนเข้ามาโดยไม่ sanitize
DOM API (createElement, textContent)ปลอดภัยจาก XSS เพราะ textContent ไม่ parse HTMLเขียนโค้ดยาวและอ่านยากกว่าเมื่อโครงสร้างซับซ้อน
  • จัดการ DOM ใน constructor แทน connectedCallback — ตอนนั้น element ยังไม่ถูกแทรกเข้าเอกสาร attribute อาจยัง parse ไม่ครบ ทำให้ getAttribute คืนค่าไม่ตรงที่คาด
  • ใส่ค่าจากผู้ใช้ลงใน innerHTML โดยตรง — เช่น this.innerHTML = userInput เปิดช่องให้เกิด XSS ควรใช้ textContent หรือ sanitize ก่อนเสมอ
  • ลืมว่า getAttribute คืน null ไม่ใช่สตริงว่าง — ถ้าไม่ใส่ fallback ด้วย || 'default' ค่าที่ต่อ string จะกลายเป็นคำว่า "null" ปนอยู่ในผลลัพธ์

💡 ตัวอย่างจากของจริง

GitHub<clipboard-copy> ใช้ DOM API และ textContent ในการอ่าน/เขียนเนื้อหาแทน innerHTML แบบดิบ เพื่อป้องกันความเสี่ยงด้านความปลอดภัยใน production

Adobe Spectrum — component ใน Spectrum Web Components เรนเดอร์เนื้อหาใน connectedCallback ตามแพตเทิร์นมาตรฐานของสเปก เพื่อให้ attribute พร้อมใช้งานก่อนแตะ DOM เสมอ

ทำไมคุณจึงควรหลีกเลี่ยงการกำหนด innerHTML ใน constructor?
connectedCallback ถูกเรียกเมื่อใด?
แนวทางใดปลอดภัยกว่าเมื่อเรนเดอร์ข้อมูลที่ผู้ใช้ป้อนเข้ามา?
getAttribute คืนค่าอะไรเมื่อ attribute ไม่มีอยู่บน element?