Mục lục
Khi dự án của bạn lớn dần lên, một nhóm frontend duy nhất không thể ôm trọn mọi thứ. Codebase trở thành một "nồi lẩu thập cẩm", thời gian build kéo dài tính bằng chục phút, và mỗi lần Deploy là một nỗi ám ảnh vì xung đột mã nguồn.
Đó là lý do kiến trúc Micro Frontends ra đời. Nó cho phép chia nhỏ một ứng dụng khổng lồ thành nhiều ứng dụng con độc lập, được phát triển và Deploy bởi các nhóm khác nhau. Hôm nay, chúng ta sẽ kết hợp khái niệm Module Federation đình đám với "chiếc xe đua F1" của làng Web Tooling là Vite.
Lời Nguyền Của Monolith Frontend
Phần lớn các ứng dụng React/Vue/Angular truyền thống được xây dựng theo kiểu Monolithic (nguyên khối). Tức là tất cả các module: Xác thực (Auth), Giỏ hàng (Cart), Thanh toán (Checkout), Sản phẩm (Product)... đều nằm chung trong 1 kho chứa (Repository) và chung 1 quá trình Build.
- Scale kém: 50 lập trình viên cùng làm việc trên 1 repo sẽ liên tục gặp Merge Conflict.
- Build chậm: Dự án lớn dùng Webpack có thể mất tới 10-20 phút chỉ để ra file bundle.
- Rủi ro Deploy: Team Checkout sửa 1 dòng code, nhưng phải test lại toàn bộ Team Auth, Team Cart rồi mới dám deploy toàn hệ thống.
Giải pháp: Micro Frontends. Tách UI thành các Repo riêng, deploy riêng, và "ghép" chúng lại trên trình duyệt của người dùng.
Module Federation Là Gì?
Ra mắt từ Webpack 5, Module Federation (MF) là kỹ thuật cho phép một ứng dụng JavaScript "tải và chạy" mã nguồn của một ứng dụng JavaScript khác ngay lúc Runtime (chạy trên trình duyệt), thay vì phải cài đặt qua NPM lúc Build-time.
Trong kiến trúc này, chúng ta có 2 khái niệm:
- Host (Shell): Ứng dụng vỏ bọc bên ngoài. Nó chứa thanh điều hướng chung và load các ứng dụng con vào.
- Remote (Micro-app): Ứng dụng con chứa một chức năng cụ thể (ví dụ: Trang Thanh toán). Nó phơi bày (expose) các component của nó ra ngoài.
Tuyệt vời hơn, Vite (thông qua Rollup) hoàn toàn có thể hỗ trợ chuẩn Module Federation này bằng các plugin bên thứ ba, cho phép ta vừa có kiến trúc xịn, vừa giữ được tốc độ HMR chớp nhoáng.
Cài Đặt Vite Plugin
Để mang tính năng này vào Vite, chúng ta sẽ sử dụng plugin rất nổi tiếng:
@originjs/vite-plugin-federation.
Bắt đầu bằng việc khởi tạo 2 dự án Vite React độc lập:
# Khởi tạo Host App
npm create vite@latest host-app -- --template react-ts
# Khởi tạo Remote App
npm create vite@latest remote-app -- --template react-ts
# Cài đặt plugin cho cả hai
npm i -D @originjs/vite-plugin-federation
Cấu Hình Remote App
Mở file vite.config.ts của dự án remote-app. Nhiệm vụ của chúng ta là phải
"xuất" (expose) một component (ví dụ: Button) ra cho thế giới bên ngoài dùng.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'remote_app',
filename: 'remoteEntry.js', // File entry point mà Host sẽ gọi
exposes: {
'./Button': './src/components/Button.tsx', // Xuất component Button
'./CartApp': './src/App.tsx' // Có thể xuất cả 1 trang App
},
shared: ['react', 'react-dom']
})
],
build: {
target: 'esnext' // Yêu cầu bắt buộc để chạy MF trên Vite
}
});
Khởi động remote app ở một cổng cố định, ví dụ 5001.
Cấu Hình Host App
Bây giờ, sang vite.config.ts của host-app. Chúng ta sẽ "nhập" (remotes) cái
remoteEntry.js từ server 5001 vừa rồi.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'host_app',
remotes: {
// Tên tham chiếu: URL tới file entry của remote
remoteApp: 'http://localhost:5001/assets/remoteEntry.js'
},
shared: ['react', 'react-dom']
})
],
build: {
target: 'esnext'
}
});
Cách sử dụng trong React (Host):
import React, { lazy, Suspense } from 'react';
// Import component từ Remote (Tên_tham_chiếu/Tên_Export)
// Phải dùng lazy load vì mã này lấy từ mạng (Network) về
const RemoteButton = lazy(() => import('remoteApp/Button'));
function App() {
return (
<div>
<h1>Đây là Host App (Vỏ ngoài)</h1>
<Suspense fallback={<div>Đang tải Button từ Remote...</div>}>
<RemoteButton onClick={() => alert('Alo!')} />
</Suspense>
</div>
);
}
export default App;
Khi chạy Host App, trình duyệt sẽ tự động bắn 1 request tải file remoteEntry.js từ port
5001, parse và nạp Component Button vào DOM một cách thần kỳ!
Chia Sẻ Dependencies (Tối Ưu Hóa)
Bạn có để ý mảng shared: ['react', 'react-dom'] ở cả Host và Remote không? Đó là một tính
năng cực kỳ thông minh của Module Federation.
Khi Host App đã tải thư viện React từ mạng về, và sau đó nó tiếp tục gọi Remote App (vốn cũng được viết bằng React), thì trình duyệt sẽ KHÔNG tải lại React lần thứ 2!
Module Federation sẽ kiểm tra phiên bản (version) của các thư viện dùng chung. Nếu chúng tương thích, Remote App sẽ tái sử dụng luôn instance React của Host App. Điều này giúp giảm hàng trăm KB dung lượng tải xuống và giữ nguyên trạng thái React Context an toàn.
Lưu Ý Về CSS Isolation (Cách ly CSS)
Micro Frontends mang lại kiến trúc tuyệt vời, nhưng nhược điểm lớn nhất của nó là CSS Leak (Rò rỉ CSS).
Vì cả Host và Remote cuối cùng đều render lên cùng 1 cây DOM của Trình duyệt. Nếu Remote App có một đoạn
CSS h1 { color: red; }, thì tất cả các thẻ H1 của Host App cũng sẽ bị biến thành màu đỏ!
- Sử dụng CSS Modules: Đảm bảo mọi tên class đều được băm (hash) duy nhất
.Button_xyz123. - Tailwind Prefix: Nếu dùng Tailwind, hãy đặt
prefix: 'remote-'trongtailwind.config.jscủa ứng dụng con để tránh đụng độ class với Host. - Web Components (Shadow DOM): Đây là mức độ cách ly cao nhất nhưng sẽ khó kết nối với React Context.
Micro Frontends là một kiến trúc rất phức tạp. Hãy chỉ sử dụng nó khi ứng dụng của bạn đủ lớn (hàng chục dev) và có nghiệp vụ cần chia cắt rõ ràng. Đừng lạm dụng nó cho các dự án nhỏ vì nó sẽ biến hệ thống CI/CD của bạn thành ác mộng. Chúc bạn thành công với kiến trúc này trên Vite!