Module Federation
Hiểu host, remote và runtime sharing; chọn public contract, shared dependencies và chính sách tương thích để các frontend có thể release riêng.
Giả sử catalog vừa thêm bộ lọc sản phẩm. Bạn muốn deploy catalog mà không build lại shell và cart, nhưng vẫn hiển thị catalog bên trong cùng ứng dụng. Nếu shell chỉ import catalog từ một npm package, code catalog thường đã được chốt vào bản build của shell; cập nhật package chưa làm người dùng nhận được tính năng mới.
Module Federation cho phép một build lấy module từ một build độc lập trong lúc ứng dụng chạy. Shell có thể tải public module của catalog, còn catalog giữ pipeline và artifact riêng. Nhưng việc tải được module chỉ là bước đầu: shell cũ có hiểu API của catalog mới không, React nào sẽ được dùng, và UI còn điều hướng được khi remote lỗi?
Module Federation giải quyết việc cung cấp, tải và chia sẻ module giữa các build; nó không tự tạo một kiến trúc micro-frontend độc lập.
Phạm vi nhóm bài
Đây là trang định hướng cho sáu bài chuyên sâu về Module Federation. Ví dụ dùng shop giả định gồm shell, catalog và cart, không phải ứng dụng có sẵn trong repository docs. Phần cấu hình minh họa dùng plugin tích hợp trong webpack 5; không phải cấu hình cho Next.js của site này.
Mục lục
- Module Federation nằm ở đâu trong kiến trúc
- Các khái niệm cần nắm
- Từ một lệnh import tới module của remote
- Đọc một cấu hình webpack tối thiểu
- Shared dependencies là một hợp đồng riêng
- Phân biệt webpack gốc và hệ sinh thái mở rộng
- Release riêng cần gì ngoài federation
- Lộ trình đọc trong nhóm
- Bài tập kiểm chứng boundary đầu tiên
- Checklist trước khi nối thêm remote
- Đọc tiếp
- Tài liệu tham khảo
Module Federation nằm ở đâu trong kiến trúc
Micro-frontend là cách chia frontend thành những phần có ownership và ranh giới tích hợp rõ. Runtime composition là cách ghép các artifact riêng khi ứng dụng chạy. Module Federation là một cơ chế có thể dùng để thực hiện việc ghép đó thông qua JavaScript modules.
Trong shop, shell quyết định catalog xuất hiện dưới /catalog/*. Federation runtime giúp shell lấy module công khai của catalog; shell vẫn phải quyết định khi nào render, loading UI nằm ở đâu, ai sở hữu navigation và lỗi được cô lập thế nào. Loader không thay thế application shell hay lifecycle contract.
Module được expose cũng không bắt buộc là một màn hình. Nó có thể là component, function, adapter hoặc API mount. Tuy vậy, khả năng expose mọi module không phải lý do để biến mọi helper nội bộ thành public API. Mình mặc định chọn boundary đủ lớn, như một nhánh route của domain, thay vì federate từng nút bấm.
Khác package và code splitting
| Cách lấy code catalog | Code được chốt ở đâu | Khi catalog đổi nội bộ |
|---|---|---|
| Import npm package | Trong dependency graph của bản build shell | Shell thường phải cập nhật dependency rồi build và release lại |
import('./CatalogPage') trong cùng app | Trong artifact graph của bản build chung | Có thể tải muộn, nhưng vẫn thuộc bản release của app |
| Import federated module | Host build biết public request; code remote được lấy lúc chạy | Remote có thể release riêng nếu URL resolution, contract và dependencies vẫn tương thích |
Dynamic import trong một app giúp code splitting; nó không tự tạo đơn vị deploy độc lập. Ngược lại, một federated remote vẫn có thể dùng code splitting bên trong nó. “Tải muộn” và “build riêng” là hai thuộc tính khác nhau.
Khi nào nên chọn và khi nào không
Mình chọn Module Federation khi nhiều team sở hữu các domain rõ ràng, cần release theo nhịp riêng và muốn ghép module trong cùng trải nghiệm frontend. Team cũng cần chấp nhận vận hành compatibility, artifact hosting, fallback và telemetry tại boundary.
Nếu nhu cầu chỉ là chia sẻ design system hoặc helper có cùng vòng release với ứng dụng, npm package thường đơn giản hơn. Nếu mọi phần đều phải deploy cùng nhau vì cùng sửa một store nội bộ, thêm federation chưa giải quyết được coupling đó.
Nếu cần cách ly code không tin cậy, Module Federation không phải lựa chọn sandbox. Nếu mỗi domain có thể phục vụ document riêng, navigation bằng link có thể đủ. Với yêu cầu isolation hoặc framework khác nhau, hãy so sánh thêm iframe, Web Components và lifecycle API thay vì mặc định dùng React component chung cây.
Các khái niệm cần nắm
| Khái niệm | Ý nghĩa | Ví dụ trong shop |
|---|---|---|
| Host hoặc consumer | Build tiêu thụ module từ build khác | Shell import catalog/ProductPage |
| Remote hoặc provider | Build công bố module cho consumer | Catalog expose ./ProductPage |
| Container | Giao diện runtime để khởi tạo sharing và lấy exposed module | Container catalog trong mô hình webpack script remote |
| Remote entry | Điểm vào giúp runtime tiếp cận container | remoteEntry.js của catalog |
exposes | Ánh xạ tên public module tới module nội bộ | ./ProductPage → ./src/ProductPage.jsx |
remotes | Khai báo remote mà host có thể tiêu thụ | Alias catalog → container và URL entry |
shared | Chính sách cung cấp và tiêu thụ dependencies chung | React với version policy rõ ràng |
| Share scope | Namespace runtime chứa các shared module và version được cung cấp | Scope default của các build tham gia |
| Public contract | Thỏa thuận về exports, input, hành vi và lifecycle | Props của ProductPage, navigation callback và semantics lỗi |
Host và remote là vai trò, không phải hai loại ứng dụng loại trừ nhau. Một build có thể vừa expose module, vừa consume remote khác. Nhưng remote tải remote khác cũng tạo thêm dependency và failure path; đừng thêm chuỗi phụ thuộc chỉ vì plugin cho phép.
Share scope không phải một global store nghiệp vụ. Nó hỗ trợ resolution của shared modules; quyền đọc và ghi cart vẫn cần domain owner cùng API riêng.
Từ một lệnh import tới module của remote
Luồng tải trong shop
Với webpack script remote, có thể hình dung đường đi như sau:
Một lần người dùng mở catalog thường có các mốc sau:
- Shell nhận route và bắt đầu tải public module, đồng thời giữ navigation cùng loading UI.
- Runtime dùng khai báo remote để tải container entry nếu chưa có.
- Runtime khởi tạo sharing giữa các build. Với container API của webpack,
initnhận share scope;get('./ProductPage')lấy factory của exposed module. - Runtime tải những chunk cần thiết, lấy factory và đánh giá module để trả exports cho consumer.
- Shell dùng public export để render component hoặc gọi API
mount; remote tiếp tục xử lý dữ liệu và UI của domain.
Đây là luồng khái niệm, không phải cam kết về từng network request. Cache, preload và cách build chia chunk có thể thay đổi việc tải. Khi dùng remotes thông thường, webpack sinh phần plumbing này; bạn không cần tự chèn script hay gọi container.get trong component.
Tải code thành công chưa có nghĩa trang đã usable. Catalog còn có thể cần API sản phẩm, CSS hoặc font. Vì vậy nên phân biệt mốc “module loaded”, “UI mounted” và “nội dung sẵn sàng dùng” khi đo trải nghiệm.
Remote entry không phải toàn bộ ứng dụng
remoteEntry.js thường là điểm vào container, không phải một file chứa sẵn mọi màn hình, dependency và asset. Entry có thể trả 200, nhưng exposed module vẫn lỗi vì một chunk bị 404 hoặc base URL khiến chunk bị tìm nhầm ở origin của shell.
Vì vậy deploy phải giữ nhất quán cả artifact graph: entry, JavaScript chunks, CSS và assets liên quan. Trong webpack, output.publicPath cần giúp remote tìm đúng assets của nó; output.uniqueName cũng cần khác giữa các build cùng xuất hiện trên trang để tránh runtime collision.
Đừng coi mọi entry là native ESM
Một remoteEntry.js dạng script container không mặc nhiên export React component hay hàm mount qua native import(url). Loader phải khớp protocol và format của artifact. Trong mô hình container của webpack, public module được lấy qua federation runtime, không bằng cách đoán exports từ tên file entry.
Đọc một cấu hình webpack tối thiểu
Các đoạn sau chỉ minh họa phần plugin, không phải hai project chạy hoàn chỉnh. Giả sử host và remote đã có entry, JSX tooling và một component ProductPage default export. Với component React được render bởi host, cần thêm chính sách shared phù hợp trước khi tích hợp thực tế; phần kế tiếp giải thích lựa chọn này.
Remote công bố module
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
// Các cấu hình entry, output và loader không được trình bày ở đây.
plugins: [
new ModuleFederationPlugin({
name: 'catalog',
filename: 'remoteEntry.js',
exposes: {
'./ProductPage': './src/ProductPage.jsx',
},
}),
],
};./ProductPage là tên public mà catalog cam kết hỗ trợ. ./src/ProductPage.jsx là đường dẫn nội bộ. Catalog có thể chuyển component sang thư mục khác mà host không cần biết, miễn mapping public và contract vẫn giữ nguyên.
Host khai báo nơi lấy module
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
catalog: 'catalog@https://assets.example.com/catalog/r17/remoteEntry.js',
},
}),
],
};Trong cú pháp webpack script remote này, key catalog là alias host dùng trong import. Phần catalog trước @ là tên container do remote công bố; phần sau @ là URL entry. Alias và container name có thể khác nhau, nhưng container name phải khớp remote thực tế.
Host tiêu thụ module qua một async boundary:
export async function loadCatalogPage() {
const remoteModule = await import('catalog/ProductPage');
return remoteModule.default;
}Chuỗi catalog/ProductPage ghép alias catalog của host với exposed key ./ProductPage của remote. Đây không phải request trực tiếp tới file ProductPage.jsx, cũng không phải npm package tên catalog. Hàm chỉ tải component; shell còn phải tích hợp loading, render và xử lý Promise reject.
URL cố định vẫn là tải ở runtime
Host ở ví dụ biết URL r17 khi build, nhưng không nhúng implementation của catalog vào artifact host; runtime vẫn lấy code từ remote. Tuy nhiên, để đổi URL sang r18, cấu hình tĩnh này thường đòi hỏi build lại host.
Bạn có thể dùng địa chỉ ổn định có nội dung thay đổi, nhưng phải xử lý cache và nguy cơ entry/chunk không đồng bộ. Mình ưu tiên artifact URL bất biến theo release, rồi đưa quyết định chọn release ra runtime config hoặc cơ chế dynamic remote khi cần rollout riêng mà không rebuild host.
Webpack gốc hỗ trợ dynamic containers và promise-based remotes; các runtime mở rộng có thể cung cấp API hoặc manifest riêng. Chọn một cơ chế cụ thể rồi thiết kế cache và rollback theo nó, không trộn cú pháp giữa các plugin.
Shared dependencies là một hợp đồng riêng
Federation không yêu cầu chia sẻ mọi dependency. Hai build có thể mang các thư viện riêng; shared cho runtime cơ hội dùng provider chung theo scope, key và version policy. Đổi lại, dependency shared trở thành một điểm tương thích giữa các build.
Mình chỉ share những dependency có lý do rõ: cần cùng instance ở boundary hoặc việc tải trùng đáng để quản lý thêm coupling. Không mặc định biến toàn bộ dependencies thành shared.
Singleton không đồng nghĩa với tương thích
Nếu catalog expose component React để shell render trong cùng cây, React và renderer phải tương thích và module resolution phải tránh tình trạng component dùng một React instance khác với renderer. Trong mô hình này, React và React DOM thường là ứng viên cho singleton ở cả hai bên.
Ví dụ sau là phần shared thêm vào cả host và remote. deps đọc từ package.json tương ứng của mỗi app; range khác nhau giữa hai app vẫn cần được kiểm tra.
const deps = require('./package.json').dependencies;
const shared = {
react: {
singleton: true,
requiredVersion: deps.react,
strictVersion: true,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
strictVersion: true,
},
};
// Truyền shared vào new ModuleFederationPlugin({ ..., shared }).singleton hướng tới một version được dùng cho shared key trong scope đó; nó không sửa breaking changes giữa các version. requiredVersion mô tả range consumer chấp nhận. strictVersion: true cho phép từ chối version không hợp lệ thay vì tiếp tục với một cảnh báo; đây là lựa chọn fail-fast cần đi kèm fallback UI và kiểm thử compatibility.
Không có policy nào trong đoạn này tự upgrade package đã cài hoặc bảo đảm “luôn dùng React của host”. Provider được chọn còn phụ thuộc các version được cung cấp và trạng thái runtime, đặc biệt khi module đã được consume.
Cấu hình root package cũng không mặc nhiên bao phủ mọi subpath import như react-dom/client hay react/jsx-runtime. Khi triển khai, kiểm tra các request thực tế và cách plugin xử lý chúng. Chi tiết nằm ở Shared dependencies.
Eager không phải nút sửa mọi lỗi bootstrap
eager: true có thể đưa shared dependency cùng fallback vào initial chunk và tăng lượng code tải sớm. Không bật cho mọi package chỉ để né lỗi eager consumption. Với webpack, hãy kiểm tra async bootstrap boundary và thứ tự khởi tạo sharing trước, rồi mới quyết định dependency nào thực sự cần eager.
Chia sẻ thư viện khác chia sẻ state
Shell và cart cùng consume một bản thư viện store không có nghĩa chúng dùng chung một store instance. Nếu mỗi bên gọi hàm tạo store, vẫn có hai object độc lập. Điều tương tự xảy ra với query client, event bus và framework context được tạo riêng.
Nếu nhiều consumer thực sự cần cùng instance, hãy xác định ai tạo nó, lifecycle của nó và cách inject public API. Module Federation có thể đưa code tạo instance tới consumer, nhưng không quyết định quyền ghi dữ liệu hay owner của state.
Một React singleton cũng không nối hai root React độc lập thành một cây context chung. Provider, tree boundary và contract truyền dữ liệu vẫn cần thiết kế riêng. Xem Giao tiếp và trạng thái trước khi expose mutable store của domain như một đường tắt.
Phân biệt webpack gốc và hệ sinh thái mở rộng
“Module Federation” có thể chỉ cơ chế trong webpack 5 hoặc các plugin/runtime mở rộng trong hệ sinh thái. Cùng tên khái niệm không có nghĩa cùng package, API hay artifact format.
| Lựa chọn | Điều cần phân biệt |
|---|---|
webpack.container.ModuleFederationPlugin | Plugin tích hợp trong webpack 5; ví dụ name@url, container và sharing ở trang này dùng mô hình đó |
| Hệ sinh thái Module Federation mở rộng | Có thể bổ sung federation runtime, manifest, runtime plugins và hỗ trợ types; phải đọc tài liệu đúng integration và version |
| Plugin cho bundler khác | Config, remote format, dev mode và shared resolution có thể khác; không giả định tương thích chỉ vì cùng có exposes và remotes |
Manifest cung cấp metadata cho runtime, không phải React component để import trực tiếp. Types được sinh hoặc tải tự động giúp developer biết API khi viết code, nhưng không thay runtime validation hay chứng minh host đang chạy tương thích với remote vừa deploy.
Khi bắt đầu, hãy ghi rõ bundler, plugin package, version, remote format và môi trường thực thi. Component import chạy trong browser không đủ để kết luận SSR, hydration hoặc React Server Components sẽ hoạt động với cùng cấu hình.
Release riêng cần gì ngoài federation
Public contract và version skew
Giả sử shell cũ truyền onNavigate(path) vào ProductPage, nhưng catalog mới đổi sang một object navigation bắt buộc. Remote vẫn tải được và export vẫn tồn tại; UI có thể lỗi khi người dùng bấm vào sản phẩm. Module resolution thành công không chứng minh contract tương thích.
Public contract cần nói rõ exports, props/input, kết quả, lifecycle, semantics lỗi và những capability shell cung cấp. Mình ưu tiên thay đổi bổ sung có default an toàn, giữ consumer cũ hoạt động trong compatibility window được công bố, rồi mới loại bỏ API cũ sau khi có bằng chứng consumer không còn dùng.
TypeScript kiểm tra lúc build. Một declaration được host dùng hôm qua không kiểm tra code remote mà browser tải hôm nay; object ép kiểu cũng không validate dữ liệu network. Hãy kiểm tra những boundary runtime cần thiết và test các tổ hợp version được hỗ trợ, bao gồm shell cũ với remote mới và đường rollback tương ứng.
Chọn artifact và giữ khả năng rollback
Remote cần định danh artifact rõ, không chỉ có tên domain. Ví dụ catalog-r17 và catalog-r18 là hai bản build minh họa, còn version contract là một thông tin khác: hai artifact có thể cùng hỗ trợ một contract.
Mình mặc định chọn một snapshot cấu hình đã validate cho mỗi document, trỏ tới artifact graph bất biến. Tab đang mở giữ lựa chọn của nó; tab mới có thể nhận config rollout mới. Muốn thay remote giữa phiên là một yêu cầu riêng, phải xử lý module cache, shared dependencies, instance và state đang tồn tại.
Giữ các chunk của release cũ đủ lâu để hỗ trợ tab cũ và rollback. Chỉ đổi entry về bản trước nhưng đã xóa chunks của bản đó không phải rollback hoàn chỉnh. Đổi cấu hình trên server cũng không tự thay code đã đánh giá trong browser đang mở.
Fallback và quan sát lỗi
| Phase lỗi | Ví dụ | Phản ứng cần thiết kế |
|---|---|---|
| Resolve hoặc tải entry | Config sai, URL không được phép, CDN lỗi | Báo remote chưa tải được; giữ shell và navigation usable |
| Khởi tạo hoặc lấy module | Shared version không hợp lệ, exposed key không tồn tại | Ghi rõ remote/build và loại lỗi; không retry mù một mismatch cố định |
| Tải chunk | Entry có nhưng chunk thiếu, publicPath sai | Kiểm tra artifact graph; áp dụng retry hoặc yêu cầu reload có kiểm soát |
| Render hoặc mount | Component lỗi, lifecycle contract sai | Cô lập tại UI boundary; cleanup tài nguyên của instance lỗi |
| Dữ liệu domain | Product API lỗi sau khi UI đã mount | Remote hiển thị error state theo nghiệp vụ, không giả thành danh sách rỗng |
Loading UI, load rejection và lỗi render là các đường xử lý khác nhau. Trong React, Suspense hỗ trợ trạng thái chờ, Error Boundary hỗ trợ các lỗi trong cây render; Promise reject của loader riêng vẫn cần được xử lý tại đường gọi tương ứng. Không kỳ vọng một boundary bắt mọi lỗi async hoặc lỗi trong event handler.
Telemetry nên ghi host build, remote build, config release, module request, phase lỗi và thời gian tới usable. Từ đó bạn có thể phân biệt “CDN không tải được entry” với “catalog render được nhưng API sản phẩm lỗi”. Không đưa token hay dữ liệu nhạy cảm vào log.
Remote code vẫn là code trong document của bạn
Remote tải từ CDN khác không vì thế có một sandbox hay origin riêng khi chạy như script của trang. Nó có thể truy cập tài nguyên của document theo quyền của môi trường thực thi. Nguồn artifact và runtime config phải được kiểm soát; backend vẫn phải enforce authorization, bất kể frontend thuộc team nào.
Lộ trình đọc trong nhóm
Đọc theo thứ tự dưới đây nếu bạn mới bắt đầu. Nếu đã có một integration chạy được, đi thẳng tới phần shared dependencies và compatibility để kiểm tra các giả định dễ bị bỏ sót.
| Bài | Câu hỏi cần trả lời sau khi đọc |
|---|---|
| Cơ chế Module Federation | Container, share scope và module factory tham gia vào luồng tải thế nào? |
| Cấu hình exposes và remotes | Public key, host alias và remote location liên hệ ra sao trong bundler mình dùng? |
| Shared dependencies | Package nào cần share; singleton, version range, fallback và eager thay đổi hành vi thế nào? |
| Dynamic remotes | Làm sao chọn artifact ở runtime mà vẫn kiểm soát URL, cache và rollback? |
| Types và contracts | Types hỗ trợ developer ra sao và runtime contract còn phải kiểm tra những gì? |
| Version compatibility | Những cặp host/remote nào được hỗ trợ; rollout và loại bỏ API cũ theo quy tắc nào? |
Bài tập kiểm chứng boundary đầu tiên
Bắt đầu với một remote catalog cho một nhánh route, chưa cần nối mọi domain. Viết một bản mô tả ngắn gồm public module, input, navigation capability, shared dependency policy, artifact selection và fallback. Nếu chưa xác định được catalog được phép gọi API nào của shell, hãy giải quyết contract trước khi thêm cấu hình plugin.
Sau đó kiểm chứng bằng các tình huống cụ thể:
- Build shell với public contract của catalog hiện tại, rồi release một catalog mới chỉ đổi implementation. Shell phải dùng được bản mới theo artifact selection đã thiết kế mà không sửa source shell.
- Chặn request entry và một request chunk trong browser. Xác nhận shell giữ được navigation, phân biệt phase lỗi và cho người dùng đường phục hồi.
- Chạy một remote cố ý không tương thích shared version trong môi trường test. Quan sát policy fail-fast hoặc fallback có đúng như đã công bố không.
- Mở một tab trước rollout, một tab sau rollout, rồi rollback. Ghi lại artifact mỗi tab dùng thay vì giả định mọi tab đều chuyển bản cùng lúc.
- Vào catalog, rời route rồi quay lại. Kiểm tra listener, subscription và instance được dọn đúng; unmount UI không có nghĩa module đã bị unload khỏi runtime.
Mục tiêu không phải chỉ có hai dev server giao tiếp được. Bạn cần chứng minh boundary còn đúng khi release lệch nhau và một phần hệ thống không sẵn sàng.
Checklist trước khi nối thêm remote
- Có owner cho public contract và một lý do release riêng cụ thể.
- Exposed modules là API có chủ đích, không phải đường truy cập file nội bộ tùy ý.
- Bundler, plugin version và remote format được ghi rõ.
- Host alias, container name và các runtime name của build không xung đột.
- Shared dependencies có scope, key và version policy đã được kiểm chứng.
- Loading, load rejection, render failure và domain error có cách xử lý riêng.
- Entry, chunks, CSS và assets thuộc một artifact graph nhất quán.
- Có chính sách cho tab cũ, retention của artifact và rollback.
- Kiểm thử compatibility không chỉ dùng mọi ứng dụng ở version mới nhất.
- Telemetry đủ xác định cặp host/remote và phase lỗi mà không lộ dữ liệu nhạy cảm.
- Artifact/config chỉ đến từ nguồn tin cậy; federation không được coi là sandbox.
Đọc tiếp
Nếu chưa thấy rõ phần nào do shell quản lý và phần nào do federation runtime quản lý, đọc hai bài kiến trúc trước. Sau đó quay lại luồng container và cấu hình cụ thể.
Runtime composition
Phân biệt loader, artifact selection và trách nhiệm ghép ứng dụng.
Application shell
Thiết kế route ownership, lifecycle, capabilities và failure boundary.
Cơ chế Module Federation
Đi sâu vào remote entry, container và quá trình lấy public module.
So sánh cách tích hợp
Chọn cơ chế theo contract và isolation thay vì chỉ theo tên công cụ.
Tài liệu tham khảo
- webpack — Module Federation concepts: independent builds, container API, dynamic remotes, async boundary và các lỗi tích hợp thường gặp.
- webpack — ModuleFederationPlugin:
exposes,remotes, remote type và các sharing hints nhưsingleton,requiredVersion,strictVersion,eager. - Module Federation — Introduction: phân biệt cơ chế webpack tích hợp với hệ sinh thái mở rộng có manifest, federation runtime và runtime plugins.
Chọn kiến trúc bằng trade-off
Chọn cách ghép frontend theo ranh giới release, team autonomy, UX, năng lực vận hành và chi phí thay đổi; kiểm chứng bằng pilot và ADR.
Cơ chế Module Federation
Theo dõi một federated import qua remote entry, container, share scope, module factory và cache để hiểu host thực sự lấy code từ remote như thế nào.