Nỗi ám ảnh mang tên Monolith Frontend
Mình từng cầm lái một dự án React với codebase vượt ngưỡng 50.000 dòng. Cảm giác lúc đó thực sự là ác mộng. Chỉ cần sửa một dòng CSS ở Header, mình phải ngồi đợi Jenkins build mất 20 phút. Tệ hơn nữa, một lỗi logic nhỏ ở trang Profile cũng có thể kéo sập luôn trang Checkout. Đó là lúc mình hiểu rằng kiến trúc Monolith (nguyên khối) đã chạm giới hạn khi team tăng quy mô.
Micro-frontend ra đời như một vị cứu tinh, tương tự cách Microservices thay đổi cuộc chơi Backend. Thay vì ôm đồm một cục source code khổng lồ, mình chia nhỏ nó thành các module chạy độc lập. Mỗi team có thể tự do phát triển, test và deploy phần việc của mình mà không cần nhìn sắc mặt team khác.
Ba con đường triển khai Micro-frontend phổ biến
Trước khi gõ code, anh em cần chọn đúng công cụ. Không có giải pháp nào là vạn năng, chỉ có giải pháp phù hợp nhất với bài toán hiện tại.
1. iFrames: Kẻ tiền nhiệm già cỗi
Đây là cách đơn giản nhất. Mỗi micro-app là một trang web riêng nằm gọn trong thẻ <iframe>. Nó cách ly lỗi cực tốt nhưng lại là thảm họa về hiệu năng và SEO. Việc chia sẻ dữ liệu giữa các app qua postMessage cũng cực kỳ cồng kềnh.
2. NPM Packages: An toàn nhưng chậm chạp
Anh em đóng gói module thành thư viện rồi cài vào app chính. Cách này quản lý version rất chặt chẽ. Tuy nhiên, mỗi khi team A cập nhật một nút bấm, team B (Host app) lại phải cài lại và build lại toàn bộ. Nó không giải quyết được bài toán deploy độc lập mà chúng ta mong muốn.
3. Module Federation: Cuộc cách mạng từ Webpack 5
Đây là “chân ái” cho các dự án hiện đại. Module Federation cho phép ứng dụng tải code động từ một server khác ngay lúc runtime. Host app không cần build lại khi Remote app thay đổi. Trong dự án gần nhất, mình đã giảm được 85% thời gian build tổng thể nhờ cách này.
Cái giá của sự linh hoạt: Ưu và nhược điểm
Dù rất mạnh mẽ, Module Federation không phải là chiếc đũa thần. Sau vài lần “ăn hành” trên production, mình rút ra vài điểm lưu ý:
- Điểm cộng:
- Tối ưu tài nguyên: Nếu cả hai app cùng dùng React, trình duyệt chỉ tải đúng một bản duy nhất.
- Tự chủ tuyệt đối: Team Dashboard có thể deploy 10 lần một ngày mà không ảnh hưởng đến team Profile.
- Bundle size siêu gọn: Thay vì bắt user tải bundle 5MB, giờ họ chỉ tải 200KB cho mỗi module cần dùng.
- Điểm trừ:
- Config dễ tẩu hỏa nhập ma: Webpack config chỉ cần sai một dấu ngoặc là ứng dụng trắng trang.
- Xung đột CSS: Nếu không dùng CSS Modules hoặc Tailwind, class của app này rất dễ đè lên app kia.
- Quản lý State: Việc đồng bộ thông tin user giữa các app cần một chiến lược Event Bus hoặc Global State mỏng.
Bắt tay vào code: React + Webpack 5
Giả sử mình có hai app: Host (trang chính) và Remote (widget sản phẩm). Kinh nghiệm xương máu của mình là hãy viết Unit Test thật kỹ trước khi tách module. Nếu không, bug sẽ nhảy lung tung giữa các app và rất khó trace.
Bước 1: Cấu hình phía Remote App
Tại remote-app/webpack.config.js, mình dùng ModuleFederationPlugin để “show hàng” component.
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const deps = require("./package.json").dependencies;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "remoteApp",
filename: "remoteEntry.js",
exposes: {
"./ProductWidget": "./src/components/ProductWidget",
},
shared: {
...deps,
react: { singleton: true, requiredVersion: deps.react },
"react-dom": { singleton: true, requiredVersion: deps["react-dom"] },
},
}),
],
};
Lưu ý: singleton: true là bắt buộc để đảm bảo chỉ có một instance React duy nhất chạy ngầm, tránh lỗi xung đột Hook.
Bước 2: Cấu hình phía Host App
Tại host-app/webpack.config.js, mình đăng ký nhận code từ Remote.
new ModuleFederationPlugin({
name: "hostApp",
remotes: {
remoteApp: "remoteApp@http://localhost:3001/remoteEntry.js",
},
shared: {
...deps,
react: { singleton: true, requiredVersion: deps.react },
"react-dom": { singleton: true, requiredVersion: deps["react-dom"] },
},
}),
Bước 3: Nhúng component vào giao diện
Vì code được tải qua mạng, mình bắt buộc dùng React.lazy để ứng dụng không bị treo khi chờ tải.
import React, { Suspense } from "react";
const ProductWidget = React.lazy(() => import("remoteApp/ProductWidget"));
const App = () => (
<div>
<h1>Giao diện chính</h1>
<Suspense fallback={<p>Đang kết nối tới module sản phẩm...</p>}>
<ProductWidget />
</Suspense>
</div>
);
Bài học thực chiến khi lên Production
Làm chạy được dưới localhost mới chỉ là 50% chặng đường. Khi đưa lên môi trường thật, anh em cần chú ý:
- Dynamic URL: Đừng hardcode URL localhost. Hãy dùng biến môi trường để trỏ tới đúng domain Staging hoặc Production.
- Error Boundary: Luôn bọc Remote component trong một
Error Boundary. Nếu server chứa module sản phẩm bị sập, trang web chính của bạn vẫn phải hiển thị được phần còn lại thay vì chết đứng. - Versioning: Nên có chiến lược đánh version cho
remoteEntry.jsđể tránh lỗi cache trình duyệt khi bạn update code mới.
Chuyển sang Micro-frontend không chỉ là đổi công nghệ, mà là thay đổi tư duy làm việc nhóm. Nếu dự án của anh em đang phình to và các team bắt đầu dẫm chân lên nhau, Module Federation chính là lối thoát hiệu quả nhất hiện nay.
