Skip to content

บทที่ 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 cart

3.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.0Native Federationsingle-spaWeb Components
BundlerWebpack onlyuniversaluniversaluniversal
Cross-frameworkจำกัดจำกัด✅ ดีสุด✅ ดี
Type sharing✅ enhanced⚠️ manual
Tree-shake shared⚠️ partial
DXbestgoodmediumhard
bash
ng add @angular-architects/native-federation
typescript
// 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.com

Shell 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 → localhost

Part 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 alone

10.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

  1. Premature MFE — small team ใช้แล้ว complexity เกินคุ้ม
  2. Tight coupling — MFE depend deep บน MFE อื่น = distributed monolith
  3. No design system — UI inconsistent
  4. Duplicate huge deps — ไม่ shared singleton
  5. Tight version coupling — Angular 17 ใน shell, Angular 18 ใน MFE → conflict
  6. Mix routing chaos — multiple router systems
  7. 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 separately

Part 14: Checkpoint

  1. ปัญหาที่ MFE แก้?
  2. Build-time vs Run-time composition?
  3. Module Federation ทำงานยังไง?
  4. Native Federation ต่าง Module Federation?
  5. Shared singleton ทำไมสำคัญ?
  6. Communicate ระหว่าง MFE 3 วิธี?
  7. Cross-framework MFE ทำยังไง?
  8. Versioning + Rollback ของ MFE?
  9. ทำไมไม่ควรเริ่ม MFE ทันที?
  10. 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 ต้องการ


← บทที่ 10 | สารบัญ | บทที่ 12: Accessibility →