โหมดมืด
บทที่ 11 — Micro-frontends + Module Federation
← บทที่ 10: NgRx + Signal Store | สารบัญ | บทที่ 12: Accessibility →
⚠️ โซนขั้นสูงสุด — มือใหม่ข้ามได้ 100%ใครควรอ่าน: ทีมขนาด 20+ คน หลาย squad ที่ต้อง deploy แยก, มี hot path (ส่วนที่ผู้ใช้เข้าถึงบ่อยหรือสำคัญมาก) ที่อยาก rollout เร็วโดยไม่กระทบทั้งแอป อย่าใช้ Micro-frontends ถ้า:
- ทีม < 10 คน — โอเวอร์เฮดของการ setup/sync version จะมากกว่าประโยชน์
- ยังไม่เคยทำ Angular SPA จบสักโปรเจกต์ — อ่านบท 0-8 ให้แน่นก่อน
บทนี้เป็น reference สำหรับทำงานในบริษัทใหญ่ — ไม่จำเป็นต้องเข้าใจตอนนี้
ใน backend เราเคยตัด monolith เป็น microservices มาแล้ว — คำถามคือ ใน frontend ทำแบบเดียวกันได้ไหม?
📌 ทบทวนศัพท์: monolith = แอปที่โค้ดทั้งหมดอยู่ในโปรเจกต์เดียวและ deploy พร้อมกัน ส่วน microservices = แยกออกเป็น service เล็ก ๆ ที่ deploy อิสระจากกัน
คำตอบคือได้ — Micro-frontends = ตัด big SPA เป็นชิ้นเล็กที่ deploy + own แยก
ใช้เวลา 3-4 ชั่วโมง
Part 1: ทำไม Micro-frontends
1.1 ปัญหา big SPA
เมื่อแอปและทีมโตขึ้นมาก การมีทุกอย่างอยู่ใน codebase เดียวเริ่มสร้างปัญหา — build ช้า, deploy ทั้งก้อนทุกครั้ง, bug เล็ก ๆ ก็ต้องปล่อยใหม่หมด และเปลี่ยนเทคโนโลยียากเพราะทุกคนล็อกอยู่เวอร์ชันเดียวกัน ลองดูอาการที่พบบ่อย:
text
- Team 50 frontend dev บน 1 codebase
- Build time 5-10 min
- Deploy ทั้งก้อนทุกครั้ง
- 1 bug = release ใหม่ทั้งหมด
- Tech stack เปลี่ยนยาก (lock Angular 12 ตลอดกาล)
- Reuse component ระหว่าง app ลำบาก1.2 Micro-frontend Solution
แนวคิด micro-frontend คือ "ตัด" แอปใหญ่ออกเป็นชิ้นเล็ก (MFE) ที่แต่ละทีมเป็นเจ้าของ พัฒนาและ deploy ได้อิสระ โดยมี "Shell" เป็นตัวกลางคอยโหลดแต่ละชิ้นมาประกอบกันและจัดการ routing — เหมือนแยก backend เป็น microservices แต่ทำกับฝั่งหน้าบ้าน:
แต่ละ MFE:
- Own repo
- Own team
- Own deploy
- ปรับ tech stack ของตัวเองได้
1.3 Trade-offs
micro-frontend ไม่ใช่ของฟรี — มันแลกความเป็นอิสระของทีมมาด้วยความซับซ้อนที่เพิ่มขึ้น (routing, แชร์ state, auth, ขนาด bundle ที่ซ้ำกัน) ก่อนตัดสินใจใช้ ต้องชั่งข้อดี-ข้อเสียให้ดี โดยเฉพาะทีมเล็กที่อาจไม่คุ้ม:
✅ Team autonomy (ทีมเป็นอิสระต่อกัน — แต่ละทีม own MFE ของตัวเอง) ✅ Independent deploy (แต่ละ MFE ปล่อยเวอร์ชันใหม่ได้โดยไม่ต้องรอทีมอื่น) ✅ Tech diversity (เลือก framework คนละตัวได้ — Angular/React/Vue) ✅ Reuse across apps (เอา MFE ตัวเดียวไปใช้ในหลายแอป)
❌ Complexity (routing, state sharing, auth — เรื่องที่ปกติ framework จัดให้ ต้องทำเอง) ❌ Bundle size (deps duplicate — เผลอ ๆ user โหลด Angular runtime ซ้ำหลายรอบ) ❌ Performance overhead (โหลด MFE ผ่าน HTTP runtime + ประกอบร่าง = ช้ากว่า monolith) ❌ Design consistency challenge (หลายทีม → UI หน้าตาเริ่มต่าง ต้องมี design system คุม) ❌ Operational complexity (ระบบ DevOps/Monitoring ซับซ้อนขึ้น — หลาย CI/CD, หลาย deployment)
⚠️ อย่าใช้ micro-frontend สำหรับ small team — overhead เกิน
Part 2: Approaches
2.1 Build-time Composition
วิธีแรกคือประกอบร่างตอน build — แต่ละ MFE เป็น npm package ที่ shell import เข้ามาตอนคอมไพล์ ข้อเสียคือทุกครั้งที่ MFE อัปเดต ต้อง build shell ใหม่หมด จึงยังไม่ "อิสระ" จริง:
แต่ละ MFE = npm package → shell import:
typescript
import { CartModule } from '@acme/cart';
import { CatalogModule } from '@acme/catalog';📌
@acme/= ชื่อ organization บน npm (สมมติ — placeholder) ของจริงจะเป็นชื่อบริษัทคุณ เช่น@scb/,@kbank/
→ ต้อง rebuild shell ทุกครั้ง MFE update — ไม่ independent
2.2 Run-time Composition (Module Federation)
วิธีที่ดีกว่าคือประกอบตอน runtime — shell โหลด MFE ผ่าน HTTP ตอนผู้ใช้เปิดใช้งานจริง ทำให้แต่ละ MFE deploy แยกได้สมบูรณ์โดยไม่ต้องแตะ shell เทคโนโลยีหลักคือ Module Federation ของ Webpack 5:
text
Shell loads MFE at runtime via HTTP
→ Independent deploy ✅Webpack 5 Module Federation = เทคโนโลยีที่ให้ JavaScript app หนึ่งโหลดโค้ดจาก app อื่นแบบ runtime — เป็น standard เดิม (ก่อน Angular 17); Angular 17+ ใช้ Native Federation แทน (ดู Part 4)
2.3 iframe
วิธีดั้งเดิมที่สุดคือฝังแต่ละ MFE เป็น iframe — แยกขาดจากกันสมบูรณ์ (CSS/JS ไม่ปนกัน) แต่ประสบการณ์ใช้งานแย่และสื่อสารข้ามกันยาก จึงไม่ค่อยนิยมในแอปสมัยใหม่:
แต่ละ MFE = iframe
- ✅ Isolation perfect
- ❌ UX bad, communication ยาก, routing แยก
2.4 Web Components
อีกวิธีคือห่อแต่ละ MFE เป็น custom element (Web Component) ตามมาตรฐานเว็บ ข้อดีคือใช้ได้ข้าม framework (Angular/React/Vue) แต่การสื่อสารระหว่าง component ยังยุ่งยาก:
แต่ละ MFE = custom element
- ✅ Framework-agnostic
- ❌ Inter-component comm ยาก
Part 3: Module Federation in Angular
⚠️ ก่อนเริ่ม Part 3: วิธีนี้ใช้ Webpack เป็น builder — Angular 17+ (รวมถึง Angular 20 ที่บทนี้ใช้) เปลี่ยน default builder เป็น esbuild แล้ว ถ้าสร้างโปรเจกต์ใหม่ด้วย
ng newตามปกติจะได้ esbuild builder ทันที ซึ่ง config แบบwebpack.config.jsใน Part 3 นี้จะใช้ไม่ได้ตรง ๆ ต้องสลับกลับไปใช้ Webpack builder ก่อน (นอกเหนือขอบเขตบทนี้) — ถ้าเริ่มโปรเจกต์ใหม่ตอนนี้ แนะนำข้ามไปอ่าน Part 4: Native Federation แทน (เป็นวิธีหลักของ Angular 17+) แล้วย้อนกลับมาอ่าน Part 3 ทีหลังเพื่อเข้าใจแนวคิดดั้งเดิม
3.1 Setup
มาลงมือทำจริงด้วย Module Federation บน Angular — ก่อนอื่นต้องสร้าง Angular monorepo (workspace เดียวมีหลาย project) จากนั้นจึงติดตั้ง schematic ของ @angular-architects/module-federation ให้กับทั้งโปรเจกต์ shell และ MFE แต่ละตัว มันจะสร้างไฟล์ config และปรับ build ให้อัตโนมัติ:
bash
# ขั้นที่ 1 — สร้าง Angular monorepo (workspace ไม่มี default app)
ng new my-mfe-app --create-application false
cd my-mfe-app
# ขั้นที่ 2 — สร้าง project shell และ cart ภายใน workspace
ng generate application shell
ng generate application cart
# ขั้นที่ 3 — ติดตั้ง Module Federation
ng add @angular-architects/module-federation --project shell
ng add @angular-architects/module-federation --project cart3.2 Remote (cart MFE)
ฝั่ง MFE (เรียกว่า "remote") ต้องประกาศว่าจะ "เปิดเผย" (expose) อะไรให้ shell หยิบไปใช้บ้าง เช่น component หรือ routes และระบุ dependency ที่จะ "แชร์" ร่วมกับ shell (เช่น Angular core) เพื่อไม่ให้โหลดซ้ำ:
📌 ไฟล์
webpack.config.jsถูก สร้างอัตโนมัติ เมื่อรันng add— อยู่ที่projects/cart/webpack.config.js(root ของ project นั้น)
exposes= ประกาศ "ของ" ที่จะให้ shell ดึงไปใช้ (เช่น Component, Routes)shared= ระบุ dependency ที่แชร์ร่วมกันเพื่อไม่โหลดซ้ำ (เช่น@angular/core)
webpack.config.js (ที่ projects/cart/):
javascript
const { withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'cart',
exposes: {
'./Component': './projects/cart/src/app/cart/cart.component.ts',
'./Routes': './projects/cart/src/app/cart/cart.routes.ts',
},
shared: {
// 🔴 อย่าใช้ strictVersion: true ใน production
// ถ้า shell ใช้ Angular 20.0 แต่ remote ใช้ 20.1 → throw runtime ทันที shell ตาย
// ใช้ singleton: true + version range แทน (warn เฉย ๆ ถ้า mismatch)
// หมายเหตุ: เลข version ต้องตรงกับ Angular major ที่ host ติดตั้งจริง (บทนี้ใช้ v20)
'@angular/core': { singleton: true, requiredVersion: '^20.0.0' },
'@angular/common': { singleton: true, requiredVersion: '^20.0.0' },
'@angular/router': { singleton: true, requiredVersion: '^20.0.0' },
'rxjs': { singleton: true, requiredVersion: '^7.8.0' },
},
});📖 semver (Semantic Versioning) = รูปแบบเลขเวอร์ชัน
MAJOR.MINOR.PATCHเช่น20.1.2— สัญลักษณ์^หมายถึง "ยอมรับ minor/patch ที่สูงกว่าได้ แต่ไม่เปลี่ยน major" เช่น^20.0.0ยอมรับ20.1.0,20.2.3แต่ไม่ยอมรับ21.0.0
🔴 Version mismatch handling — กฎเหล็ก:
สรุปง่าย ๆ ก่อน (กฎ 3 ข้อ):
- ห้ามให้ Angular runtime (โค้ดแกนของ Angular) มี 2 ตัวในแอปเดียวกัน (เดี๋ยว crash) — ใช้
singleton: trueบังคับให้ใช้ตัวเดียวกัน- อย่าใช้
strictVersionเพราะถ้า remote กับ shell version ต่างกันแม้แต่นิดเดียวจะ throw error เปิดเว็บไม่ได้ — ใช้ semver range แทน (เช่น^20.0.0= ยอมรับเวอร์ชัน 20.x ทุกตัว)- ใช้บอตอัปเดต dependency อย่าง Renovate/Dependabot ช่วย sync version ให้ตรงกันทุก repo
รายละเอียด:
singleton: true= บังคับให้ใช้ instance เดียวกัน (instance = สำเนาที่กำลังทำงานอยู่จริง). สำคัญสำหรับ@angular/coreไม่งั้น Zone.js / DI (Dependency Injection) สอง instance ขัดกัน → CD (Change Detection — กลไกที่ Angular ใช้ตรวจการเปลี่ยน state) พังstrictVersion: true= throw error ถ้า version ไม่ match → ห้ามใช้ใน production เพราะถ้า remote deploy ก่อน shell update → user เปิดเว็บแล้วเจอ white screen- ทางที่ถูก:
requiredVersion: '^20.0.0'(semver range — ยอมรับเวอร์ชัน 20.x ทุกตัว) + monorepo (โค้ดทุกโปรเจกต์อยู่ repo เดียว) lock major version ของ Angular ใน shell + ทุก remote ผ่าน Renovate/Dependabot (บอตที่ส่ง PR อัปเดต dependency อัตโนมัติ)
💡 Module Federation 2.0 (Webpack 5 + @module-federation/enhanced) — ปี 2026 เป็น de-facto standard (มาตรฐานโดยพฤตินัย):
สรุป: Module Federation 2.0 เพิ่มฟีเจอร์ที่ทำให้ deploy MFE ง่ายขึ้น — เปลี่ยน URL ของ remote ได้โดยไม่ต้อง redeploy shell, มี type ของ remote ให้ IDE ช่วย auto-complete, มี DevTools เฉพาะสำหรับ debug
- Runtime Plugin (ปลั๊กอินที่ทำงานตอน runtime) — load remote dynamic + retry/fallback ถ้าโหลดไม่ผ่าน
- Manifest Protocol — central registry (ทะเบียนกลาง) ของ remote URLs (เปลี่ยน URL ไม่ต้อง redeploy shell)
- Type Federation — auto-publish
.d.ts(TypeScript declaration files — ไฟล์บอก type ของ API) ให้ shell ได้ IntelliSense (auto-complete ใน IDE) ของ remote API- Chrome DevTools — debug federated module โดยเฉพาะ
- ติดตั้ง:
npm i @module-federation/enhanced+ import จาก@module-federation/runtime
3.3 Shell (host)
ฝั่ง shell (เรียกว่า "host") ทำตรงข้าม — ประกาศรายชื่อ "remotes" ว่าจะไปดึง MFE ตัวไหนจาก URL ไหน และระบุ dependency ที่แชร์ให้ตรงกับฝั่ง remote:
javascript
module.exports = withModuleFederationPlugin({
remotes: {
// ตอน dev — hardcode URL
cart: "http://localhost:4201/remoteEntry.js",
catalog: "http://localhost:4202/remoteEntry.js",
},
shared: {
// ✅ ใช้ singleton + requiredVersion (semver range) — ไม่ใช้ strictVersion
// เพื่อให้สอดคล้องกับคำเตือน "อย่าใช้ strictVersion ใน production" ด้านบน
'@angular/core': { singleton: true, requiredVersion: '^20.0.0' },
// ... same as remote (version range ต้องตรงกัน)
},
});3.4 Load Remote in Shell
ขั้นสุดท้ายคือผูก remote เข้ากับ routing ของ shell — ใช้ loadRemoteModule() ใน loadChildren เพื่อให้ MFE ถูกโหลดเมื่อผู้ใช้นำทางไปยัง path นั้น (lazy load) เช่นเข้าหน้า /cart แล้วค่อยดึง cart MFE มา:
typescript
// shell app.routes.ts
import { loadRemoteModule } from '@angular-architects/module-federation';
export const routes: Routes = [
{
path: 'cart',
loadChildren: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './Routes',
}).then(m => m.CART_ROUTES),
},
{
path: 'catalog',
loadChildren: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4202/remoteEntry.js',
exposedModule: './Routes',
}).then(m => m.CATALOG_ROUTES),
},
];3.5 Dynamic Remote (Production)
typescript
// manifest.json (deployed separately)
{
"cart": "https://cart.acme.com/remoteEntry.js",
"catalog": "https://catalog.acme.com/remoteEntry.js"
}
// shell bootstrap
import { loadManifest } from '@angular-architects/module-federation';
loadManifest('/assets/mf.manifest.json')
.then(() => import('./bootstrap'));→ MFE URL config ที่ runtime → swap ได้โดยไม่ rebuild shell
Part 4: Native Federation (ปี 2026 = standard ของ Angular 17+)
🎯 Native Federation = วิธีหลักของ Angular 17+ (esbuild builder) — esbuild = build tool รุ่นใหม่ที่เร็วกว่า Webpack มาก; Angular 17 เปลี่ยนมาใช้ esbuild เป็น default ทำให้ Webpack plugin แบบเก่าใช้ไม่ได้โดยตรง ใช้ Native Federation แทน (โดย Manfred Steyer ของ @angular-architects)
Native Federation = polyfill Module Federation ผ่าน web standards (Import Maps + es-module-shims) — ไม่ผูกกับ bundler
📌 ศัพท์:
- polyfill = โค้ดเสริมที่ทำให้ feature ใหม่ทำงานได้ในเบราว์เซอร์เก่า/บนเครื่องมือที่ยังไม่รองรับ
- Import Maps = มาตรฐานเว็บใหม่ที่ให้กำหนดได้ว่า
import 'cart'จะไปดึงจาก URL ไหน- es-module-shims = library polyfill ของ Import Maps (ให้เบราว์เซอร์ที่ยังไม่รองรับใช้ได้)
- bundler = เครื่องมือรวมไฟล์ JS หลายไฟล์เป็นไฟล์เดียวสำหรับ deploy เช่น Webpack, esbuild, Vite
4.1 Trade-offs ของ Native Federation
- ✅ ทำงานกับ esbuild + Vite + Webpack เหมือนกัน
- ✅ ใช้ web standard → future-proof
- ⚠️ ณ เวลาที่เขียนบทนี้ (ดู
last_verifiedด้านบนของไฟล์) ยัง ไม่มี true tree-shaking ของ shared dependencies ระดับ Module Federation 2.0 — แต่ฟีเจอร์นี้เปลี่ยนเร็ว ต้องตรวจสอบสถานะล่าสุดในเอกสารทางการก่อนใช้จริง (ตรวจที่@angular-architects/native-federation@19.x— ดู changelog) - ⚠️ dynamic import ต้อง polyfill ด้วย
es-module-shims(~30KB) - ⚠️ ยังไม่มี runtime plugin/manifest แบบ MF 2.0 (กำลังพัฒนา)
4.2 alternative — single-spa
ถ้า frontend ใช้ หลาย framework (Angular shell + React MFE + Vue MFE) → Module Federation/Native Federation ไม่เหมาะ ใช้ single-spa แทน:
- เป็น library ที่ทำหน้าที่เป็น "ผู้ประสาน" ระหว่าง framework หลายตัวในหน้าเว็บเดียวกัน ไม่ขึ้นกับ bundler
- รองรับ Angular/React/Vue/Svelte ใน app เดียว
- มี root-config สำหรับ orchestrate
- ติดตั้ง:
npm i single-spa+single-spa-angular
| Module Federation 2.0 | Native Federation | single-spa | Web Components | |
|---|---|---|---|---|
| Bundler | Webpack only | universal | universal | universal |
| Cross-framework | จำกัด | จำกัด | ✅ ดีสุด | ✅ ดี |
| Type sharing | ✅ enhanced | ⚠️ manual | ❌ | ❌ |
| Tree-shake shared | ✅ | ⚠️ partial | ❌ | ❌ |
| DX | best | good | medium | hard |
bash
ng add @angular-architects/native-federationtypescript
// federation.config.ts (ถูกสร้างอัตโนมัติจาก ng add — TypeScript ไม่ใช่ JSON)
export default {
name: 'cart',
exposes: {
'./Component': './projects/cart/src/app/cart/cart.component.ts',
},
shared: {
// ✅ ใช้ requiredVersion (semver range) — ไม่ใช้ strictVersion: true
// (strictVersion: true จะ throw runtime error ถ้า shell กับ remote version ต่างกันแม้แต่ minor)
'@angular/core': { singleton: true, requiredVersion: '^20.0.0' },
'@angular/common': { singleton: true, requiredVersion: '^20.0.0' },
},
};typescript
// shell
import { loadRemoteModule } from '@angular-architects/native-federation';
const m = await loadRemoteModule('cart', './Component');→ 2026 default สำหรับ Angular MFE
Part 5: Communication Between MFEs
5.1 Routing
วิธีสื่อสารข้าม MFE ที่ง่ายที่สุดคือผ่าน router ของ shell — MFE ตัวหนึ่งสั่งนำทางไปอีก MFE ได้ด้วย router.navigateByUrl() ตามปกติ เพราะทุก MFE ใช้ router instance เดียวกัน (ที่แชร์มาจาก shell):
typescript
// Cart MFE navigate to catalog
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { inject } from '@angular/core';
@Component({ standalone: true })
export class CartComponent {
private router = inject(Router); // ✅ modern pattern (inject() แทน constructor injection)
go() {
this.router.navigateByUrl('/catalog'); // ← uses shell's router
}
}5.2 Shared State
เมื่อ MFE ต้องแชร์ข้อมูลกัน (เช่นจำนวนสินค้าในตะกร้า) มีหลายวิธีให้เลือกตามระดับการ coupling: ส่งสัญญาณผ่าน custom event บน window (หลวมที่สุด), ใช้ service singleton ที่แชร์ร่วมกัน, หรือ event bus สำหรับข้าม framework:
Option A: Custom Events
typescript
// MFE A
window.dispatchEvent(new CustomEvent('cart:item-added', { detail: { item } }));
// MFE B
window.addEventListener('cart:item-added', (e) => { console.log(e.detail); });Option B: Shared Service (Singleton)
typescript
// shared lib (npm package or federated)
@Injectable({ providedIn: 'root' })
export class GlobalCartService {
items = signal<CartItem[]>([]);
add(item: CartItem) { this.items.update(arr => [...arr, item]); }
}→ All MFE ใช้ instance เดียวกัน (shared singleton)
Option C: Cross-framework — Signal/Observable
ใช้เมื่อ MFE ต่าง framework กัน (เช่น Angular shell + React MFE) — event bus = ช่องสื่อสารกลางที่รับ-ส่งเหตุการณ์ระหว่าง MFE โดยไม่ต้อง import กันโดยตรง:
Use @module-federation/utilities หรือ custom event bus
5.3 Auth Propagation
เรื่อง login ไม่ควรให้แต่ละ MFE ทำเอง — ปกติให้ shell จัดการ login ที่เดียว แล้วเก็บ token ไว้ที่ส่วนกลาง (localStorage/cookie หรือ AuthService singleton) ให้ทุก MFE อ่านใช้ร่วมกัน:
Shell handle login → JWT in localStorage/cookie → MFE read
หรือใช้ shared AuthService singleton
Part 6: Styling + Design System
6.1 ปัญหา CSS Conflict
เมื่อหลาย MFE มาอยู่ในหน้าเดียวกัน CSS ของแต่ละตัวอาจชนกัน เช่นทั้งคู่นิยาม .button แต่หน้าตาต่างกัน ทำให้ปุ่มเพี้ยน นี่คือปัญหาคลาสสิกที่ต้องวางแผนแก้ตั้งแต่แรก:
MFE A ใช้ .button { ... }, MFE B ใช้ .button { ... } แบบต่างกัน → conflict
6.2 Solutions
โชคดีที่ Angular ช่วยได้บางส่วนด้วย ViewEncapsulation (scope CSS ต่อ component ให้อยู่แล้ว) แต่ระดับ MFE ยังต้องมีกลยุทธ์เพิ่ม เช่น ใช้ design system ร่วมกัน, ตั้ง prefix ให้ชื่อ class, หรือใช้ Shadow DOM แยกขาด:
ViewEncapsulation
Angular default = Emulated (scoped) → ไม่ conflict ภายใน component
Design System ร่วม
shared package (เช่น @acme/design-system):
text
- Tokens (color, spacing)
- Component library (Material, custom)
- Themeทุก MFE depend → consistent
Style Isolation Tricks
- CSS-in-JS
- Prefix CSS class (
.cart-button,.catalog-button) - Shadow DOM (
ViewEncapsulation.ShadowDom)
Part 7: Bundle Size + Performance
7.1 ปัญหา
ปัญหาใหญ่ด้าน performance ของ MFE คือ ถ้าแต่ละตัวพก Angular runtime มาเอง ผู้ใช้จะต้องโหลด Angular ซ้ำหลายรอบ (เช่น 300KB × 5 = 1.5MB) ทำให้แอปอืดโดยใช่เหตุ:
ทุก MFE ship Angular runtime → ~300KB × 5 MFE = 1.5MB
7.2 Solutions
Shared Singleton
javascript
shared: {
'@angular/core': { singleton: true, requiredVersion: '^20.0.0' },
}→ Angular ใน shell load 1 ครั้ง, MFE ใช้ร่วม (กฎเดียวกับ section 3.2 — ใช้ semver range, ไม่ใช้ strictVersion ใน production)
Required Version (semver range)
javascript
'@angular/core': {
singleton: true,
requiredVersion: '^20.0.0',
// ⚠️ อย่าใส่ strictVersion: true ใน production — ดูคำเตือนใน 3.2
}→ ใช้ semver range ยอมรับ minor mismatch (เช่น 20.0 กับ 20.1 อยู่ร่วมกันได้) — เลี่ยง strictVersion เพราะ throw error ทันทีเมื่อ version ต่าง
Lazy Load Remote
typescript
{
path: 'cart',
loadChildren: () => loadRemoteModule(...)
}MFE โหลดเมื่อ user navigate
Preload
typescript
provideRouter(routes, withPreloading(PreloadAllModules))→ background preload หลัง initial render
Part 8: Deployment
8.1 Independent Deploy
หัวใจของ MFE คือแต่ละตัว deploy ได้อิสระ — มี repo และ CI/CD ของตัวเอง ปล่อยเวอร์ชันใหม่ได้โดยไม่ต้องแตะ shell หรือ MFE อื่น และในทางกลับกัน shell ก็ deploy ได้โดยไม่กระทบ MFE:
text
github.com/acme/cart-mfe → cart.acme.com (own CI/CD)
github.com/acme/catalog-mfe → catalog.acme.com
github.com/acme/shell-app → app.acme.comShell deploy = ไม่ต้องเปลี่ยน MFE MFE deploy = ไม่ต้อง redeploy shell
8.2 Versioning
การคุมเวอร์ชันทำผ่านไฟล์ manifest.json ที่ชี้ว่า MFE แต่ละตัวอยู่ URL ไหน (รวมเลขเวอร์ชัน) แค่แก้ manifest ให้ชี้เวอร์ชันใหม่ shell ก็โหลดของใหม่ทันทีแบบ atomic และถ้าต้อง rollback ก็แค่ย้อน URL กลับ:
json
manifest.json:
{
"cart": "https://cdn.acme.com/cart/v2.3.1/remoteEntry.js"
}Update manifest → shell load version ใหม่ทันที (atomic swap — สลับเวอร์ชันในคราวเดียว ไม่มีสถานะกึ่งกลาง ผู้ใช้เห็นแบบเดิมหรือแบบใหม่เท่านั้น ไม่ปนกัน)
Rollback = แค่เปลี่ยน manifest version
8.3 Multi-environment
text
- prod manifest → prod CDN URLs
- staging manifest → staging URLs
- dev manifest → localhostPart 9: Cross-Framework MFE
Module Federation รองรับ MFE ของ framework ต่าง:
text
Shell (Angular)
├── Cart (Angular)
├── Catalog (React)
├── Profile (Vue)
└── Reports (Svelte)ผ่าน Web Components:
typescript
// React MFE export Web Component
// ⚠️ React ไม่มี built-in API สำหรับ Web Component — ต้องใช้ third-party library เช่น:
// npm install @r2wc/react-to-web-component
import r2wc from '@r2wc/react-to-web-component';
import MyReactComponent from './MyReactComponent';
// props: { data: 'json' } บอก r2wc ว่า attribute ชื่อ "data" ที่ custom element รับมา
// เป็น string รูปแบบ JSON — ให้ parse เป็น object ก่อนส่งเข้าไปเป็น prop ของ React component
const ReactCartElement = r2wc(MyReactComponent, { props: { data: 'json' } });
customElements.define('react-cart', ReactCartElement);html
<!-- Angular shell template ใช้ custom element ที่ React สร้างไว้ -->
<react-cart [attr.data]="jsonData"></react-cart>→ Polyglot frontend (หลายภาษา/framework ในที่เดียว — เหมือน polyglot backend ที่แต่ละ service เลือก stack เองได้)
⚠️ บังคับเฉพาะกรณีจำเป็น — coordinate ยาก
Part 10: Testing
10.1 MFE Standalone
ระดับแรกของการเทสต์คือทดสอบแต่ละ MFE แบบเดี่ยว ๆ เหมือนแอปธรรมดา — รัน unit test ของมันเองได้โดยไม่ต้องพึ่ง shell:
bash
ng test cart # test cart MFE alone10.2 Shell + MFE Integration
ระดับถัดมาคือทดสอบว่า shell โหลดและประกอบ MFE เข้าด้วยกันได้จริงไหม — ตรงนี้ต้องใช้ E2E test (Playwright/Cypress) เปิดเบราว์เซอร์จริงแล้วลองนำทางเข้าไปยัง MFE:
ใช้ Playwright / Cypress end-to-end:
typescript
test('shell loads cart MFE', async ({ page }) => {
await page.goto('http://localhost:4200');
await page.click('[href="/cart"]');
await expect(page.locator('cart-component')).toBeVisible();
});10.3 Contract Testing
Contract Testing = ทดสอบว่า MFE แต่ละตัวส่งและรับข้อมูลในรูปแบบที่ตกลงกันไว้ (contract) ป้องกัน MFE หนึ่งเปลี่ยน type แล้ว MFE อื่นพัง — define shared contract ผ่าน TypeScript types ใน shared lib:
typescript
// shared
export interface CartEvent { type: 'add' | 'remove'; itemId: string; }ทุก MFE ใช้ type นี้ → contract ตรง
Part 11: เลือก Approach
→ อย่ากระโดดข้าม monolith → MFE ทันที
Part 12: Anti-Patterns
- Premature MFE — small team ใช้แล้ว complexity เกินคุ้ม
- Tight coupling — MFE depend deep บน MFE อื่น = distributed monolith
- No design system — UI inconsistent
- Duplicate huge deps — ไม่ shared singleton
- Tight version coupling — Angular 17 ใน shell, Angular 18 ใน MFE → conflict
- Mix routing chaos — multiple router systems
- No fallback — MFE ล่ม = shell ตาย
Part 13: Best Practices
text
☐ Shared design system + theme
☐ Shared package (auth, telemetry, common types)
☐ Module Federation / Native Federation
☐ Singleton + semver range shared deps (ห้ามใช้ strictVersion ใน production)
☐ Lazy load remote
☐ Manifest-driven URLs (no hardcode)
☐ Independent CI/CD per MFE
☐ Versioned MFE URL (CDN immutable)
☐ Error boundary per MFE (isolate failures)
☐ Cross-MFE communication: event bus / shared service
☐ E2E test across MFEs
☐ Monitor each MFE separatelyPart 14: Checkpoint
- ปัญหาที่ MFE แก้?
- Build-time vs Run-time composition?
- Module Federation ทำงานยังไง?
- Native Federation ต่าง Module Federation?
- Shared singleton ทำไมสำคัญ?
- Communicate ระหว่าง MFE 3 วิธี?
- Cross-framework MFE ทำยังไง?
- Versioning + Rollback ของ MFE?
- ทำไมไม่ควรเริ่ม MFE ทันที?
- Design system สำคัญยังไงใน MFE?
Part 15: สรุปบทนี้
- Micro-frontends = ตัด frontend เป็นชิ้น ๆ ที่ deploy แยกกันได้ (independently deployable units)
- Module Federation = มาตรฐานที่อิงกับ Webpack (Webpack-based standard)
- Native Federation = Vite/esbuild + Import Maps (2024+) — มาตรฐานหลักของ Angular 17+
- Shared singleton = แชร์ Angular, RxJS, common libs ตัวเดียวกันทั้งแอป
- Manifest-driven = เปลี่ยน URL ของ MFE ตอน runtime ได้โดยไม่ต้อง redeploy shell
- Cross-framework ทำได้ผ่าน Web Components (custom element มาตรฐานเว็บ)
- เริ่มจาก modular monolith (โครงสร้างที่จัดกลุ่มเป็น module ชัดเจน แต่ยัง deploy รวมเป็นก้อนเดียว) ก่อนเสมอ — ค่อยตัดเป็น MFE เมื่อจำเป็นจริง ๆ
🚀 จบบทที่ 11 — Architecture บทใหญ่
ครบ "core path" 0-11 แล้ว — รากฐาน framework + scale architecture ต่อด้วยบท 12-15 ที่เป็น cross-cutting concerns ที่ production app ต้องการ