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
- Istio Metrics: Request Rate, Error Rate, Latency (RED Metrics) ถูกเก็บอัตโนมัติผ่าน Envoy Sidecar
- Distributed Tracing: ใช้ Jaeger หรือ Zipkin ติดตาม Request ตั้งแต่ Frontend จน Backend
- Kiali Dashboard: แสดง Service Graph ให้เห็นภาพรวมการสื่อสารระหว่าง Services
- Web Vitals Monitoring: ติดตาม Core Web Vitals (LCP, FID, CLS) ของ Stencil App
- Error Tracking: ใช้ Sentry หรือ Rollbar จับ JavaScript Errors ใน Production
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 ใหม่
