SiamCafe.net Blog
Technology

Web Components SaaS Architecture

web components saas architecture
Web Components SaaS Architecture | SiamCafe Blog
2025-10-16· อ. บอม — SiamCafe.net· 11,703 คำ

Web Components คืออะไร

Web Components เป็นชุดมาตรฐานของ Web Browser ที่ให้สร้าง Reusable Custom HTML Elements ได้ ประกอบด้วย 4 เทคโนโลยีหลัก คือ Custom Elements (สร้าง HTML Tag ใหม่), Shadow DOM (แยก Style และ DOM), HTML Templates (Template ที่ไม่ Render จนกว่าจะใช้) และ Slots (ช่องรับ Content จากภายนอก)

สำหรับ SaaS Architecture Web Components เหมาะอย่างยิ่งเพราะ SaaS มักมีหลาย Product, Micro-frontends หรือ Embeddable Widgets ที่ต้องทำงานข้าม Framework Web Components เป็น Framework-agnostic ใช้ได้ทุกที่ที่มี Browser

สร้าง Web Components พื้นฐาน

// === Custom Element พื้นฐาน ===
// saas-pricing-card.js

class SaaSPricingCard extends HTMLElement {
  // Observed Attributes — React เมื่อ Attribute เปลี่ยน
  static get observedAttributes() {
    return ['plan', 'price', 'currency', 'period', 'featured'];
  }

  constructor() {
    super();
    // สร้าง Shadow DOM
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    // เรียกเมื่อ Element ถูกเพิ่มใน DOM
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // เรียกเมื่อ Observed Attribute เปลี่ยน
    if (oldValue !== newValue) {
      this.render();
    }
  }

  get plan() { return this.getAttribute('plan') || 'Basic'; }
  get price() { return this.getAttribute('price') || '0'; }
  get currency() { return this.getAttribute('currency') || '$'; }
  get period() { return this.getAttribute('period') || '/mo'; }
  get featured() { return this.hasAttribute('featured'); }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          font-family: system-ui, -apple-system, sans-serif;
        }
        .card {
          border: 1px solid ;
          border-radius: 12px;
          padding: 2rem;
          text-align: center;
          background: ;
          color: ;
          transition: transform 0.2s, box-shadow 0.2s;
          position: relative;
        }
        .card:hover {
          transform: translateY(-4px);
          box-shadow: 0 12px 24px rgba(0,0,0,0.1);
        }
        .plan-name {
          font-size: 1.25rem;
          font-weight: 600;
          margin: 0 0 1rem;
        }
        .price {
          font-size: 3rem;
          font-weight: 800;
          line-height: 1;
        }
        .currency { font-size: 1.5rem; vertical-align: super; }
        .period {
          font-size: 0.875rem;
          opacity: 0.7;
          margin-top: 0.25rem;
        }
        .features {
          list-style: none;
          padding: 0;
          margin: 1.5rem 0;
          text-align: left;
        }
        .features ::slotted(li) {
          padding: 0.5rem 0;
          border-bottom: 1px solid ;
        }
        .cta {
          display: inline-block;
          padding: 0.75rem 2rem;
          border-radius: 8px;
          font-weight: 600;
          cursor: pointer;
          border: none;
          font-size: 1rem;
          background: ;
          color: ;
          transition: opacity 0.2s;
        }
        .cta:hover { opacity: 0.9; }
        .badge {
          position: absolute;
          top: -12px;
          left: 50%;
          transform: translateX(-50%);
          background: #f59e0b;
          color: #fff;
          padding: 4px 16px;
          border-radius: 20px;
          font-size: 0.75rem;
          font-weight: 600;
          display: ;
        }
      </style>

      <div class="card">
        <div class="badge">Most Popular</div>
        <h3 class="plan-name"></h3>
        <div class="price">
          <span class="currency"></span>
          
        </div>
        <div class="period"></div>
        <ul class="features">
          <slot name="features"></slot>
        </ul>
        <button class="cta">
          <slot name="cta">Get Started</slot>
        </button>
      </div>
    `;

    // Event Handling
    this.shadowRoot.querySelector('.cta').addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('plan-selected', {
        bubbles: true,
        composed: true,  // ข้าม Shadow DOM boundary
        detail: { plan: this.plan, price: this.price },
      }));
    });
  }
}

// Register Custom Element
customElements.define('saas-pricing-card', SaaSPricingCard);

// === ใช้งานใน HTML ===
// <saas-pricing-card plan="Pro" price="29" featured>
//   <li slot="features">Unlimited Projects</li>
//   <li slot="features">Priority Support</li>
//   <li slot="features">API Access</li>
//   <span slot="cta">Start Free Trial</span>
// </saas-pricing-card>

Design System ด้วย Web Components สำหรับ SaaS

// === Design System — Base Component Class ===
// lib/base-element.js

export class BaseElement extends HTMLElement {
  static styles = '';  // Override ใน Subclass

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._state = {};
  }

  // Reactive State
  setState(newState) {
    this._state = { ...this._state, ...newState };
    this.render();
  }

  // Theme Support
  get theme() {
    return {
      primary: getComputedStyle(this).getPropertyValue('--ds-primary')
        || '#6366f1',
      background: getComputedStyle(this).getPropertyValue('--ds-bg')
        || '#ffffff',
      text: getComputedStyle(this).getPropertyValue('--ds-text')
        || '#1f2937',
      border: getComputedStyle(this).getPropertyValue('--ds-border')
        || '#e5e7eb',
      radius: getComputedStyle(this).getPropertyValue('--ds-radius')
        || '8px',
      font: getComputedStyle(this).getPropertyValue('--ds-font')
        || 'system-ui, sans-serif',
    };
  }

  connectedCallback() {
    this.render();
  }

  render() {
    // Override ใน Subclass
  }
}

// === Design Token CSS Variables ===
// ตั้ง Theme ที่ Root Level
// :root {
//   --ds-primary: #6366f1;
//   --ds-bg: #ffffff;
//   --ds-text: #1f2937;
//   --ds-border: #e5e7eb;
//   --ds-radius: 8px;
//   --ds-font: 'Inter', system-ui, sans-serif;
// }
//
// /* Dark Theme */
// [data-theme="dark"] {
//   --ds-primary: #818cf8;
//   --ds-bg: #111827;
//   --ds-text: #f9fafb;
//   --ds-border: #374151;
// }

// === Button Component ===
// components/ds-button.js
import { BaseElement } from '../lib/base-element.js';

class DSButton extends BaseElement {
  static get observedAttributes() {
    return ['variant', 'size', 'disabled', 'loading'];
  }

  attributeChangedCallback() { this.render(); }

  get variant() { return this.getAttribute('variant') || 'primary'; }
  get size() { return this.getAttribute('size') || 'md'; }
  get isLoading() { return this.hasAttribute('loading'); }

  render() {
    const t = this.theme;
    const sizes = { sm: '8px 16px', md: '10px 20px', lg: '14px 28px' };
    const fontSizes = { sm: '0.875rem', md: '1rem', lg: '1.125rem' };

    this.shadowRoot.innerHTML = `
      <style>
        :host { display: inline-block; }
        button {
          font-family: ;
          padding: ;
          font-size: ;
          border-radius: ;
          border: 1px solid transparent;
          cursor: pointer;
          font-weight: 600;
          transition: all 0.2s;
          display: inline-flex;
          align-items: center;
          gap: 8px;
        }
        button[disabled] { opacity: 0.5; cursor: not-allowed; }
        .primary {
          background: ;
          color: white;
        }
        .outline {
          background: transparent;
          color: ;
          border-color: ;
        }
        .ghost {
          background: transparent;
          color: ;
        }
        .spinner {
          width: 16px; height: 16px;
          border: 2px solid currentColor;
          border-top-color: transparent;
          border-radius: 50%;
          animation: spin 0.6s linear infinite;
        }
        @keyframes spin { to { transform: rotate(360deg); } }
      </style>
      <button class=""
        
        >
        
        <slot></slot>
      </button>
    `;
  }
}
customElements.define('ds-button', DSButton);

Embeddable Widget สำหรับ SaaS

// === Embeddable Chat Widget สำหรับ SaaS ===
// widget/saas-chat-widget.js
// ลูกค้าฝังบนเว็บด้วย <script> tag เดียว

class SaaSChatWidget extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._isOpen = false;
    this._messages = [];
  }

  static get observedAttributes() {
    return ['api-key', 'position', 'theme-color'];
  }

  get apiKey() { return this.getAttribute('api-key') || ''; }
  get position() { return this.getAttribute('position') || 'bottom-right'; }
  get themeColor() { return this.getAttribute('theme-color') || '#6366f1'; }

  connectedCallback() {
    this.render();
    this.setupWebSocket();
  }

  disconnectedCallback() {
    if (this._ws) this._ws.close();
  }

  setupWebSocket() {
    this._ws = new WebSocket(
      `wss://chat.saas-app.com/ws?key=`
    );
    this._ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      this._messages.push(msg);
      this.updateMessages();
    };
  }

  toggleChat() {
    this._isOpen = !this._isOpen;
    this.render();
  }

  sendMessage(text) {
    if (!text.trim()) return;
    this._messages.push({ role: 'user', text, time: new Date() });
    this._ws.send(JSON.stringify({ text }));
    this.updateMessages();
  }

  render() {
    const pos = this.position === 'bottom-left'
      ? 'left: 20px' : 'right: 20px';

    this.shadowRoot.innerHTML = `
      <style>
        :host { position: fixed; bottom: 20px; ; z-index: 99999;
                font-family: system-ui, sans-serif; }
        .fab {
          width: 56px; height: 56px; border-radius: 50%;
          background: ; border: none; cursor: pointer;
          color: white; font-size: 24px; display: flex;
          align-items: center; justify-content: center;
          box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        }
        .panel {
          display: ;
          flex-direction: column; width: 360px; height: 500px;
          background: white; border-radius: 12px;
          box-shadow: 0 8px 32px rgba(0,0,0,0.15);
          margin-bottom: 12px; overflow: hidden;
        }
        .header {
          background: ; color: white;
          padding: 16px; font-weight: 600;
        }
        .messages {
          flex: 1; overflow-y: auto; padding: 16px;
        }
        .input-area {
          display: flex; padding: 12px; border-top: 1px solid #e5e7eb;
        }
        .input-area input {
          flex: 1; padding: 8px 12px; border: 1px solid #e5e7eb;
          border-radius: 8px; outline: none;
        }
        .input-area button {
          margin-left: 8px; padding: 8px 16px;
          background: ; color: white;
          border: none; border-radius: 8px; cursor: pointer;
        }
      </style>

      <div class="panel">
        <div class="header">Chat Support</div>
        <div class="messages" id="messages"></div>
        <div class="input-area">
          <input type="text" placeholder="Type a message..."
                 id="chatInput" />
          <button id="sendBtn">Send</button>
        </div>
      </div>
      <button class="fab" id="fabBtn">
        
      </button>
    `;

    // Events
    this.shadowRoot.getElementById('fabBtn')
      .addEventListener('click', () => this.toggleChat());

    const input = this.shadowRoot.getElementById('chatInput');
    const sendBtn = this.shadowRoot.getElementById('sendBtn');
    if (input && sendBtn) {
      sendBtn.addEventListener('click', () => {
        this.sendMessage(input.value);
        input.value = '';
      });
      input.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
          this.sendMessage(input.value);
          input.value = '';
        }
      });
    }
    this.updateMessages();
  }

  updateMessages() {
    const container = this.shadowRoot.getElementById('messages');
    if (!container) return;
    container.innerHTML = this._messages.map(m => `
      <div style="margin:8px 0;text-align:">
        <span style="display:inline-block;padding:8px 12px;border-radius:12px;
          max-width:80%;background:;
          color:">
          
        </span>
      </div>
    `).join('');
    container.scrollTop = container.scrollHeight;
  }
}
customElements.define('saas-chat-widget', SaaSChatWidget);

// ลูกค้าฝังด้วย:
// <script src="https://cdn.saas-app.com/widget.js"></script>
// <saas-chat-widget api-key="abc123"
//   theme-color="#6366f1"></saas-chat-widget>

ใช้ Web Components กับ React, Vue, Angular

Build และ Distribution

Web Components คืออะไร

Web Components เป็นมาตรฐาน Browser สร้าง Custom HTML Elements ประกอบด้วย Custom Elements, Shadow DOM, HTML Templates และ Slots ทำงานได้ทุก Framework ไม่ต้องมี Runtime เพิ่ม เหมาะสำหรับ Shared Components ข้าม Framework

ทำไมต้องใช้ Web Components กับ SaaS

SaaS มีหลาย Product และ Micro-frontends ที่ใช้ต่าง Framework Web Components สร้าง Shared Components ใช้ได้ทุก Framework เช่น Design System, Embeddable Widget สำหรับลูกค้าฝัง ลด Duplicate Code และ Maintenance Cost

Shadow DOM คืออะไร

Shadow DOM เป็น Encapsulated DOM Tree แยกจาก Main DOM CSS ภายในไม่กระทบภายนอกและภายนอกไม่กระทบภายใน เหมาะกับ SaaS Widget ที่ Embed ในเว็บลูกค้า Style ไม่ชนกัน ใช้ CSS Variables สำหรับ Customization

Web Components ต่างจาก React Components อย่างไร

Web Components เป็นมาตรฐาน Browser ใช้ได้ทุก Framework ไม่ต้อง Runtime แต่ State Management ซับซ้อนกว่า React Components ผูกกับ React มี Virtual DOM และ State Management ดีกว่า เลือกตามความต้องการของ Project

สรุป

Web Components เป็นทางเลือกที่ดีสำหรับ SaaS Architecture ที่ต้องการ Shared UI Components ข้าม Framework ใช้ Shadow DOM สำหรับ Style Encapsulation, Custom Events สำหรับ Communication และ Slots สำหรับ Content Projection เหมาะกับ Design System, Embeddable Widgets และ Micro-frontends Distribute ผ่าน CDN หรือ NPM ให้ลูกค้าใช้ Script Tag เดียว

📖 บทความที่เกี่ยวข้อง

Radix UI Primitives SaaS Architectureอ่านบทความ → Web Components Shift Left Securityอ่านบทความ → Rust Actix Web Machine Learning Pipelineอ่านบทความ → Kubernetes HPA VPA SaaS Architectureอ่านบทความ → Vue Nuxt Server SaaS Architectureอ่านบทความ →

📚 ดูบทความทั้งหมด →