SiamCafe.net Blog
Technology

Stencil.js Service Mesh Setup

stenciljs service mesh setup
Stencil.js Service Mesh Setup | SiamCafe Blog
2025-11-27· อ. บอม — SiamCafe.net· 8,956 คำ

Stencil.js คืออะไร

Stencil.js เป็น Compiler ที่สร้างโดยทีม Ionic สำหรับสร้าง Web Components ตามมาตรฐาน W3C โดยใช้ TypeScript และ JSX จุดเด่นคือ Component ที่สร้างออกมาเป็น Native Web Components ที่ทำงานได้ในทุก Framework ไม่ว่าจะเป็น React, Angular, Vue หรือ Vanilla JavaScript โดยไม่ต้องพึ่ง Runtime Library เพิ่มเติม

Stencil ใช้ Virtual DOM สำหรับ Rendering ที่มีประสิทธิภาพสูง รองรับ Lazy Loading แบบอัตโนมัติ มี Built-in SSR (Server-Side Rendering) และสร้าง Output ที่มีขนาดเล็กมากเมื่อเทียบกับ Framework อื่นๆ ทำให้เหมาะสำหรับสร้าง Design System หรือ Component Library ที่ต้องใช้ข้าม Project

การตั้งค่า Stencil.js Project

# สร้าง Stencil Project ใหม่
npm init stencil

# เลือก component (สำหรับ Library) หรือ app (สำหรับ Application)
# เลือก: component
# ชื่อ project: my-web-components

cd my-web-components
npm install

# โครงสร้าง Project
# ├── src/
# │   ├── components/       # Web Components
# │   │   └── my-component/
# │   │       ├── my-component.tsx
# │   │       ├── my-component.css
# │   │       └── my-component.spec.ts
# │   ├── index.html         # Dev preview
# │   └── index.ts
# ├── stencil.config.ts      # Stencil Configuration
# ├── tsconfig.json
# └── package.json

# รัน Development Server
npm start

# Build สำหรับ Production
npm run build

# รัน Unit Tests
npm test

สร้าง Component ตัวอย่าง

// src/components/app-header/app-header.tsx
import { Component, Prop, State, h } from '@stencil/core';

@Component({
  tag: 'app-header',
  styleUrl: 'app-header.css',
  shadow: true,
})
export class AppHeader {
  @Prop() appTitle: string = 'My App';
  @Prop() logoUrl: string = '';
  @State() menuOpen: boolean = false;

  private toggleMenu = () => {
    this.menuOpen = !this.menuOpen;
  };

  render() {
    return (
      <header class="app-header">
        <div class="header-content">
          {this.logoUrl && (
            <img src={this.logoUrl} alt="Logo" class="logo" />
          )}
          <h1>{this.appTitle}</h1>
          <button
            class="menu-toggle"
            onClick={this.toggleMenu}
            aria-label="Toggle menu"
          >
            ☰
          </button>
        </div>
        <nav class={`nav-menu `}>
          <slot name="nav-items"></slot>
        </nav>
      </header>
    );
  }
}

// src/components/data-table/data-table.tsx
import { Component, Prop, Watch, State, h } from '@stencil/core';

interface TableColumn {
  key: string;
  label: string;
  sortable?: boolean;
}

@Component({
  tag: 'data-table',
  styleUrl: 'data-table.css',
  shadow: true,
})
export class DataTable {
  @Prop() columns: string = '[]';
  @Prop() data: string = '[]';
  @Prop() apiEndpoint: string = '';
  @State() parsedColumns: TableColumn[] = [];
  @State() parsedData: any[] = [];
  @State() sortKey: string = '';
  @State() sortDir: 'asc' | 'desc' = 'asc';
  @State() loading: boolean = false;

  @Watch('columns')
  parseColumns(val: string) {
    this.parsedColumns = JSON.parse(val);
  }

  @Watch('data')
  parseData(val: string) {
    this.parsedData = JSON.parse(val);
  }

  async componentWillLoad() {
    this.parseColumns(this.columns);
    if (this.apiEndpoint) {
      await this.fetchData();
    } else {
      this.parseData(this.data);
    }
  }

  private async fetchData() {
    this.loading = true;
    try {
      const resp = await fetch(this.apiEndpoint);
      this.parsedData = await resp.json();
    } catch (err) {
      console.error('Failed to fetch data:', err);
    }
    this.loading = false;
  }

  private handleSort(key: string) {
    if (this.sortKey === key) {
      this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
    } else {
      this.sortKey = key;
      this.sortDir = 'asc';
    }
    this.parsedData = [...this.parsedData].sort((a, b) => {
      const va = a[key], vb = b[key];
      const cmp = va > vb ? 1 : va < vb ? -1 : 0;
      return this.sortDir === 'asc' ? cmp : -cmp;
    });
  }

  render() {
    if (this.loading) {
      return <div class="loading">Loading...</div>;
    }
    return (
      <table>
        <thead>
          <tr>
            {this.parsedColumns.map(col => (
              <th
                onClick={() => col.sortable && this.handleSort(col.key)}
                class={col.sortable ? 'sortable' : ''}
              >
                {col.label}
                {this.sortKey === col.key && (this.sortDir === 'asc' ? ' ▲' : ' ▼')}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {this.parsedData.map(row => (
            <tr>
              {this.parsedColumns.map(col => (
                <td>{row[col.key]}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    );
  }
}

Stencil.js Configuration สำหรับ Production

// stencil.config.ts
import { Config } from '@stencil/core';

export const config: Config = {
  namespace: 'my-web-components',
  taskQueue: 'async',
  sourceMap: false,
  outputTargets: [
    // สร้าง Custom Elements Bundle
    {
      type: 'dist-custom-elements',
      customElementsExportBehavior: 'auto-define-custom-elements',
      minify: true,
    },
    // สร้าง Distribution สำหรับ CDN
    {
      type: 'dist',
      esmLoaderPath: '../loader',
    },
    // สร้าง www สำหรับ Standalone App
    {
      type: 'www',
      serviceWorker: null,
      baseUrl: '/',
      copy: [
        { src: 'assets', dest: 'assets' },
      ],
    },
  ],
  extras: {
    enableImportInjection: true,
  },
  testing: {
    browserArgs: ['--no-sandbox', '--disable-setuid-sandbox'],
    coverageThreshold: {
      global: {
        branches: 80,
        functions: 80,
        lines: 80,
        statements: 80,
      },
    },
  },
};

Dockerize Stencil.js Application

# Dockerfile — Multi-stage Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --no-audit
COPY . .
RUN npm run build

FROM nginx:1.25-alpine
# Nginx Configuration สำหรับ SPA
COPY --from=builder /app/www /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Security Headers
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup && \
    chown -R appuser:appgroup /usr/share/nginx/html && \
    chown -R appuser:appgroup /var/cache/nginx && \
    chown -R appuser:appgroup /var/log/nginx && \
    touch /var/run/nginx.pid && \
    chown appuser:appgroup /var/run/nginx.pid

USER appuser
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

---
# nginx.conf
server {
    listen 8080;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    # Gzip Compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml;
    gzip_min_length 256;

    # Cache Static Assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA Routing
    location / {
        try_files $uri $uri/ /index.html;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    }

    # Health Check
    location /healthz {
        return 200 "ok";
        add_header Content-Type text/plain;
    }
}

---
# Build และ Push Docker Image
docker build -t registry.company.com/stencil-app:v1.0.0 .
docker push registry.company.com/stencil-app:v1.0.0

Deploy บน Kubernetes กับ Istio Service Mesh

# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: stencil-frontend
  namespace: production
  labels:
    app: stencil-frontend
    version: v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: stencil-frontend
  template:
    metadata:
      labels:
        app: stencil-frontend
        version: v1
      annotations:
        sidecar.istio.io/inject: "true"
    spec:
      containers:
        - name: stencil-frontend
          image: registry.company.com/stencil-app:v1.0.0
          ports:
            - containerPort: 8080
              name: http
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 250m
              memory: 256Mi
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: stencil-frontend
---
apiVersion: v1
kind: Service
metadata:
  name: stencil-frontend
  namespace: production
spec:
  selector:
    app: stencil-frontend
  ports:
    - port: 80
      targetPort: 8080
      name: http

---
# istio/virtual-service.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: stencil-frontend
  namespace: production
spec:
  hosts:
    - "app.example.com"
  gateways:
    - istio-system/main-gateway
  http:
    - match:
        - uri:
            prefix: /
      route:
        - destination:
            host: stencil-frontend
            port:
              number: 80
          weight: 100
      retries:
        attempts: 3
        perTryTimeout: 2s
        retryOn: "5xx, reset, connect-failure"
      timeout: 10s
      corsPolicy:
        allowOrigins:
          - exact: "https://example.com"
        allowMethods:
          - GET
          - OPTIONS
        allowHeaders:
          - Content-Type
        maxAge: "24h"

---
# istio/destination-rule.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: stencil-frontend
  namespace: production
spec:
  host: stencil-frontend
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        h2UpgradePolicy: DEFAULT
        http1MaxPendingRequests: 100
        http2MaxRequests: 1000
    outlierDetection:
      consecutive5xxErrors: 3
      interval: 30s
      baseEjectionTime: 60s
      maxEjectionPercent: 50
    tls:
      mode: ISTIO_MUTUAL

Observability สำหรับ Stencil.js บน Service Mesh

เมื่อ Deploy บน Istio Service Mesh จะได้ Observability มาฟรีหลายอย่าง แต่ยังต้องตั้งค่า Frontend Monitoring เพิ่มเติมเพื่อติดตาม User Experience

Canary Deployment สำหรับ Stencil.js บน Istio

# istio/canary-virtual-service.yaml
# Canary: ส่ง 10% Traffic ไปยัง v2
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: stencil-frontend
  namespace: production
spec:
  hosts:
    - "app.example.com"
  gateways:
    - istio-system/main-gateway
  http:
    - match:
        - headers:
            x-canary:
              exact: "true"
      route:
        - destination:
            host: stencil-frontend
            subset: v2
    - route:
        - destination:
            host: stencil-frontend
            subset: v1
          weight: 90
        - destination:
            host: stencil-frontend
            subset: v2
          weight: 10

---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: stencil-frontend
  namespace: production
spec:
  host: stencil-frontend
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL

Stencil.js คืออะไรและต่างจาก React อย่างไร

Stencil.js เป็น Compiler สำหรับสร้าง Web Components มาตรฐาน W3C ที่ทำงานได้ทุก Framework ต่างจาก React ที่เป็น Library เฉพาะ Ecosystem Component ที่สร้างจาก Stencil เป็น Native Custom Elements ที่ใช้ใน React, Angular, Vue หรือ Vanilla JS ได้โดยไม่ต้องพึ่ง Runtime ทำให้เหมาะสำหรับ Design System ที่ใช้ข้าม Project

Service Mesh จำเป็นสำหรับ Frontend Microservices หรือไม่

จำเป็นเมื่อมี Frontend Microservices หลายตัวที่ต้องสื่อสารกันหรือต้องการ Canary Deployment, mTLS, Rate Limiting โดยไม่ต้องแก้ Code ใน Application สำหรับ Stencil App ตัวเดียวที่ Serve Static Files อาจใช้แค่ Ingress Controller ก็เพียงพอ แต่หากอยู่ใน Microservices Architecture ที่ใช้ Istio อยู่แล้วก็ควร Integrate เข้าไปด้วย

วิธี Deploy Stencil.js App บน Kubernetes ทำอย่างไร

Build เป็น Static Files ด้วย npm run build จากนั้น Pack เป็น Docker Image ที่ใช้ Nginx serve static files สร้าง Kubernetes Deployment, Service และ Istio VirtualService สำหรับ Traffic Routing ใช้ kubectl apply -f Deploy ทั้งหมด และตรวจสอบด้วย kubectl get pods ว่า Pod ทำงานปกติ

สรุปและแนวทางปฏิบัติ

Stencil.js เป็นเครื่องมือที่ทรงพลังสำหรับสร้าง Web Components ที่ใช้ได้ทุก Framework และเมื่อนำมา Deploy บน Kubernetes ที่ใช้ Istio Service Mesh จะได้ระบบที่มี Observability, Security (mTLS) และ Traffic Management ในตัว สิ่งสำคัญคือการตั้งค่า Nginx ให้เหมาะสมสำหรับ SPA, การทำ Multi-stage Docker Build เพื่อ Image ที่มีขนาดเล็ก และการใช้ Istio VirtualService สำหรับ Canary Deployment เพื่อลดความเสี่ยงในการ Release Version ใหม่

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

Htmx Alpine.js Service Mesh Setupอ่านบทความ → Linkerd Service Mesh Production Setup Guideอ่านบทความ → mTLS Service Mesh Machine Learning Pipelineอ่านบทความ → OPA Gatekeeper Service Mesh Setupอ่านบทความ → Flatcar Container Linux Service Mesh Setupอ่านบทความ →

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