Làm sao để khi click vào /product/123, hệ thống biết phải gọi MFE Catalog thay vì MFE Checkout một cách mượt mà nhất?
1. Phân tích thiết kế: Điều hướng tập trung vs Phân tán
Cách làm truyền thống: Centralized Routing (Tập trung)
App Shell giữ một bản đồ khổng lồ chứa toàn bộ các route của tất cả các MFE.
Nhược điểm:
- Nghẽn cổ chai (Orchestration Bottleneck): Mỗi khi Team Catalog thêm một trang mới (Vd:
/flash-sale), họ phải yêu cầu Team App Shell cập nhật code và deploy lại Shell. Điều này vi phạm tính độc lập của MFE. - Phình to Bundle: File cấu hình route của Shell sẽ ngày càng phình to khi hệ thống có hàng ngàn trang.
Cách làm mới: Distributed Routing (Phân tán)
Shell chỉ nắm giữ các "Top-level routes" hoặc "Prefixes". Việc điều hướng chi tiết bên trong là trách nhiệm của từng MFE.
Ưu điểm & Vấn đề giải quyết:
- Decoupling: Team Catalog tự do thêm bớt sub-routes mà không cần báo cáo với Shell.
- Lazy Loading tối ưu: Chỉ khi URL khớp với
/shop/*, Shell mới tải code của Catalog MFE.
2. Navigation Contract (Hợp đồng điều hướng)
Để các MFE có thể "nói chuyện" về mặt điều hướng mà không cần biết code của nhau, chúng ta dùng URL làm Source of Truth.
Các cơ chế đồng bộ:
- Custom Events: MFE bắn một sự kiện
navigatekèm theo path, Shell lắng nghe và thực hiệnhistory.push. - Shared History Object: Host truyền đối tượng
historyxuống cho tất cả các Remote.
3. Đánh đổi (Trade-offs)
- Cơn ác mộng đồng bộ: Nếu Shell dùng Next.js (SSR) và Remote dùng React Router (CSR), việc đồng bộ trạng thái router cực kỳ phức tạp (dễ gây lỗi lặp vô tận).
- Deep Linking: Cần cấu hình đặc biệt để khi user F5 tại trang
/shop/product/123, Server biết phải trả về trang Shell và để Shell "ném" path đó vào cho Catalog xử lý.
4. Code minh họa: Cấu hình Distributed Routing
// App Shell (Host)
const routes = [
{
path: '/shop/*', // Catch-all prefix
component: React.lazy(() => import('catalog/App')),
},
{
path: '/order/*',
component: React.lazy(() => import('order/App')),
}
];
// Catalog MFE (Remote)
const CatalogRouter = () => (
<Routes>
<Route path="product/:id" element={<ProductDetail />} />
<Route path="search" element={<SearchPage />} />
</Routes>
);Bài tập: Thiết kế Host App
Hãy thiết kế sơ đồ routing cho Host App chứa 2 remote:
/catalog/*-> loadRemoteCatalogApp./checkout-> loadRemoteCheckout.- Yêu cầu: Xử lý trường hợp người dùng chuyển từ Catalog sang Checkout mà vẫn giữ được Context của người dùng (giữ nguyên state của giỏ hàng trên Header).
Câu hỏi phỏng vấn
Q: Làm sao để đồng bộ hóa Router giữa Host (Next.js) và Remote (React Router)?
- Trả lời: Đây là vấn đề phổ biến nhất. Giải pháp là sử dụng Shared History Object. Chúng ta tạo một
browserHistoryduy nhất ở Host và truyền nó xuống cho các Remote. Các Remote lắng nghe sự thay đổi của link và tự động điều hướng nội bộ. Một cách khác hiện đại hơn là dùng Navigation Events (Shell bắn event khi URL thay đổi, Remote nghe và update render).
Q: Centralized Routing và Distributed Routing: Bạn sẽ chọn cái nào cho dự án Shopee?
- Trả lời: Với dự án siêu lớn, Distributed Routing là bắt buộc. Shell chỉ nắm giữ các "Top-level routes" (Vd:
/cart/*đi tới MFE Checkout). Bên trong Checkout có các sub-routes như/shipping,/paymentthì team Checkout tự quản lý. Nếu dùng Centralized, team App Shell sẽ trở thành "nút thắt cổ chai" vì team nào cũng phải đòi update config của Shell.
Q: Deep Linking hoạt động như thế nào trong kiến trúc Microfrontends?
- Trả lời: Khi người dùng vào thẳng link
/shop/product/123, Host App phải đủ thông minh để nhận diện prefix/shop/, tải bundle của Catalog MFE, sau đó truyền toàn bộ path/product/123vào cho Catalog. Catalog sẽ dùng Router nội bộ để hiển thị đúng trang Detail. Quá trình này yêu cầu sự phối hợp chặt chẽ giữa Host Router và Remote Router.