Nút thắt cổ chai của kiến trúc Monolith
Việc mở rộng một ứng dụng frontend thường mang lại cảm giác như đang điều khiển một con tàu chở hàng khổng lồ đi qua một con kênh hẹp. Trong những ngày đầu làm việc với các dự án React cấp doanh nghiệp, mọi tính năng đều nằm trong một repository duy nhất và cồng kềnh.
Ban đầu mọi thứ vẫn ổn. Tuy nhiên, khi đội ngũ của chúng tôi mở rộng từ 5 lên 50 lập trình viên, sự ma sát bắt đầu trở nên rõ rệt. Thời gian build tăng từ 2 phút lên hơn 20 phút, các xung đột khi merge (merge conflicts) trở thành những cuộc đối đầu căng thẳng hàng ngày, và một lỗi CSS nhỏ ở phần footer cũng có thể làm sập toàn bộ luồng thanh toán một cách bất ngờ.
Kiến trúc Monolith cuối cùng cũng sẽ chạm trần. Mặc dù chúng ta thường cố gắng giải quyết vấn đề này bằng cách chia nhỏ mã nguồn thành các gói NPM nội bộ, nhưng cách tiếp cận đó có một khuyết điểm lớn: bất kỳ bản cập nhật nào cũng yêu cầu build lại và deploy lại toàn bộ ứng dụng host. Chúng tôi cần một cách để deploy các tính năng UI một cách độc lập mà không gây rủi ro cho sự ổn định của toàn hệ thống. Micro-frontends chính là “lối thoát” đó.
Bước chuyển mình sang Module Federation
Micro-frontends chia nhỏ một ứng dụng web khổng lồ thành các phần nhỏ, độc lập nhưng vẫn hiển thị như một sản phẩm duy nhất đối với người dùng cuối. Trong khi các lập trình viên đã thử dùng iframe hoặc server-side composition trong nhiều năm, Webpack 5 đã giới thiệu một giải pháp thanh thoát hơn gọi là Module Federation.
Công nghệ này cho phép một ứng dụng JavaScript tải mã nguồn từ một bản build khác một cách linh hoạt ngay tại runtime. Nó thay đổi cách đóng gói (bundling) truyền thống, nơi mọi dependency phải có sẵn lúc build. Thay vào đó, bạn có thể chia sẻ các component, hook hoặc toàn bộ trang web giữa các server khác nhau. Trong môi trường production, tôi đã thấy kiến trúc này giúp các team có thể deploy 10 lần mỗi ngày mà không cần phải phối hợp với các bộ phận khác.
Để làm chủ công nghệ này, bạn cần hiểu ba vai trò cốt lõi:
- Host: Ứng dụng vỏ (shell) đóng vai trò là điểm truy cập chính và “tiêu thụ” các phần remote.
- Remote: Một ứng dụng độc lập “cung cấp” (expose) các component hoặc logic cụ thể.
- Shared: Các dependency dùng chung, như React hoặc Material UI, được tải một lần và chia sẻ trong toàn bộ hệ sinh thái để tiết kiệm băng thông.
Xây dựng kiến trúc Micro-frontend đầu tiên của bạn
Lý thuyết thế là đủ—hãy cùng bắt tay vào triển khai. Chúng ta sẽ xây dựng hai ứng dụng: một Remote (một Product Card) và một Host (Dashboard chính).
Bước 1: Thiết lập ứng dụng Remote
Đầu tiên, hãy tạo một thư mục cho ứng dụng remote và khởi tạo nó. Chúng ta sẽ sử dụng Webpack 5 để xử lý các tác vụ nặng.
mkdir remote-app && cd remote-app
npm init -y
npm install react react-dom webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/preset-react --save-dev
Tiếp theo, tạo một component đơn giản trong src/Button.js:
import React from 'react';
const RemoteButton = () => (
<button style={{ padding: '12px 24px', background: '#0070f3', color: 'white', border: 'none', borderRadius: '5px' }}>
Nút Remote Trực tiếp
</button>
);
export default RemoteButton;
File webpack.config.js là nơi diễn ra cấu hình. File này cho Webpack biết những component nào cần được công khai:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
devServer: { port: 3001 },
module: {
rules: [
{ test: /\.js$/, loader: 'babel-loader', options: { presets: ['@babel/preset-react'] } }
]
},
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
new HtmlWebpackPlugin({ template: './public/index.html' }),
],
};
Bước 2: Kết nối ứng dụng Host
Ứng dụng Host sẽ lấy Button từ Remote. Hãy thiết lập một thư mục riêng tên là host-app sử dụng port 3000. Trong cấu hình của Host, bạn phải trỏ tới URL của Remote:
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
// ... cấu hình tiêu chuẩn
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
};
Bước 3: Tích hợp tại Runtime
Trong ứng dụng Host, hãy import remote component bằng lazy và Suspense của React. Vì mã nguồn được lấy qua mạng, bạn cần một loading state để xử lý độ trễ.
import React, { Suspense } from 'react';
const RemoteButton = React.lazy(() => import('remoteApp/Button'));
const App = () => (
<div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
<h1>Bảng điều khiển doanh nghiệp (Host)</h1>
<Suspense fallback="Đang tải component remote...">
<RemoteButton />
</Suspense>
</div>
);
export default App;
Quản lý State và Dependency dùng chung
Sự sai lệch phiên bản là cạm bẫy phổ biến nhất trong môi trường federation. Nếu Host của bạn chạy React 18 nhưng Remote lại cố tình tải React 17, ứng dụng có khả năng cao sẽ bị sập. Việc sử dụng flag singleton: true là điều bắt buộc ở đây. Nó đảm bảo Webpack chỉ tải một instance duy nhất của thư viện, ngay cả khi có nhiều ứng dụng cùng yêu cầu.
Khi nói đến state, hãy giữ cho nó đơn giản. Tôi khuyên bạn nên quản lý global state ở cấp độ Host và truyền dữ liệu qua props. Nếu các micro-frontends cần giao tiếp với nhau mà không muốn bị ràng buộc chặt chẽ, hãy sử dụng một event bus tùy chỉnh nhẹ hoặc một thư viện như mitt. Đừng chia sẻ một Redux store khổng lồ duy nhất giữa các ứng dụng. Làm như vậy sẽ tạo ra các dependency ngầm, làm mất đi mục đích của việc “độc lập”.
Lời kết
Áp dụng Module Federation là một bước đi chiến lược, không chỉ đơn thuần là kỹ thuật. Nó làm tăng độ phức tạp cho pipeline CI/CD và yêu cầu đội ngũ của bạn phải nắm vững cơ chế bên trong của Webpack. Tuy nhiên, đối với các tổ chức lớn, sự đánh đổi này là xứng đáng. Bạn sẽ có thời gian build nhanh hơn và các team có khả năng làm chủ chu kỳ release của riêng mình.
Hãy bắt đầu từ những việc nhỏ. Di chuyển một utility ít rủi ro hoặc một component điều hướng đơn giản trước. Một khi bạn trải nghiệm việc deploy mà chỉ cần build 5% codebase để ra mắt một tính năng, bạn sẽ không bao giờ muốn quay lại kiến trúc monolith nữa.

