Trong thế giới phát triển phần mềm không ngừng biến đổi, việc xây dựng và bảo trì các ứng dụng web ngày càng trở nên phức tạp. Các ứng dụng nguyên khối (monolithic) truyền thống, dù có ưu điểm trong giai đoạn đầu, thường trở nên cồng kềnh, khó mở rộng và khó quản lý khi quy mô dự án lớn dần. Để giải quyết thách thức này, một kiến trúc mới đã ra đời, lấy cảm hứng từ thành công của microservices ở phía backend - đó chính là Micro-Frontends.
Vậy Micro-Frontends là gì và tại sao nó lại được coi là tương lai của phát triển frontend? Hãy cùng khám phá chi tiết trong bài viết này.
Micro-Frontends - Làn gió mới cho kiến trúc hiện đại 🚀
Hãy tưởng tượng một trang web lớn như Amazon hay Netflix. Thay vì xây dựng toàn bộ giao diện người dùng như một thực thể duy nhất, kiến trúc Micro-Frontends đề xuất một cách tiếp cận khác: chia nhỏ giao diện người dùng thành các phần nhỏ hơn, độc lập và có thể quản lý được.
Mỗi phần này, được gọi là một "micro-frontend", sẽ chịu trách nhiệm cho một tính năng hoặc một khu vực cụ thể của trang web (ví dụ: thanh tìm kiếm, giỏ hàng, đề xuất sản phẩm). Chúng hoạt động giống như các "ứng dụng mini" được phát triển, triển khai và quản lý bởi các nhóm khác nhau một cách độc lập. Sau đó, tất cả các micro-frontend này được tập hợp lại để tạo thành một trang web hoàn chỉnh, liền mạch cho người dùng cuối.
Nói một cách đơn giản, Micro-Frontends là "Microservices cho phía Frontend". Nó áp dụng triết lý phân tách và độc lập của microservices vào thế giới giao diện người dùng.
Tại sao nên "chia để trị"? Lợi ích vượt trội của Micro-Frontends
Việc áp dụng kiến trúc này không chỉ là một xu hướng công nghệ mà còn mang lại những lợi ích chiến lược to lớn cho các tổ chức và đội ngũ phát triển.
1. Độc lập & Tự chủ (Independent Teams)
Đây là lợi ích cốt lõi. Mỗi nhóm có thể toàn quyền sở hữu một phần của sản phẩm, từ cơ sở dữ liệu đến giao diện người dùng. Họ có thể tự do lựa chọn công nghệ (React, Angular, Vue.js, v.v.), kiến trúc và quy trình phát triển riêng mà không ảnh hưởng đến các nhóm khác. Điều này thúc đẩy tốc độ và sự đổi mới.
2. Phát triển & Triển khai độc lập (Independent Deployment)
Thay vì phải triển khai lại toàn bộ ứng dụng mỗi khi có một thay đổi nhỏ, với Micro-Frontends, bạn chỉ cần triển khai lại "ứng dụng mini" mà bạn đã sửa đổi. Quá trình này nhanh hơn, giảm thiểu rủi ro và cho phép cập nhật tính năng đến tay người dùng một cách nhanh chóng.
3. Codebase nhỏ hơn & Dễ quản lý (Smaller, More Cohesive Codebases)
Mỗi micro-frontend có một codebase riêng, nhỏ hơn và tập trung vào một mục tiêu kinh doanh cụ thể. Điều này giúp mã nguồn trở nên dễ hiểu, dễ phát triển, dễ kiểm thử và dễ bảo trì hơn rất nhiều so với một codebase nguyên khối khổng lồ.
4. Khả năng mở rộng & Nâng cấp dễ dàng (Scalability & Easier Upgrades)
Việc nâng cấp hoặc thay thế một phần của ứng dụng trở nên đơn giản hơn bao giờ hết. Bạn có thể viết lại một micro-frontend bằng công nghệ mới mà không cần phải "đập đi xây lại" toàn bộ hệ thống. Điều này giúp ứng dụng của bạn luôn được cập nhật với những công nghệ hiện đại nhất.
5. Tăng cường tính bền vững (Increased Resilience)
Nếu một micro-frontend gặp lỗi, nó sẽ chỉ ảnh hưởng đến một phần nhỏ của ứng dụng, thay vì làm sập toàn bộ trang web. Điều này giúp tăng cường độ tin cậy và trải nghiệm người dùng.
Các phương pháp tích hợp Micro-Frontends phổ biến
Vậy làm thế nào để kết hợp các "mảnh ghép" micro-frontend này lại với nhau thành một thể thống nhất? Có nhiều kỹ thuật khác nhau, mỗi kỹ thuật có ưu và nhược điểm riêng.
- Server-Side Integration: Các micro-frontend được render trên máy chủ và ghép lại thành một trang HTML hoàn chỉnh trước khi gửi đến trình duyệt. Đây là một cách tiếp cận tốt cho SEO và hiệu suất tải trang ban đầu.
- Client-Side Integration with iFrames: Sử dụng
<iframe>
là cách đơn giản nhất để nhúng một ứng dụng vào một ứng dụng khác. Nó cung cấp sự cô lập tuyệt vời về CSS và JavaScript nhưng có thể tạo ra các vấn đề về SEO, khả năng truy cập và giao tiếp giữa các thành phần. - Client-Side Integration with JavaScript: Mỗi micro-frontend được đóng gói như một file JavaScript. Ứng dụng "vỏ" (container) sẽ tải và gắn (mount) các micro-frontend này vào các vị trí thích hợp trên trang. Đây là cách tiếp cận linh hoạt và phổ biến nhất hiện nay.
- Module Federation (Webpack 5): Đây là một tính năng đột phá của Webpack 5, cho phép các ứng dụng JavaScript khác nhau có thể chia sẻ và sử dụng mã nguồn của nhau một cách linh hoạt ngay tại thời điểm chạy (runtime). Nó được xem là một trong những giải pháp tối ưu nhất cho Micro-Frontends hiện nay.
Khi nào nên (và không nên) sử dụng Micro-Frontends?
Mặc dù có nhiều lợi ích, Micro-Frontends không phải là "viên đạn bạc" cho mọi dự án. Việc áp dụng kiến trúc này cũng đi kèm với những thách thức riêng.
Hãy cân nhắc sử dụng Micro-Frontends khi:
- Bạn có một ứng dụng web lớn và phức tạp.
- Bạn có nhiều đội ngũ phát triển làm việc song song.
- Bạn muốn tăng tốc độ phát triển và triển khai.
- Ứng dụng của bạn cần được nâng cấp và bảo trì thường xuyên.
Bạn nên cẩn trọng nếu:
- Dự án của bạn nhỏ và đơn giản.
- Đội ngũ phát triển của bạn còn nhỏ.
- Bạn chưa có kinh nghiệm với các hệ thống phân tán.
Sự phức tạp trong việc quản lý, triển khai và đảm bảo trải nghiệm người dùng nhất quán giữa các micro-frontend là những thách thức cần được xem xét kỹ lưỡng.
Ví dụ: Micro-Frontends với React & Module Federation
Trước khi kết thúc bài viết, để dễ hình dung hơn chúng ta hãy cùng xem một ví dụ trong thực tế. Hãy tưởng tượng chúng ta đang xây dựng một trang thương mại điện tử đơn giản. Thay vì làm một ứng dụng React duy nhất, chúng ta sẽ chia nó thành:
- Host App: Là ứng dụng chính, đóng vai trò "cái khung" chứa các phần khác. Nó sẽ quản lý layout chung, thanh điều hướng (navigation). Tạm gọi là
container
. - Micro-Frontend App: Một ứng dụng React độc lập, chịu trách nhiệm hiển thị danh sách sản phẩm. Tạm gọi là
products
.
Mục tiêu của chúng ta là ứng dụng container
có thể hiển thị component ProductListPage
từ ứng dụng products
mà không cần phải cài đặt products
như một thư viện phụ thuộc (dependency).
Bước 1: Cấu hình products (Micro-Frontend App)
Đây là ứng dụng sẽ "chia sẻ" component của nó.
-
Cài đặt Webpack:
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
-
Tạo một component React đơn giản:
// src/ProductListPage.js import React from 'react' const ProductListPage = () => ( <div> <h2>Đây là trang danh sách sản phẩm MFE</h2> <ul> <li>Sản phẩm A</li> <li>Sản phẩm B</li> <li>Sản phẩm C</li> </ul> </div> ) export default ProductListPage
-
Cấu hình
webpack.config.js
để "chia sẻ" component:const HtmlWebpackPlugin = require('html-webpack-plugin') const { ModuleFederationPlugin } = require('webpack').container const deps = require('./package.json').dependencies module.exports = { // ... các cấu hình khác mode: 'development', devServer: { port: 3001, // Chạy MFE này ở cổng 3001 }, plugins: [ new ModuleFederationPlugin({ name: 'products', // Tên duy nhất của MFE filename: 'remoteEntry.js', // Tên file để ứng dụng khác gọi vào exposes: { // "Chia sẻ" component ProductListPage './ProductListPage': './src/ProductListPage', }, shared: { // Chia sẻ các thư viện chung ...deps, react: { singleton: true, requiredVersion: deps.react }, 'react-dom': { singleton: true, requiredVersion: deps['react-dom'], }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], }
Điểm mấu chốt ở đây là ModuleFederationPlugin
:
name: 'products'
: Đặt tên cho micro-frontend này.filename: 'remoteEntry.js'
: Tên file mà ứng dụngcontainer
sẽ tìm để biếtproducts
có những gì.exposes: {'./ProductListPage': ...}
: "Public" componentProductListPage
ra ngoài với tên là./ProductListPage
.
Bước 2: Cấu hình container (Host App)
Đây là ứng dụng sẽ "tiêu thụ" component từ products
.
-
Cấu hình
webpack.config.js
để "nhận" component:const HtmlWebpackPlugin = require('html-webpack-plugin') const { ModuleFederationPlugin } = require('webpack').container const deps = require('./package.json').dependencies module.exports = { // ... các cấu hình khác mode: 'development', devServer: { port: 3000, // Chạy Host App ở cổng 3000 }, plugins: [ new ModuleFederationPlugin({ name: 'container', remotes: { // Định nghĩa nguồn remote products: 'products@http://localhost:3001/remoteEntry.js', }, shared: { // Đảm bảo dùng chung thư viện ...deps, react: { singleton: true, requiredVersion: deps.react }, 'react-dom': { singleton: true, requiredVersion: deps['react-dom'], }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], }
Điểm mấu chốt:
remotes: { products: '...' }
: Khai báo rằngcontainer
muốn sử dụng một micro-frontend tên làproducts
, và nó có thể được tìm thấy tạihttp://localhost:3001/remoteEntry.js
.
-
Sử dụng component được chia sẻ trong
container
:Bây giờ, bạn có thể
import
component từproducts
một cách linh hoạt như thể nó là một phần của ứng dụngcontainer
.// src/App.js import React, { Suspense } from 'react' // Tải động component từ remote 'products' const RemoteProductListPage = React.lazy( () => import('products/ProductListPage'), ) const App = () => { return ( <div> <h1>Đây là ứng dụng Vỏ (Container)</h1> <nav> <a href="/">Trang Chủ</a> | <a href="/products">Sản phẩm</a> </nav> <hr /> <h2>Nội dung của Micro-Frontend sẽ hiển thị bên dưới:</h2> <Suspense fallback={<div>Đang tải sản phẩm...</div>}> <RemoteProductListPage /> </Suspense> </div> ) } export default App
Điểm mấu chốt:
React.lazy(() => import('products/ProductListPage'))
: Đây là cú pháp đặc biệt.products
là tên remote chúng ta đã định nghĩa trong Webpack, vàProductListPage
là component đượcexposes
ra.<Suspense>
: Vì component được tải động qua mạng, chúng ta cần dùngSuspense
để hiển thị một thông báo "đang tải" trong khi chờ đợi.
Thành quả của các bước
Bây giờ, bạn hãy chạy cả hai ứng dụng cùng lúc:
- Trong thư mục
products
, chạynpm start
(sẽ chạy ở cổng 3001). - Trong thư mục
container
, chạynpm start
(sẽ chạy ở cổng 3000).
Khi bạn truy cập http://localhost:3000
, bạn sẽ thấy giao diện của ứng dụng container
. Bên trong nó, danh sách sản phẩm từ ứng dụng products
(đang chạy ở cổng 3001) được tải và hiển thị một cách liền mạch.
Điều kỳ diệu là đội ngũ phát triển products
có thể cập nhật, sửa lỗi và triển khai lại ứng dụng của họ mà không cần đội container
phải làm bất cứ điều gì. Lần tới khi người dùng tải lại trang, họ sẽ tự động nhận được phiên bản mới nhất của micro-frontend products
.
Đây chính là sức mạnh của sự độc lập và linh hoạt mà kiến trúc Micro-Frontends mang lại.
Kết luận: Hướng tới tương lai của Frontend
Kiến trúc Micro-Frontends đại diện cho một sự thay đổi tư duy quan trọng trong cách chúng ta xây dựng các ứng dụng web. Bằng cách "chia để trị", nó cho phép các tổ chức xây dựng những sản phẩm lớn, phức tạp một cách linh hoạt, bền vững và hiệu quả hơn.
Dù không phải là giải pháp cho tất cả mọi bài toán, nhưng với sự phát triển của các công cụ hỗ trợ như Module Federation, Micro-Frontends chắc chắn sẽ tiếp tục là một xu hướng chủ đạo, định hình tương lai của kiến trúc frontend.