GĐ26 — Frontend Architecture: ứng dụng mở rộng, Design System, Micro Frontends & Module Federation
Tài liệu dành cho frontend engineer đã làm được một SPA/React app và muốn giữ codebase, UI consistency cùng nhịp giao hàng khi sản phẩm và số team lớn lên. Giai đoạn này bắt đầu từ kiến trúc modular trong một ứng dụng, rồi mới xem xét chia ứng dụng thành nhiều phần deploy độc lập.
Kiểm chứng ngày 2026-10-05: các đoạn code mới ở GĐ này được cài và chạy trong thư mục tạm (không nằm trong repo) với ESLint 10.12,
typescript-eslint8.71,eslint-plugin-boundaries7.2,@module-federation/runtime2.9.2,@module-federation/vite1.23, webpack 5.111, TanStack Query 5.104, Changesets CLI 3.0, Playwright 1.63,@axe-core/playwright, React 19.3, TypeScript 6.0, Chrome thật điều khiển bằng Playwright. Nguồn tra cứu: module-federation.io, eslint-plugin-boundaries, Changesets, TanStack Query, Design Tokens Format 2025.10. Những gì mỗi mục đã chạy hay chưa được ghi ngay tại mục đó; Radix và React Aria chỉ được kiểm là gói có trên npm, chưa dựng thử.
1. Kết quả cần đạt#
Sau giai đoạn này, bạn có thể:
- Chia frontend theo năng lực nghiệp vụ và luồng người dùng, thay vì chỉ chia theo loại file.
- Thiết lập ranh giới module có public API, dependency rule, route ownership và test tương ứng.
- Thiết kế Design System có token, component API, trạng thái, accessibility, tài liệu, release/versioning và quy trình nhận đóng góp.
- Giải thích Micro Frontend giải quyết bài toán tổ chức đội ngũ và triển khai độc lập như thế nào, cùng chi phí về tải, vận hành và governance.
- Phân biệt package/component library, route-level app, server-side composition, iframe/Web Component và runtime Module Federation.
- Dựng một host + remote bằng Module Federation ở mức minh họa, hiểu
remotes,exposes,shared, version negotiation, remote failure và rollback. - Đưa ra quyết định có bằng chứng: modular monolith, monorepo package hay Micro Frontend phù hợp với quy mô nào.
Điều kiện vào#
Bạn nên đọc JavaScript/TypeScript, viết được HTML/CSS, dùng một UI framework và hiểu route, component state, npm package cùng quá trình build/deploy frontend. Kiến thức backend/CI ở các giai đoạn trước giúp hiểu API, auth và pipeline, nhưng GĐ26 là nhánh chuyên sâu frontend; không cần làm xong GĐ25 Capstone mới được học.
2. Kiến trúc frontend là gì?#
Kiến trúc không phải cây thư mục trông “enterprise”. Nó là các quyết định có chủ đích về:
- Ranh giới: tính năng nào sở hữu state, API, route và UI nào?
- Hướng phụ thuộc: phần nào được phép import phần nào?
- Giao tiếp: module trao đổi dữ liệu qua function, event, route, API hay message contract?
- Thay đổi: một yêu cầu nghiệp vụ bình thường sửa bao nhiêu module và có cần phối hợp bao nhiêu team?
- Chất lượng: lỗi permission, render lại, bundle tăng và visual regression bị phát hiện ở đâu?
Kiến trúc tốt giúp một thay đổi có phạm vi đoán trước. Ví dụ: thay cách tải danh sách sản phẩm nên chủ yếu ở feature catalog; không làm luồng checkout, navigation và mọi package UI cùng thay đổi.
Chia theo feature / vertical slice#
Một tổ chức đơn giản có thể bắt đầu như sau:
features/checkout có thể dùng primitive shared/ui/Button, nhưng shared/ui không import features/checkout. Feature không nên đọc thẳng internal file của feature khác; trao đổi qua public API hẹp hoặc tầng ứng dụng điều phối.
Ví dụ public API:
Consumer import từ package boundary:
Tránh:
Public barrel chỉ có ích nếu nó thật sự là contract. Đừng export mọi file vì “mai mốt tiện”; mỗi export làm tăng bề mặt API cần giữ tương thích.
Phân ranh component#
- Primitive: button, text field, dialog, menu. Không biết order/catalog/user cụ thể.
- Domain component:
ProductPrice,OrderStatus,AddressForm. Hiểu domain nhưng không sở hữu cả page. - Feature/page: điều phối query, mutation, loading/empty/error state và các component domain.
- Application shell: routing, session, navigation toàn app, global providers, error boundaries.
Không có một luật “component phải dưới 200 dòng” đủ tốt cho mọi dự án. Hãy tách khi một module có hơn một lý do thay đổi, public API khó hiểu, test setup nặng hoặc một thay đổi kéo theo sửa nhiều consumer.
3. Ranh giới dữ liệu, route và state#
Chọn đúng nơi giữ state#
| Loại state | Ví dụ | Nơi phù hợp |
|---|---|---|
| Tạm thời của một control | dialog đang mở, input chưa submit | local component state |
| Theo URL, có thể bookmark/share | filter, sort, page, tab | route/query string |
| Dữ liệu từ server | product list, order detail | query/cache layer có invalidation rõ |
| State cross-cutting của app | session hiện tại, locale | application provider/store với owner rõ |
| State bền giữa lần mở | draft, offline queue | IndexedDB/server; có schema version và cleanup |
| State giữa các micro frontend | current route, identity | contract tối thiểu từ shell; tránh global mutable store dùng chung |
Không đưa mọi state vào một global store. Global hóa làm thay đổi nhỏ ảnh hưởng khó theo dõi, mở rộng quyền ghi và khiến feature không còn tự kiểm soát lifecycle của state.
API adapter theo feature#
Feature giữ mapping từ giao thức API sang kiểu UI của nó. Đừng để component rải URL, header, retry policy và cách parse lỗi khắp nơi.
Gắn adapter vào cache dữ liệu server bằng TanStack Query#
Adapter ở trên chỉ biết gọi API. Việc cache, dedupe, hủy request và invalidate sau mutation thuộc về một query layer; TanStack Query là lựa chọn phổ biến cho React. Hook nằm trong feature và chỉ hook này được export qua public API:
Quy tắc đi kèm: mọi giá trị làm đổi kết quả (filter) phải nằm trong queryKey; staleTime là cam kết "dữ liệu này đủ mới bao lâu", không phải mặc định tùy tiện; một QueryClient cho cả ứng dụng do shell tạo, feature và remote chỉ dùng hook. Nếu remote và shell không chia sẻ cùng instance @tanstack/react-query thì hai bên có hai cache riêng (xem phần shared ở mục 7). Bản này qua tsc --strict (đã chạy, sạch); chưa chạy với server thật.
Kiểm tra dependency rule#
Đặt lint rule hoặc dependency graph check để chặn:
sharedimportfeatures;- feature A import internal path của feature B;
- module UI gọi trực tiếp module route cấp cao;
- code trình duyệt import build config hoặc secret tooling;
- vòng phụ thuộc giữa các feature.
Khi codebase nhỏ, import discipline và vài module boundary đủ dùng. Thêm Nx/Turborepo/dep graph khi nhiều package hoặc team thật sự cần cache, affected builds hay enforce ownership — không thêm công cụ chỉ vì repo có vẻ lớn.
Cấu hình ESLint flat để máy chặn import sai tầng#
ESLint 10 chỉ đọc flat config (eslint.config.js). Plugin eslint-plugin-boundaries 7 gán mỗi file vào một "element" theo đường dẫn, rồi boundaries/dependencies quyết định element nào được import element nào. Cấu hình dưới đây mã hóa đúng cây app / features / shared ở mục 2: app chỉ vào feature qua index.ts, feature chỉ vào shared, trong chính nó, hoặc index.ts của feature khác, shared chỉ vào shared.
Cần thêm eslint-import-resolver-typescript cùng typescript-eslint vào devDependencies. Đã chạy (ESLint 10.12, plugin 7.2) trên một cây mẫu có hai lỗi cố ý: features/cart import ../catalog/components/ProductCard và shared/ui/Dialog import features/catalog. Kết quả quan sát: npx eslint . thoát với 2 lỗi boundaries/dependencies, mỗi lỗi nêu cặp element bị cấm (feature name="cart" tới feature name="catalog", và shared tới feature), còn import qua ../catalog (index) không bị báo. Trước khi thêm dòng import/resolver, cùng cây đó báo 0 lỗi dù có vi phạm: đó là lỗi cấu hình dễ gặp nhất, nên luôn cố tình viết một import sai để chắc rằng rule đang chạy. Chạy lệnh này như một bước CI bắt buộc: cách dựng pipeline ở GĐ15 mục 8, cách gắn kiểm tra vào CI ở GĐ13 mục 14.
4. Mở rộng ứng dụng: monolith, package hay Micro Frontend?#
Micro Frontend là một cách phân phối và sở hữu UI, không phải cấp độ trưởng thành bắt buộc sau React. Bắt đầu bằng modular frontend trong một SPA thường có ít chi phí nhất.
| Lựa chọn | Khi hợp | Trade-off chính |
|---|---|---|
| Modular monolith | Một team hoặc vài team phối hợp được; deploy chung vẫn đáp ứng nhu cầu | Build/release chung, cần giữ module boundary bằng code review/lint |
| Shared package trong monorepo | Muốn chia component/util, version code cùng release của app | Consumer thường vẫn build/deploy lại; chưa phải runtime independent deployment |
| Route-level applications + reverse proxy/server composition | Domain/route có owner/deploy riêng, composition ở navigation/server | Cross-app navigation, shared shell, cache/headers và observability cần vận hành |
| Iframe | Cần cách ly mạnh hoặc nhúng sản phẩm bên thứ ba | UX, sizing, focus, auth, communication và accessibility phức tạp |
| Web Components | Muốn trao đổi qua HTML/custom element contract hoặc phục vụ nhiều framework | Styling, event API, lifecycle và SSR/hydration phải thiết kế rõ |
| Runtime Module Federation | Muốn host nạp code từ build khác khi chạy, remote release độc lập | Runtime dependency graph, version drift, tải lỗi, security và debugging phức tạp |
Dấu hiệu đáng xem xét Micro Frontend#
- Nhiều team sở hữu domain/luồng người dùng riêng và thường bị nghẽn do phải release chung.
- Các ranh giới nghiệp vụ và UX đủ rõ để một team có thể chịu trách nhiệm từ UI đến API contract.
- Có nhu cầu nâng cấp dần hệ thống cũ hoặc công nghệ theo từng domain, không thể rewrite toàn bộ.
- Đã có pipeline, monitoring, rollback, versioned contract và người chịu trách nhiệm vận hành remote.
Dấu hiệu chưa nên chia#
- “Code nhiều file” nhưng một team vẫn làm việc hiệu quả trong repo hiện tại.
- Các module cùng sửa navigation/global state/design system mỗi sprint.
- Các team chỉ tách theo loại kỹ thuật (team CSS, team forms, team routing) thay vì domain.
- Chưa có quyền owner, compatibility test, observability hoặc rollback.
- Tách từng component nhỏ thành remote nhưng host phải chờ hàng chục request/remote mới render được trang.
MFE có thể làm team độc lập hơn nhưng không làm nghiệp vụ tự nhiên ít phụ thuộc hơn. Nếu hai remote cần đồng bộ deploy mỗi lần đổi contract, bạn có thể đã tạo distributed monolith cùng thêm network latency. Cùng bài toán ở phía backend được phân tích ở GĐ20 mục 3: modular monolith trước, tách sau khi có bằng chứng.
Monorepo bằng npm workspaces#
Bước trung gian rẻ nhất giữa một app và nhiều app là một repo, nhiều package cùng version control. Chỉ cần npm workspaces, chưa cần công cụ build:
Đã chạy (npm 11, thư mục tạm): npm install ở gốc tạo symlink node_modules/@acme/* trỏ vào packages/* và apps/*; npm ls --workspaces cho thấy @acme/catalog yêu cầu design-system@^1.2.0 được thỏa bằng đúng bản 1.3.0 trong repo (deduped), và import('@acme/catalog') chạy qua symlink. Điều đó chứng minh package dùng chung theo version cùng một lần build, không phải triển khai độc lập: đổi design-system vẫn phải build và deploy lại app. Thêm Nx hay Turborepo khi cần cache build hoặc "chỉ build phần bị ảnh hưởng"; đừng thêm trước khi đo được thời gian build là nút thắt.
Import map và custom element, phương án nhẹ hơn Federation#
Khi remote chỉ là một widget độc lập, trình duyệt đã đủ cơ chế: import map ánh xạ tên module sang URL có version, custom element là ranh giới component giữa các framework, Shadow DOM cô lập CSS, CustomEvent mang contract.
Đã chạy (Chrome, server tạm): bấm "Thêm vào giỏ" hai lần làm <output> của shell lên 2 nhờ sự kiện đi xuyên Shadow DOM (composed: true); đổi đường dẫn trong import map từ /catalog/v1/ sang /catalog/v2/ thì widget in catalog v2 mà không sửa mã shell. Giới hạn: không có thương lượng phiên bản dependency, không có type tự động, SSR và hydration phải tự lo (xem bảng ở đầu mục). Nếu nhu cầu dừng ở "nhúng một widget có version", đây là chi phí thấp hơn Federation; khi remote cần chia sẻ React singleton với host, quay lại mục 7.
5. Design System — nền tảng, component, governance#
Design System là hệ thống giúp đội ngũ tạo giao diện thống nhất và tiếp tục thay đổi an toàn. Nó thường gồm design tokens, component primitives, patterns, accessibility rules, tài liệu, ownership và cách phát hành. Một thư viện button đơn lẻ chưa phải Design System.
Lớp cấu thành#
- Design foundations: màu, typography, spacing, radius, elevation, motion, breakpoints, focus ring.
- Semantic tokens: ý nghĩa dùng ở UI (
text.primary,surface.canvas,action.primary) độc lập khỏi palette thô. - Primitives: Button, TextField, Checkbox, Dialog, Tooltip, Select, Table.
- Patterns: form validation, empty state, pagination, confirm destructive action, filter bar.
- Guidance: dùng ở đâu, accessibility behavior, responsive states, do/don't, migration.
- Governance: owner, contribution proposal, review, deprecation window, release notes và support.
Primitive tokens và semantic tokens#
Primitive token mô tả giá trị palette/scale; semantic token diễn tả vai trò. Component nên dùng semantic token để đổi theme/brand mà không thay từng component.
Ví dụ định dạng JSON của Design Tokens Community Group, theo Format Module 2025.10 (đây là Final Community Group Report của một W3C Community Group, không phải tiêu chuẩn W3C và không nằm trên W3C Standards Track):
Điểm khác với bản draft cũ (chuỗi "#174ea6", "1rem"): ở 2025.10, $value của color là object có colorSpace và components (với srgb, mỗi component là số từ 0 đến 1), alpha và hex là tùy chọn (không có alpha thì coi là 1). $value của dimension là { "value": số, "unit": "px" | "rem" }. Alias vẫn viết "{color.blue.600}". components ở trên là #174ea6 chia 255, làm tròn 4 chữ số; để hex làm fallback cho công cụ chưa hiểu colorSpace. Công cụ build token (ví dụ Style Dictionary 5 với usesDtcg: true) tự đổi object này ra #174ea6 và 1rem khi sinh CSS; chọn công cụ và version đang hỗ trợ đúng bản spec bạn dùng, vì nhiều tool vẫn đọc cả kiểu chuỗi cũ.
Spec 2025.10 chưa kèm JSON Schema hay validator chính thức (mục editor's note nói nhóm đang cân nhắc thêm), nên cách kiểm là đối chiếu với spec và cho token đi qua công cụ build thật.
Có thể build ra CSS custom properties:
Token names nên biểu đạt ý nghĩa, không khóa vào giá trị hiện tại. text-danger là semantic; red-600 là palette. Đừng tạo hàng trăm token trước khi có ví dụ UI và cách duy trì chúng.
Component API là contract#
Một component hệ thống cần có:
- API hẹp, có kiểu và không nhận mọi prop implementation của DOM một cách vô kiểm soát;
- trạng thái mặc định, loading, disabled, error, focus, hover, pressed, empty rõ;
- keyboard behavior và semantic HTML đúng;
- nội dung tùy chỉnh qua slot/children thay vì yêu cầu fork component;
- ví dụ và story cho các trạng thái quan trọng;
- policy về breaking changes, deprecation và phiên bản.
Ví dụ API:
Hai chi tiết dễ sai trong bản Button: đặt className sau {...buttonProps} mà không gộp sẽ làm mất className của consumer; thiếu type="button" thì nút trong <form> mặc định là submit và gửi form ngoài ý muốn. Bản trên đã qua tsc --strict (đã chạy, sạch).
loading không thay cho accessible label. Với action async, cần quyết định focus, thông báo kết quả qua live region và chống submit lặp. Với destructive action, “danger” chỉ là visual variant; nghiệp vụ xác nhận vẫn nằm ở consumer/pattern.
Publish và vận hành Design System#
- Có package source of truth, changelog, owner và release cadence.
- Dùng semver nếu consumer dựa vào version; breaking prop/behavior cần migration guide.
- Chạy typecheck, component test, visual review và accessibility audit trên các trạng thái đại diện.
- Tạo story/docs cho component mới; tài liệu cho người dùng component, không chỉ nội bộ tác giả.
- Có quy trình contribution: request → RFC ngắn → prototype/story → review design + engineering + accessibility → release.
- Theo dõi adoption và deprecated API; không để một component cũ vĩnh viễn vì không có chủ dọn.
- Đừng buộc mọi sản phẩm dùng chung mọi pattern nếu khác nhau thật; standardize primitive/contract, cho phép domain composition.
Storybook có thể giúp xem, tài liệu hóa và kiểm tra các trạng thái component; chọn công cụ tương ứng với stack hiện tại, không để docs chạy lệch khỏi production component.
Tự xây hay dùng headless primitive#
Dialog, Menu, Select, Combobox là loại component khó làm đúng bàn phím, focus trap, aria-* và đọc bằng screen reader. Quyết định theo ba câu hỏi: component có hành vi tương tác phức tạp không, đội có người đủ chuyên môn a11y để bảo trì không, và cần giao diện riêng đến đâu.
| Cách | Hợp khi | Chi phí |
|---|---|---|
HTML gốc (<button>, <dialog>, <select>) | Hành vi trình duyệt đã đủ | Ít tùy biến giao diện; kiểm hỗ trợ trình duyệt |
Headless primitive (Radix: gói @radix-ui/react-dialog; React Aria: gói react-aria-components) | Cần hành vi chuẩn, giao diện tự làm theo token | Thêm dependency, tuân theo API và lịch phát hành của họ |
| Tự viết toàn bộ | Hành vi rất khác thường, hoặc không dùng được thư viện | Tự chịu mọi lỗi a11y và regression |
Hai gói trên có trên npm (đã kiểm khi soạn); phần API chi tiết và trạng thái hỗ trợ React 19 chưa dựng thử, hãy đọc tài liệu đúng bản bạn cài. Dù chọn cách nào, test a11y vẫn thuộc về design system: axe bắt được một nhóm lỗi tĩnh, không thay thế pass bàn phím bằng tay.
Đã chạy (Playwright 1.63 với Chrome thật): axe báo button-name cho nút chỉ có icon, và hết lỗi sau khi thêm aria-label="Đóng". Fixture đầu tiên còn bị báo page-has-heading-one vì thiếu <h1>: axe đánh giá cả trang, nên fixture phải là một trang hợp lệ.
Phát hành bằng Changesets và chặn regression giao diện#
Changesets ghi ý định phát hành thành file nhỏ nằm trong PR, rồi gom lại thành version và changelog. Cấu hình tối thiểu:
Quy trình: PR đổi design system chạy npx changeset (chọn package, chọn patch/minor/major, viết một câu mô tả); khi phát hành chạy npx changeset version để tăng version, cập nhật CHANGELOG.md và bump dependency nội bộ, rồi npx changeset publish. Đã chạy changeset version trên workspace mẫu: một changeset minor cho @acme/design-system đưa nó từ 1.2.0 lên 1.3.0, sinh dòng changelog "Button: thêm prop loading ..." và cập nhật ^1.3.0 trong apps/shell. Chưa chạy publish (cần registry).
Visual regression bắt thay đổi giao diện mà test logic không thấy. Với Playwright:
Đã chạy: lần đầu cần npx playwright test --update-snapshots để tạo ảnh chuẩn (không có ảnh chuẩn thì test fail); các lần sau pass; đổi màu nút từ #174ea6 sang #b3261e làm test fail với báo cáo số pixel khác. Ảnh chuẩn phụ thuộc font và anti-aliasing của máy, nên chạy visual test trong cùng môi trường CI (cùng image, cùng trình duyệt) và duyệt ảnh mới như duyệt code.
6. Micro Frontend: ownership, composition, contract#
Micro Frontend là cách ghép nhiều frontend app có thể phát triển/phát hành độc lập thành một trải nghiệm thống nhất. Ranh giới tốt thường là vertical slice của domain như Catalog, Billing, Account; tránh chia ngang theo Button, CSS hay form.
Application shell sở hữu phần giao nhau#
Shell thường giữ:
- URL/navigation và route table toàn ứng dụng;
- global chrome như header/footer và layout;
- đăng nhập/đăng xuất, lấy identity và cách truyền session an toàn;
- design-system entrypoint và theme;
- error/loading boundary, telemetry, feature flag và fallback khi remote hỏng.
Remote sở hữu:
- route/feature/domain UI của mình;
- query, local state và adapter tới API domain;
- tests, pipeline, artifact/version và on-call/owner;
- semantic events cần thông báo cho shell hoặc feature khác.
Shell không nên đọc internal store của remote; remote không nên sửa shell navigation bằng global variable. Trao đổi qua contract có version, ví dụ route, typed props ở ranh giới, custom event hoặc API server.
Ví dụ event contract:
Trong app thật, bọc event bằng adapter có schema/runtime validation và contract test. Tên event tự do không thay thế versioning hay documentation.
Authentication và dữ liệu#
- Shell xác thực user; remote chỉ nhận thông tin cần thiết hoặc gọi API theo cơ chế được thống nhất.
- Không phát tán access token qua
window.__GLOBAL_AUTH__nếu có lựa chọn an toàn hơn như cùng-origin secure session/API gateway. - Backend vẫn phải authorize mọi request; frontend remote không phải security boundary.
- Xác định CORS, cookie
SameSite, credential behavior, CSP, logout/revocation và API error contract. - Không giữ hai bản global session state có thể lệch nhau; chọn owner và cách refresh.
Navigation và CSS isolation#
Chốt một bên sở hữu history/router, deep link, 404, browser back/forward và route collision. CSS nên có scope/prefix hoặc Shadow DOM nơi phù hợp; tránh global button, h1 selector đè app khác. Reset, font loading, z-index/modal layer và focus management cần contract chung.
Failure containment#
- Nếu remote load lỗi: fallback có thông báo và nút retry; shell/navigation phần khác vẫn dùng được.
- Timeout remote API/load không được treo toàn app; lazy-load route khi người dùng đến đó.
- Theo dõi remote version, host version, browser, route, request ID và load error.
- Có kill switch/roll back URL về artifact trước; deploy remote độc lập không có nghĩa là không cần tương thích ngược.
7. Module Federation — dynamic module sharing#
Module Federation là một cách để nhiều build độc lập expose và consume module ở runtime. Trong Webpack, mỗi build vừa có thể đóng vai host/container vừa đóng vai remote/container. Đây là một kỹ thuật triển khai Micro Frontend, không phải định nghĩa của Micro Frontend; MFE cũng có thể dùng server-side composition hoặc route-level deploy mà không dùng Federation.
Thuật ngữ#
- Host: app khởi tạo trang, router/shell và nạp remote.
- Remote: build expose module (route/page/widget) cho host.
- Remote entry: metadata/runtime entry dùng để tải exposed module và chunk liên quan.
exposes: module remote cho consumer dùng.remotes: container mà host biết cách tải.shared: dependency có thể dùng chung trong share scope thay vì mỗi build đóng gói riêng.
Ví dụ Webpack Module Federation, rút gọn. Cấu hình thực tế khác nhau giữa Webpack, Rspack, Rsbuild, Vite plugin và framework; dùng integration đúng bundler/version của repo.
Phân biệt hai thứ dễ bị gộp: Module Federation 2.0 là tên sản phẩm/bộ tính năng (manifest mf-manifest.json, runtime init/loadRemote/registerRemotes/loadShare, hỗ trợ nhiều bundler), còn @module-federation/enhanced là gói npm, hiện ở dòng phiên bản 2.x (2.0.0 phát hành trên npm ngày 2026-02-06). Tên "MF 2.0" đã dùng từ khi gói còn ở 0.x, nên đừng suy từ tên sản phẩm ra số phiên bản gói. Ví dụ dưới dùng ModuleFederationPlugin của webpack 5 cho dễ đọc; với @module-federation/enhanced bạn import plugin từ gói đó (ví dụ @module-federation/enhanced/webpack) và các option name/exposes/remotes/shared giữ nghĩa tương tự. Cách import, hỗ trợ manifest và type hint khác nhau theo bundler và version: đọc tài liệu module-federation.io đúng bản bạn cài, không chép nguyên ví dụ này.
Trong webpack.config.js, dependencies ở các ví dụ dưới lấy từ package.json của chính app: const { dependencies } = require('./package.json') (CommonJS) hoặc import pkg from './package.json' with { type: 'json' } rồi dùng pkg.dependencies (ESM); requiredVersion nhờ đó luôn khớp phiên bản đã cài.
Remote catalog:
Host shell:
Host dùng route-level lazy load:
Webpack TypeScript consumer thường cần declaration cho module remote hoặc types plugin/manifest:
Shared dependency là một policy, không phải “cài một lần miễn phí”#
singleton: true hợp với thư viện cần một instance/runtime như React context; nhưng khi host và remote mong major version khác nhau, có thể gây runtime error hoặc fallback ngoài dự kiến. Shared dependency giảm duplication khi cùng version tương thích, nhưng thêm version negotiation và coupling release. Chia sẻ càng nhiều, ranh giới dependency càng chặt.
Chỉ share thứ đã được đo và cần singleton/giảm transfer. Để dependency khác bundled nếu rủi ro sharing lớn hơn bytes tiết kiệm. Ghi rõ:
- ai cung cấp version chuẩn;
requiredVersion/ fallback behavior;- singleton có bắt buộc không;
- quy trình nâng React/design-system;
- compatibility matrix host × remote;
- remote có được deploy mà không release host không.
Artifact, cache và rollback#
- Tên runtime build phải duy nhất (
output.uniqueNametrong Webpack); tránh collision giữa host/remotes. - Phát hành manifest/remote entry theo version bất biến hoặc có strategy cập nhật minh bạch.
- Upload chunk assets trước khi trỏ remote entry tới version mới để tránh manifest trỏ file chưa tồn tại.
- Giữ artifact trước đó và rollback bằng config/manifest thay vì rebuild lại “phiên bản gần giống”.
- Host phải xử lý timeout/404/CSP/CORS/runtime mismatch và remote load ở browser cũ.
- Không để remote URL từ user-controlled input; chỉ nạp artifact từ origin được allowlist, áp CSP phù hợp và review supply chain.
- Đưa việc tải remote vào performance budget: remote entry, chunk count, shared fallback, duplicate library, cache hit và route first render.
Entry bất đồng bộ để sửa lỗi eager consumption#
Lỗi Shared module is not available for eager consumption xảy ra khi file entry import react ngay, trước khi runtime Federation kịp thương lượng share scope. Cách sửa là để entry chỉ làm một việc: nạp bất đồng bộ file bootstrap chứa mã thật.
Đã chạy (webpack 5.111, React 19.3, Chrome): cùng cấu hình ModuleFederationPlugin với shared: { react: { singleton: true, ... } }, entry đồng bộ (import React ... ngay trong index.js) cho trang trắng và pageerror Shared module is not available for eager consumption; đổi entry thành import('./bootstrap') thì trang render hello 19.3.0, không lỗi. Bật eager: true cũng làm hết lỗi nhưng đóng gói React vào chunk khởi động và bỏ thương lượng: chỉ dùng khi hiểu rõ lý do. Với plugin Vite hay Rspack, cách tránh lỗi này khác webpack (chưa kiểm): đọc tài liệu đúng bundler.
Module Federation 2.0 ở runtime: manifest, loadRemote, con trỏ phát hành#
Với @module-federation/vite (và các plugin Module Federation khác), manifest: true sinh mf-manifest.json cạnh remoteEntry.js. Host có thể khai báo remote bằng URL của manifest và nạp bằng API runtime thay vì import('catalog/...') tĩnh:
Quy tắc header cho hai loại file (đúng cho mọi remote, không riêng Vite):
| File | Ví dụ URL | Cache-Control | Lý do |
|---|---|---|---|
| Artifact có version | /v1/mf-manifest.json, /v1/remoteEntry.js, /v1/assets/*.js | public, max-age=31536000, immutable | Đường dẫn chứa version nên nội dung không bao giờ đổi |
| Con trỏ phát hành | /remotes.json | no-cache | Phải đọc lại mỗi lần để thấy bản mới và rollback có hiệu lực |
Remote nằm khác origin với host thì server của remote cần Access-Control-Allow-Origin hợp lệ cho origin host (lab dùng * vì dữ liệu công khai); thêm origin đó vào CSP script-src và connect-src của host.
Đã chạy (@module-federation/runtime 2.9.2, @module-federation/vite 1.23, Chrome, server tạm hai cổng): (1) con trỏ ở v1, host in catalog v1, header của file v1/* đúng như bảng và remotes.json là no-cache; (2) đổi con trỏ sang v2 rồi gọi registerRemotes(..., { force: true }) và loadRemote lại, cùng trang, hiện catalog v2; (3) tải lại trang thì vẫn v2; (4) đưa con trỏ về v1, tải lại, hiện catalog v1: rollback chỉ là đổi con trỏ, không build lại; (5) con trỏ trỏ vào v9 (404): hook errorLoadRemote được gọi ba lần (afterResolve hai lần, onLoad một lần) và host hiện fallback "Catalog tạm thời không dùng được" thay vì trang trắng. Lưu ý: API runtime, tên hook và message lỗi thay đổi theo bản; những gì ghi ở đây đúng với 2.9.2.
Rủi ro dễ gặp#
| Triệu chứng | Nguyên nhân hay gặp | Hướng điều tra |
|---|---|---|
remoteEntry.js 404 | URL/environment/publicPath sai hoặc deploy chưa upload entry | network tab, remote URL và CDN cache |
Shared module is not available for eager consumption | Entry thực thi trước async sharing boundary | federation bootstrap theo bundler/version; không bật eager đại trà |
| React invalid hook call / context không thấy | Có hai React instance hoặc version scope mismatch | bundle/share scope, dependency tree, singleton + requiredVersion |
| Host chạy local, production lỗi chunk | public path/CDN prefix/runtime artifact thiếu | kiểm tra chunk URL, deploy ordering, cache headers |
| Deploy remote làm shell vỡ | Contract không tương thích hoặc không có fallback | host-remote contract matrix, backward compatibility, rollback |
Tên lỗi và chi tiết config phụ thuộc phiên bản plugin. Tra troubleshooting của bundler/plugin đang dùng, không copy config từ một tutorial cũ vào mọi stack.
8. Lộ trình chuyển đổi thực tế#
Đừng bắt đầu bằng việc tách một app đang chạy thành năm repository. Chuyển từng bước để luôn có đường quay lại.
Giai đoạn A — làm monolith modular#
- Gắn ownership rõ cho route/features.
- Di chuyển logic vào feature modules với public API và dependency check.
- Đo build time, conflict rate, cycle count, release cadence, change failure và phần trùng code.
- Tách Design System package trước nếu nhiều app/team thực sự dùng chung UI.
Giai đoạn B — xác nhận nguyên nhân cần tách#
Viết decision record trả lời: vấn đề hiện tại là build chậm, release coordination, ownership, legacy stack hay runtime UX? Micro Frontend chỉ giải quyết một số nguyên nhân. Nếu vấn đề là import boundary thì ESLint/monorepo modules rẻ hơn runtime federation.
Giai đoạn C — tách một domain ít blast radius#
- Chọn một route/vertical slice có contract backend độc lập và metrics rõ.
- Định nghĩa route, props/events, auth, design tokens, API/error behavior và loading/error fallback.
- Dựng remote song song nhưng route mặc định còn phục vụ implementation cũ.
- Chạy compatibility tests trên host hiện tại, remote mới và tổ hợp version cần hỗ trợ.
- Canary một phần traffic, theo dõi page error, route completion, bundle/latency và rollback về app cũ nếu vượt ngưỡng.
- Sau vài lần release độc lập an toàn mới đánh giá có tách remote kế tiếp không.
9. Lab — storefront có shell, design system và một remote#
Phạm vi sản phẩm#
Một storefront có ba route: /catalog, /cart, /account. Bắt đầu trong một SPA modular. Sau khi boundary và owner rõ, chỉ tách catalog thành remote. Shell tiếp tục quản lý navigation/session; cart/account ở local để có comparison và fallback thật.
Việc cần làm#
-
Architecture baseline: vẽ dependency graph; ghi route owner, data owner và public API cho ba feature. Thêm lint test chặn import cross-feature nội bộ.
Lời giải và cách kiểm tra
Sơ đồ.
textReadyHướng làm: mỗi feature một thư mục có
index.tslà public API; ghi bảngroute | data owner | public APIcho/catalog,/cart,/account. Lint đơn giản nhất là script quét import (có thể thay bằng ruleno-restricted-importshoặceslint-plugin-boundariesnếu repo đã dùng ESLint). Code tham chiếu (đã chạy):typescriptReadyKết quả quan sát:
node scripts/check-boundaries.mjsinok: ...(exit 0); thêmimport x from '../../catalog/src/CatalogRoutes'vàoshell/thì in file:dòng và exit 1 (nhánh lỗi: suy ra từ code, chưa chạy). Lỗi hay gặp: chỉ vẽ sơ đồ mà không có check tự động nên ranh giới mục sau vài tháng. -
Design System: tạo token primitive + semantic; Button, TextField, Dialog, PageSkeleton, EmptyState. Ghi variant, keyboard behavior, disabled/loading/error và story cho ít nhất ba trạng thái mỗi component.
Lời giải và cách kiểm tra
Hướng làm: primitive trước (
color.blue.600), semantic sau (color.action.primary.background); component chỉ đọc semantic. Dùng đúng dạng DTCG 2025.10 đã trình bày ở mục 5:$valuecolor là object, dimension là{ value, unit }. Mỗi component ghi bảng variant, bàn phím, disabled/loading/error. Button tối thiểu:tsxReady(Đã chạy bản gần giống, dùng token CSS viết tay; chưa chạy Style Dictionary hay Storybook.) Story tối thiểu cho ba trạng thái:
Default,Disabled,Loading(chưa chạy, tuỳ công cụ docs repo chọn). Lỗi hay gặp: component đọc primitive trực tiếp nên đổi dark theme phải sửa từng nơi;loadingmà quêndisablednên bấm đúp gửi hai lần. -
Accessibility: tab/shift-tab, focus visible, Dialog Escape/restore focus, accessible name, color contrast, zoom 200%. Không coi axe check là thay thế manual keyboard/screen-reader pass.
Lời giải và cách kiểm tra
Hướng làm: checklist thủ công cho từng component: Tab/Shift+Tab đi đúng thứ tự, focus ring thấy rõ, Dialog đóng bằng Escape và trả focus về nút mở (dùng
<dialog>+showModal()hoặc một thư viện đã kiểm chứng thay vì tự viết), có accessible name, tương phản chữ thường tối thiểu 4,5:1 theo WCAG 2.2, zoom 200% không mất nội dung. Chưa chạy: axe và pass screen reader. Kết quả mong đợi: axe không báo lỗi nghiêm trọng, nhưng không đủ để đạt; phải có pass bàn phím bằng tay. Lỗi hay gặp:outline: nonekhông thay thế; focus bị kẹt trong Dialog khi remote lỗi. -
Federation: deploy
catalogremote, expose route module, host nạp lazy. GắnRemoteErrorBoundary, skeleton và retry. Không expose internal API rộng hơn cần thiết.Lời giải và cách kiểm tra
Sơ đồ.
textReadyHướng làm: remote expose một module route, shell nạp lazy, bọc
RemoteErrorBoundaryvàSuspense. Chỉ expose./Routes, không expose store hay API client. Code tham chiếu (đã chạy với@module-federation/vite1.23; Webpack: xem mục 7):typescriptReadytsxReadytsxReadyKhai báo kiểu cho
catalog/Routesbằngdeclare modulenhư mục 7 (đã chạy:tscsạch). Kết quả quan sát (Chrome headless,vite previewhai cổng): vào/cartshell chỉ gọi remote 1 request (tải remote entry lúc khởi tạo, chưa tải chunk catalog); vào/catalogtổng số request tới remote tăng lên 10 (entry, chunk route, react và react-dom đã thương lượng); bấm "Thêm vào giỏ" hai lần thì shell hiệnGiỏ: 2qua event. Lỗi hay gặp, đã gặp thật: (a) tắt remote rồi bấm "Thử lại" bằng cách tạo lạilazy()không hồi phục trong cùng trang, dù remote đã sống lại; tải lại trang thì được. Nghi do module map của trình duyệt (và runtime) nhớ URL import đã lỗi: đây là suy luận, chưa kiểm cách khác như đổi URL entry kèm query. Vì vậy nút Thử lại trongRemoteErrorBoundarygọilocation.reload()và route nên nằm trong URL để không mất chỗ. (b) Host thấyShared module is not available for eager consumption: xem bảng mục 7, đặt entry bất đồng bộ. -
Contract: viết typed contract cho route/events; test trường hợp remote mới với host trước đó và host mới với remote đang production.
Lời giải và cách kiểm tra
Hướng làm: gom tên event, kiểu và hàm parse vào một file dùng chung; shell chỉ tin dữ liệu sau khi parse. Test tối thiểu cho "remote mới + host cũ" và ngược lại là test parser với payload của từng phiên bản đang sống ở production. Code tham chiếu (đã chạy với Vitest 5):
typescriptReadyKết quả quan sát:
npx vitest runthấy1 passed. Thay đổi phá vỡ (đổi tên field) phải racart.updated.v2và shell nghe cả hai trong thời gian chuyển tiếp. Lỗi hay gặp:asép kiểu thay vì parse nên remote lệch kiểu làm shell crash. -
Release: artifact immutable, health/availability check, staged rollout, rollback về remote cũ; mô phỏng remote URL 404 và React version conflict.
Lời giải và cách kiểm tra
Hướng làm: thứ tự phát hành là upload chunk bất biến lên đường dẫn có version, chạy health check (
GET remoteEntry.jstrả 200 và đúng content type), rồi mới đổi con trỏ entry. Rollback là đổi con trỏ về version trước, không build lại. Entry nên đọc từ cấu hình lúc chạy (hoặc manifest do shell tải) để rollback không cần build lại shell; bản lab ghim entry lúc build nên chỉ minh hoạ. Mô phỏng 404 / remote chết (đã chạy): dừngvite previewcủa catalog, tải lại shell, vào/catalog. Kết quả quan sát: thấyCatalog tạm thời không dùng được., console cóFederation Runtime ... #RUNTIME-008(Failed to load script resources), điều hướng sang Cart vẫn hoạt động. Chưa chạy: xung đột phiên bản React (cách mô phỏng: build remote vớirequiredVersion: '^18'trong khi shell cung cấp 19, mong đợi cảnh báo hoặc lỗi version negotiation tuỳ cấu hìnhstrictVersion; kết quả chính xác chưa kiểm), staged rollout, canary. Lỗi hay gặp: đổi con trỏ entry trước khi chunk upload xong; cache CDN dài trênremoteEntry.jsnên rollback không có hiệu lực. -
Measure: trước/sau đo app shell bytes, catalog route bytes, remote entry/chunk requests, route load/error và time-to-interactive.
Lời giải và cách kiểm tra
Hướng làm: bảng trước/sau cùng build mode, cùng cache state: bytes shell, bytes route catalog, số request tới remote, tỉ lệ lỗi tải route, time-to-interactive. Công cụ: tab Network +
vite buildbáo cáo kích thước +web-vitalsở GĐ27 mục 3. Chưa đo: chưa có bảng số cho cấu hình monolith so với federation; quan sát có căn cứ duy nhất là số request ở bước 4. Kết quả mong đợi (suy luận): federation tăng số request và có một lần tải remote entry, đổi lại release độc lập; nếu không đo được lợi ích đó thì quay lại monolith modular (mục 8).
Khung và mã dùng chung
Tự làm trước, rồi mới mở. Đã chạy (máy tạm, không dùng Docker): Node 24.21, Vite 8.3, @module-federation/vite 1.23, React 19.3, Vitest 5.0, TypeScript 7.0 (tsc sạch), Chrome headless điều khiển bằng playwright-core. Phần chưa chạy được ghi rõ ở từng bước: mô phỏng xung đột phiên bản React, canary/staged rollout, axe và pass screen reader, Storybook, đo bytes bằng số liệu thật. Lab dùng React + Vite cho gọn; Webpack/Rspack khác ở cách import plugin và cấu hình (mục 7), tư duy giữ nguyên.
Cấu trúc thư mục của lab (Vite, hai ứng dụng độc lập dùng chung ds/ và contracts/):
Mỗi bước bên trên tham chiếu các file này.
Definition of done#
- Có sơ đồ module/dependency và decision record giải thích vì sao boundary theo domain này hợp lý.
- Feature public API không lộ internal components/state; dependency rule được kiểm tra tự động.
- Design tokens có semantic aliases; component docs mô tả states, a11y và ví dụ.
- Host vẫn điều hướng được nếu remote lỗi; remote có owner, version, pipeline và rollback artifact.
- Nêu rõ chi phí federation: thêm network hop/chunks/runtime contracts, shared dependency risk và ops.
- Có bằng chứng remote tải độc lập thật; nếu phải deploy host mỗi khi đổi remote thì chưa đạt mục tiêu independent deployment.
10. Tự kiểm tra#
-
Giải thích cách ESLint chặn
features/cartimport file nội bộ củafeatures/catalog, và cách chắc rằng rule thật sự đang chạyĐáp án
Mỗi file được gán một element theo
boundaries/elements(featurekèmcaptured.name);boundaries/dependenciesvớidefault: 'disallow'chỉ cho các cặp có policy: cùng feature, hoặcindex.tscủa feature khác, hoặcshared. Import../catalog/components/ProductCardkhông khớp policy nào nên báo lỗi. Để chắc rule chạy, cố tình viết một import sai và xemnpx eslint .có báo không; nếu im lặng, thường là thiếuimport/resolvercho TypeScript. Sai thường gặp: chỉ kiểm cây sạch và tin rằng 0 lỗi là đúng. Xem mục 3 của GĐ này và GĐ15 mục 8 để đưa lệnh vào CI. -
Vì sao chuyển entry thành
import('./bootstrap')sửa được lỗiShared module is not available for eager consumption?Đáp án
Entry đồng bộ import
reacttrước khi runtime Federation khởi tạo share scope, nên module share chưa có.import('./bootstrap')tạo một ranh giới bất đồng bộ: runtime thương lượng share scope trước, rồi mới chạy mã ứng dụng. Bậteager: truecũng làm hết lỗi nhưng đóng gói dependency vào chunk khởi động và mất thương lượng phiên bản. Sai thường gặp: bậteagerđại trà để "hết lỗi". Xem mục 7. -
Remote
v2hỏng sau khi phát hành. Nêu các bước rollback và header cache cần đặt cho từng loại fileĐáp án
Artifact nằm dưới
/v1/,/v2/vớiCache-Control: public, max-age=31536000, immutable; con trỏremotes.jsonlàno-cache. Rollback là đổi con trỏ vềv1/mf-manifest.json, không build lại; người dùng đang mở trang cần tải lại hoặc host gọiregisterRemotes(..., { force: true }). NếuremoteEntry.jsở URL cố định mà cache dài thì rollback không có hiệu lực. Sai thường gặp: đổi con trỏ trước khi chunk của bản mới upload xong. Cách làm CDN ở GĐ17 mục 4. -
Khi nào chọn import map cộng custom element thay cho Module Federation?
Đáp án
Khi remote là widget tự chứa, chỉ cần version URL và contract
CustomEvent, không cần chia sẻ React singleton với host và chấp nhận không có type tự động hay thương lượng phiên bản. Dùng Federation khi remote cần chia sẻ dependency runtime (React, design system) với host và có contract version rõ. Sai thường gặp: dùng Federation chỉ để nhúng một widget. Xem mục 4. -
Monorepo với npm workspaces có cho phép deploy độc lập không? Chỉ ra bằng chứng
Đáp án
Không.
npm ls --workspacescho thấy@acme/catalogdùng đúng bảndesign-systemtrong repo (symlink), nên đổi design system vẫn phải build và deploy lại app dùng nó. Monorepo cho chia sẻ code và refactor an toàn cùng một commit; deploy độc lập cần artifact và con trỏ phát hành riêng như ở mục 7. Cùng nguyên tắc "modular monolith trước" của backend ở GĐ20 mục 3. -
Vì sao Button trong design system phải gộp
classNamecủa consumer và đặttype="button"mặc định?Đáp án
Đặt
classNamesau{...buttonProps}mà không gộp sẽ ghi đè class của consumer, làm họ không tùy biến được. Nút trong<form>không cótypemặc định làsubmit, bấm sẽ gửi form ngoài ý muốn. Lỗi này thuộc loại test visual và hành vi nên có trong bộ kiểm của design system (Playwright, mục 5). Sai thường gặp: chỉ kiểm giao diện, không kiểm trong ngữ cảnh form. -
Đo tác động của việc tải remote lên hiệu năng trang thế nào?
Đáp án
Đo LCP, INP và số request tới remote ở mỗi route trước và sau khi tách, cùng cache state, bằng Core Web Vitals trong RUM và Lighthouse CI; so sánh theo phiên bản app. Tách mà không đo lợi ích thì nên quay lại modular monolith. Cách thu và đọc số liệu ở GĐ27 mục 3. Sai thường gặp: chỉ đo lab một lần trên máy mạnh.
Tài liệu tham khảo#
- Micro Frontends — Cam Jackson / Martin Fowler
- Webpack Module Federation concepts · Module Federation Plugin
- Module Federation quick start và integration theo bundler
- Design Tokens Format Module 2025.10 · Storybook documentation
- eslint-plugin-boundaries · ESLint flat config · TanStack Query
- Changesets · Playwright visual comparisons