Micro-frontend Learning
Nền tảng

Thuật ngữ cốt lõi

Phân biệt host, remote, shell, composition, isolation và independent deployment qua ví dụ shop shell–catalog–cart.

Bạn đọc một cấu hình thấy host tải remote, rồi đọc thiết kế thấy shell ghép các micro-frontend. Đó có phải cùng một thứ không? Nếu catalog đã có URL và pipeline riêng, nó đã deploy độc lập chưa?

Bài này giúp bạn gọi đúng tên từng vai trò và tránh suy ra khả năng của kiến trúc chỉ từ tên công cụ. Ví dụ xuyên suốt là một shop có shell quản lý khung trang, catalog hiển thị sản phẩm và cart hiển thị giỏ hàng.

Cách dùng trang này

Bạn có thể đọc lần lượt hoặc dùng bảng tra nhanh để tìm thuật ngữ. Host và remote được giải thích chủ yếu trong ngữ cảnh Module Federation; shell, composition và isolation là khái niệm kiến trúc rộng hơn. Các ví dụ không phải cấu hình lab chạy được.

Mục lục

Tra nhanh các thuật ngữ chính

Sáu thuật ngữ này trả lời những câu hỏi khác nhau. Một hệ thống có thể có host và remote nhưng chưa đạt independent deployment; cũng có thể đạt independent deployment mà không dùng Module Federation.

Thuật ngữCâu hỏi nó trả lờiTrong shop mẫuKhông nên suy ra
ShellAi giữ khung trải nghiệm và phối hợp các phần?App chứa navigation, layout và chọn phần theo URLShell phải chứa mọi logic nghiệp vụ
HostAi tải và tiêu thụ module được cung cấp từ build khác?Shell import module trang sản phẩm của catalogHost luôn là shell hoặc luôn là app cấp cao nhất
RemoteAi cung cấp module cho bên khác tiêu thụ?Catalog expose trang sản phẩm cho shellRemote phải ở domain khác hoặc phải là cả một app
CompositionCác phần riêng được ghép thành trải nghiệm chung như thế nào?Shell chọn catalog ở /products, cart ở /cartComposition luôn diễn ra trong trình duyệt
IsolationRanh giới nào hạn chế ảnh hưởng giữa các phần?CSS catalog không làm đổi giao diện cartTách bundle đã cách ly JavaScript, lỗi và quyền truy cập
Independent deploymentThay đổi tương thích có thể tới production mà không buộc phần khác phát hành lại không?Catalog cập nhật bộ lọc mà shell và cart giữ nguyên artifactMỗi team được phá contract tùy ý

Shell là vai trò trong sản phẩm

Application shell là phần giữ khung và điều phối trải nghiệm chung. Với shop mẫu, shell có thể quản lý header, navigation, vùng hiển thị nội dung, route cấp cao và trạng thái loading hoặc fallback khi một phần chưa tải được.

direction: down
shell: "Shell: khung trang, navigation,\nchọn theo URL"
catalog: Catalog
cart: Cart
shell -> catalog: /products
shell -> cart: /cart
Shell: khung trang, navigation,chọn theo URLCatalogCart /products /cart
Shell: khung trang, navigation,chọn theo URLCatalogCart /products /cart
Shell là vai trò trong sản phẩm

Sơ đồ này chỉ mô tả trách nhiệm, chưa quyết định shell tải nội dung bằng Module Federation, iframe hay cơ chế khác. Nó cũng không yêu cầu catalog và cart xuất hiện cùng lúc.

Shell có thể phối hợp ngữ cảnh đăng nhập và chuyển thông tin cần thiết qua contract, nhưng backend vẫn phải kiểm tra quyền của từng request. Shell không nên tính giá sản phẩm, sở hữu toàn bộ state giỏ hàng hoặc biết cấu trúc store nội bộ của mọi app. Vì nếu mọi thay đổi nghiệp vụ đều phải sửa shell, shell sẽ trở thành điểm buộc các team release cùng nhau.

Mình dùng “shell” cho vai trò tích hợp trải nghiệm trong lộ trình này. Trong các tài liệu về PWA, app shell còn thường chỉ phần khung UI có thể cache để tải nhanh; đừng tự suy ra kiến trúc micro-frontend hay deploy độc lập từ cách dùng đó.

Host và remote là vai trò tích hợp

Trong Module Federation, các build có thể cung cấp và tiêu thụ module của nhau. Hai từ host và remote mô tả hai phía của quan hệ đó, không mô tả ownership nghiệp vụ hay mức cách ly.

Host tiêu thụ module

Host là ứng dụng hoặc build tiêu thụ module từ remote. Ví dụ, shell muốn render trang danh sách sản phẩm và có một đoạn import như sau:

// Minh họa cách gọi sau khi đã cấu hình Module Federation.
const catalogModule = await import('catalog/ProductPage');

Trong quan hệ này, shell là host và catalog là remote. catalog là tên mà bên tiêu thụ dùng để tham chiếu remote; ProductPage là tên module công khai trong ví dụ. Đây không phải đường dẫn HTTP, cũng không phải cú pháp tự hoạt động trong một dự án chưa cấu hình federation.

Host chịu trách nhiệm cho cách sử dụng module: tải khi nào, đặt UI vào đâu và xử lý lỗi tích hợp ra sao. Tuy nhiên, tên “host” không có nghĩa nó luôn cung cấp toàn bộ framework hoặc shared dependency; điều đó phụ thuộc cấu hình.

Remote cung cấp module

Remote là phía cung cấp module cho bên khác tiêu thụ. Catalog có thể expose một trang, một component hoặc một API lifecycle để host gọi. Phần được expose không nhất thiết là toàn bộ code của catalog.

Trong webpack Module Federation, container là cơ chế cung cấp quyền truy cập vào các module được expose. Một container entry, thường được đặt tên remoteEntry.js, giúp runtime truy cập container và tải module cần dùng. Entry không nhất thiết chứa toàn bộ code ứng dụng; runtime có thể tải thêm các chunk sau đó. Tên file remoteEntry.js là quy ước phổ biến, không phải tên bắt buộc.

“Remote” cũng không có nghĩa khác origin. Shell và catalog có thể phục vụ từ cùng một origin qua các đường dẫn khác nhau; ngược lại, JavaScript tải từ CDN khác origin nhưng chạy trong trang shell không vì thế có sandbox riêng.

Remote không tự động là micro-frontend

Một thư viện UI được expose qua federation là remote về mặt kỹ thuật. Muốn đánh giá nó như một micro-frontend, bạn còn phải xem ranh giới nghiệp vụ, ownership và khả năng phát hành. Cơ chế expose module không tự tạo ra các ranh giới đó.

Vì sao host không đồng nghĩa với shell

Shell trả lời “ai giữ trải nghiệm chung?”, còn host trả lời “ai tiêu thụ module trong quan hệ này?”. Trong demo nhỏ, một app thường giữ cả hai vai trò nên hai từ dễ bị dùng thay nhau.

Nhưng catalog có thể vừa expose ProductPage cho shell, vừa tiêu thụ một module từ remote khác. Khi đó catalog là remote đối với shell và là host trong quan hệ tiêu thụ kia. Một build có thể vừa cung cấp vừa tiêu thụ module.

Ngược lại, shell ghép các app bằng iframe vẫn là shell, nhưng không phải host theo cơ chế Module Federation. Khi đọc thiết kế, hãy hỏi tên gọi đang mô tả trách nhiệm sản phẩm hay quan hệ tải module.

Composition là cách ghép các phần frontend

Composition là việc ghép các phần frontend thành một ứng dụng hoặc trải nghiệm thống nhất. Nó bao gồm chọn phần cần hiển thị, đặt phần đó vào layout, kết nối các contract cần thiết và phối hợp navigation.

Ghép vào thời điểm nào

Kiểu compositionNơi và thời điểm ghépVí dụHệ quả cần nhớ
Build-time compositionBundler ghép dependency khi tạo bản buildShell import package @shop/catalogCập nhật catalog thường cần build và deploy lại shell
Runtime compositionTrình duyệt tải các phần khi ứng dụng chạyShell tải module catalog từ entry riêng khi vào /productsCó thể thay catalog riêng nếu cơ chế chọn version và contract cho phép
Server-side compositionServer hoặc edge ghép HTML hay fragment trước khi trả responseServer ghép header chung với fragment sản phẩmCó thể phát hành phần riêng nếu lớp ghép không buộc release đồng bộ; phần tương tác vẫn cần thiết kế JavaScript phù hợp

Các kiểu này có thể kết hợp. Một trang có HTML ghép ở server rồi tải phần tương tác trong trình duyệt vẫn dùng nhiều lớp composition.

SSR không tự đồng nghĩa với server-side composition của micro-frontend. Một server render toàn bộ React app từ một bản build chung là SSR, nhưng chưa chứng minh rằng có các đơn vị frontend phát hành riêng được ghép ở server.

Theo cách dùng trong lộ trình này, khả năng phát hành độc lập là tiêu chí chính để phân biệt micro-frontend với modular frontend. Vì thế build-time composition vẫn là cách tổ chức code đáng học, nhưng bạn cần nhận ra coupling ở bước release thay vì chỉ đếm số package.

Ghép theo route hay trong cùng trang

Thời điểm ghép là một trục; phạm vi UI được ghép là trục khác:

  • Route-level composition: shell hiển thị catalog ở /products, cart ở /cart. Đây thường là điểm bắt đầu dễ kiểm soát vì ranh giới màn hình và navigation rõ hơn.
  • Fragment-level composition: cùng trang chứa danh sách sản phẩm của catalog và mini-cart của cart. Cách này cần thỏa thuận rõ hơn về layout, cập nhật dữ liệu và các trạng thái loading, lỗi của từng vùng.

Đừng dùng “runtime composition” để suy ra hai app phải render cùng lúc. Shell có thể chỉ tải remote khi route cần nó. Ngược lại, nhiều vùng UI trên cùng trang có thể được ghép từ một build chung và chưa có deploy riêng.

Isolation phải nói rõ cách ly điều gì

Câu “catalog đã được isolate khỏi cart” chưa đủ để đánh giá thiết kế. Catalog không làm đè CSS của cart khác hoàn toàn với catalog không đọc được dữ liệu của shell.

Isolation là mức hạn chế ảnh hưởng giữa các phần trên một ranh giới cụ thể. Mình khuyên luôn gọi tên ranh giới đó khi trao đổi, chẳng hạn “CSS isolation”, “state isolation” hay “security boundary”.

Các chiều cách ly

Chiều cách lyĐiều cần kiểm soátMột tình huống cụ thể
Code và dependencyImport chéo vào code nội bộ, version thư viện và API dùng chungCatalog không import store nội bộ của cart
CSS và DOMSelector global, reset CSS, thao tác DOM vượt vùng sở hữuStyle button của catalog không làm đổi nút checkout
JavaScript và stateGlobal variable, side effect và mutable state dùng chungCatalog không tự sửa state giỏ hàng trong window
Lỗi và tài nguyênPhạm vi lỗi, thời gian CPU, memory và cơ chế khôi phụcCatalog tải lỗi thì shell còn navigation và hiện fallback
Bảo mậtQuyền đọc dữ liệu, truy cập DOM và gọi các khả năng nhạy cảmCode không tin cậy không được chạy với toàn bộ quyền của trang shell

Tách code hoặc state theo convention giúp giảm phụ thuộc, nhưng không nhất thiết cưỡng chế được ranh giới khi chạy. Code cùng JavaScript context vẫn có thể chạm vào global hoặc làm nghẽn main thread.

Giới hạn của các cơ chế phổ biến

Cơ chếNó giúp gìNó không đảm bảo gì
CSS Modules hoặc namespace CSSGiảm va chạm tên class và selectorKhông cách ly JavaScript; style global và reset vẫn có thể ảnh hưởng phần khác
Shadow DOMĐóng gói cây DOM và phạm vi selector CSSKhông tạo JavaScript sandbox hay security boundary; inherited styles và CSS custom properties vẫn có thể đi qua host
React error boundaryBắt một số lỗi render trong cây con và hiển thị fallbackKhông bắt mọi lỗi async, event handler hoặc vòng lặp làm treo main thread
IframeTạo document và browsing context riêngKhông mặc nhiên là process riêng hay cách ly tài nguyên tuyệt đối; mức bảo mật còn tùy origin, sandbox và quyền được cấp
Module FederationTích hợp module từ các build riêng, có cơ chế shared dependencyKhông tự cách ly CSS, JavaScript, lỗi hay quyền truy cập

Iframe khác origin thường tạo ranh giới truy cập DOM tốt hơn nhờ same-origin policy, nhưng cấu hình sandbox và giao tiếp qua postMessage vẫn cần được thiết kế. Tăng isolation cũng thường tăng công việc tích hợp: đồng bộ chiều cao, focus, routing hoặc trải nghiệm accessibility.

Tải script khác origin không có nghĩa là chạy ở origin đó

Remote script chạy trong trang shell có quyền của ngữ cảnh trang đó. Đừng dùng URL CDN, tên remote hay Shadow DOM làm bằng chứng rằng code không tin cậy đã được sandbox. Nếu yêu cầu là bảo mật, hãy đánh giá threat model và cơ chế cưỡng chế ranh giới.

Independent deployment là khả năng phát hành riêng

Independent deployment nghĩa là một phần frontend có thể đưa thay đổi tương thích tới production mà không bắt buộc build và deploy lại các phần khác. Điều cần kiểm tra là đường đi của thay đổi tới người dùng, không chỉ việc artifact được upload riêng.

Phân biệt build, deploy và release

Trong bài này, mình dùng ba từ theo quy ước sau; một số team gộp deploy và release thành một bước:

TừÝ nghĩaTrong một lần cập nhật catalog
BuildTạo artifact từ source codeSinh entry, JavaScript chunks, CSS và các asset
DeployĐưa artifact hoặc cấu hình tới môi trường đíchĐưa bản catalog mới lên nơi phục vụ asset production
ReleaseCho một nhóm người dùng sử dụng thay đổiCấu hình chọn artifact mới hoặc bật tính năng cho nhóm đó

Artifact là đầu ra cụ thể của một build, không chỉ mỗi file entry. Muốn tải lại hoặc rollback một version, các chunk và asset mà version đó cần cũng phải còn khả dụng.

Một catalog đã deploy lên CDN nhưng shell vẫn trỏ tới version cũ thì người dùng chưa nhất thiết nhận bản mới. Nếu shell phải sửa source, build và deploy lại chỉ để dùng catalog mới, chu kỳ phát hành của catalog vẫn phụ thuộc shell.

Phép thử với một thay đổi catalog

Giả sử catalog thêm bộ lọc, giữ nguyên contract công khai. Hệ thống dùng runtime config để chọn artifact:

direction: down
catalog: Catalog {
  code: Sửa code
  build: Build / test
  deploy: Deploy artifact
  select: Chọn version mới
  code -> build -> deploy -> select
}
shell: "Shell: giữ artifact, đọc cấu\nhình tích hợp"
cart: "Cart: không đổi code, không\nbuild lại"
catalog.select -> shell: "Cập nhật selection"
CatalogShell: giữ artifact, đọc cấuhình tích hợpCart: không đổi code, khôngbuild lạiSửa codeBuild / testDeploy artifactChọn version mới Cập nhật selection
CatalogShell: giữ artifact, đọc cấuhình tích hợpCart: không đổi code, khôngbuild lạiSửa codeBuild / testDeploy artifactChọn version mới Cập nhật selection
Phép thử với một thay đổi catalog

Đây là dấu hiệu đạt phát hành độc lập cho thay đổi đó. Để kết luận chắc hơn, bạn cần trả lời:

  • Có thể cập nhật tham chiếu catalog mà không rebuild shell không?
  • Catalog mới có chạy với shell, cart và backend hiện tại không?
  • Hệ thống có xử lý người dùng vẫn đang chạy version cũ không?
  • Nếu cần khôi phục version trước, artifact cũ và các API mà nó dùng còn hoạt động không?

Một monorepo có thể đáp ứng các điều này; nhiều repo cũng có thể không đáp ứng. Pipeline riêng giúp tổ chức build và deploy, nhưng nếu quy trình luôn bắt tất cả app release cùng nhau thì mục tiêu độc lập vẫn chưa đạt.

Độc lập không phải “không cần phối hợp”. Đổi tên route công khai hoặc bắt bên tiêu thụ truyền một field mới có thể là breaking change và cần kế hoạch chuyển đổi. Mục tiêu là thay đổi nội bộ tương thích không bị buộc release đồng bộ, không phải mọi thay đổi đều được làm tùy ý.

Những thuật ngữ đi cùng các ranh giới

Bạn sẽ gặp các từ dưới đây khi đọc những bài tiếp theo. Chúng giúp mô tả điều kiện để sáu khái niệm chính hoạt động cùng nhau.

Domain, ownership, contract và coupling

Thuật ngữCách hiểu trong lộ trìnhVí dụ hoặc giới hạn
DomainPhạm vi nghiệp vụ mà frontend phục vụCatalog phục vụ tìm và xem sản phẩm; không phải cứ có một folder là có một domain
Bounded contextRanh giới trong đó mô hình và ngôn ngữ nghiệp vụ có ý nghĩa nhất quán, theo DDD“Sản phẩm” ở catalog và “dòng hàng” ở cart có thể mang dữ liệu khác nhau; không cần dùng chung toàn bộ model
OwnershipTrách nhiệm của team đối với một phần từ phát triển, kiểm thử đến vận hànhTeam catalog theo dõi lỗi và chất lượng của catalog, không chỉ sửa code
ContractGiao diện công khai và hành vi đã thống nhất giữa các bênRoute, export của module, props, event schema, API lifecycle và quy ước lỗi
CouplingMức một phần phải biết hoặc thay đổi theo phần khácCatalog đọc store nội bộ của cart tạo coupling mạnh hơn gọi một API công khai ổn định
CompatibilityCác version khác nhau vẫn làm việc đúng theo contractCatalog mới vẫn chạy với shell hiện tại; TypeScript không tự đảm bảo điều đó ở runtime

Contract không chỉ là type. Ví dụ, một event cart:changed cần nói rõ nó có nghĩa “backend đã cập nhật giỏ thành công” hay chỉ “người dùng vừa bấm nút”. Hai cách hiểu có thể có cùng payload nhưng khiến UI hoạt động khác nhau.

Mình mặc định giữ chi tiết store và model nội bộ trong phần sở hữu nó. Chỉ chia sẻ dữ liệu tối thiểu qua contract, vì chia sẻ toàn bộ implementation khiến các bên khó thay đổi riêng dù đã có artifact riêng.

Entry, manifest và shared dependency

Thuật ngữÝ nghĩaĐiều dễ nhầm
Entry pointĐiểm bắt đầu để tải hoặc sử dụng một phần frontendEntry có thể tải thêm chunk; lấy được entry chưa có nghĩa UI đã render
ManifestMetadata mô tả artifact, địa chỉ hoặc các module có thể tải, tùy công cụKhông phải mọi hệ thống đều có manifest; schema không có một chuẩn chung cho mọi cách tích hợp
Runtime configurationCấu hình được đọc khi chạy để chọn remote, version hoặc hành vi tích hợpChỉ ghi URL trong source rồi build lại không phải cơ chế đổi version độc lập ở runtime
Shared dependencyDependency được phối hợp dùng chung giữa các buildChia sẻ React không có nghĩa các app chia sẻ cùng business state
SingletonTrong Module Federation, tùy chọn giới hạn module được chia sẻ về một version dùng trong share scope tương ứngKhông tự bảo đảm tương thích và không có nghĩa chỉ có một instance cho mọi trang, iframe hay scope

Shared dependency có thể giảm tải trùng, nhưng nó tạo thêm ràng buộc version. Đừng đồng nhất “dùng chung framework” với “hoàn toàn độc lập về dependency”; đó là một đánh đổi cần quản lý.

Lifecycle, fallback và rollback

Lifecycle là các giai đoạn tích hợp một phần frontend: tải code, chuẩn bị nếu cần, mount UI, cập nhật và unmount. Tên và thứ tự API cụ thể tùy công cụ; không có một interface bắt buộc cho mọi micro-frontend.

Ví dụ, tải xong catalog chưa đồng nghĩa catalog đã mount vào vùng nội dung. Khi rời /products, phần chịu trách nhiệm lifecycle cần dọn UI và các tài nguyên thuộc catalog, như event listener hoặc subscription. Module Federation cung cấp module; bản thân cơ chế đó không bắt mọi remote có sẵn mount và unmount.

Fallback là UI hoặc hành vi thay thế khi một phần chưa sẵn sàng hoặc lỗi, chẳng hạn vùng catalog hiện thông báo cùng nút thử lại. Fallback giúp người dùng còn sử dụng phần khả dụng; nó không sửa nguyên nhân và không tự rollback version.

Rollback là đưa hệ thống trở lại artifact hoặc cấu hình trước đó. Với runtime composition, rollback có thể là đổi tham chiếu về catalog cũ, nhưng còn phụ thuộc cache, asset cũ và backend compatibility. Người đã mở trang cũng không nhất thiết đổi code ngay khi tham chiếu được cập nhật.

Đọc một mô tả kiến trúc cho đúng

Hãy xét mô tả giả định này:

Shop dùng shell làm host để tải catalog và cart qua Module Federation. Các app có pipeline riêng, dùng shared React và CSS Modules. Catalog được deploy độc lập.

Tách từng khẳng định ra, bạn sẽ biết điều gì đã rõ và điều gì cần hỏi thêm:

Khẳng địnhĐiều có thể hiểuĐiều chưa được chứng minh
Shell làm hostPhần giữ khung trải nghiệm cũng tiêu thụ module remoteShell có đang chứa quá nhiều logic nghiệp vụ không
Catalog và cart là remoteHai phía cung cấp module cho shellRanh giới domain và ownership có hợp lý không
Dùng Module FederationCó cơ chế tải module giữa các buildCó thể đổi version mà không rebuild shell không
Dùng shared ReactCó phối hợp dependency frameworkCác version có tương thích không; state có được giữ riêng không
Dùng CSS ModulesCó giảm va chạm classCSS global, JavaScript và lỗi đã được cách ly chưa
Có pipeline riêngCó đường build, test hoặc deploy riêng cho từng appRelease thực tế có bị buộc đồng bộ không

Trước khi chấp nhận câu “catalog deploy độc lập”, hãy yêu cầu mô tả một thay đổi nội bộ cụ thể đi tới production: artifact nào đổi, ai phải build lại, cấu hình nào chọn version và khi lỗi thì khôi phục bằng cách nào. Đó là cách biến thuật ngữ thành một điều có thể kiểm chứng.

Đọc tiếp

Các trang dưới đây đi sâu vào từng phần; những trang còn là placeholder cho biết phạm vi dự kiến của lộ trình.

Tài liệu tham khảo

On this page