Giới thiệu về micro‑frontend và nhu cầu thực tiễn
Trong những năm gần đây, kiến trúc micro‑service đã chứng tỏ sức mạnh của mình trong việc chia nhỏ hệ thống backend thành các dịch vụ độc lập, dễ bảo trì và mở rộng. Tương tự, micro‑frontend là xu hướng áp dụng cùng nguyên tắc này cho phần giao diện người dùng, cho phép các nhóm phát triển độc lập các phần UI, triển khai và cập nhật mà không ảnh hưởng lẫn nhau.
Đối với các dự án quy mô lớn, đặc biệt là các nền tảng thương mại điện tử, portal nội bộ hay SaaS đa tenant, việc chia nhỏ frontend thành các module riêng biệt giúp:
- Giảm độ phức tạp của codebase chung.
- Cho phép các team sử dụng công nghệ, framework và quy trình CI/CD riêng.
- Tăng tốc độ triển khai tính năng mới mà không cần đồng bộ toàn bộ ứng dụng.
Tuy nhiên, để thực hiện micro‑frontend một cách hiệu quả, chúng ta cần một cơ chế chia sẻ mã nguồn, tài nguyên và kiểu dữ liệu giữa các module. Webpack 5 Module Federation đã ra đời như một giải pháp tiêu chuẩn, và khi kết hợp với TypeScript, chúng ta có thể xây dựng một hệ thống mạnh mẽ, an toàn về kiểu dữ liệu và dễ bảo trì.
![]()
Lý thuyết Module Federation trong Webpack 5
Khái niệm và cách hoạt động
Module Federation cho phép một bundle (được gọi là host) tải các module được xuất khẩu từ một hoặc nhiều bundle khác (được gọi là remote) tại thời điểm chạy. Điều này nghĩa là:
- Mỗi remote có thể triển khai độc lập, thậm chí trên các server khác nhau.
- Host có thể quyết định tải remote nào dựa trên cấu hình runtime hoặc thậm chí dựa trên người dùng.
- Việc chia sẻ các thư viện chung (React, lodash,…) được thực hiện thông qua
sharedđể tránh tải lại nhiều lần.
Cấu hình cơ bản
Dưới đây là một ví dụ cấu hình webpack.config.js cho một remote module tên profile:
const path = require('path');
module.exports = {
entry: './src/index.ts',
output: {
filename: 'profile.[contenthash].js',
path: path.resolve(__dirname, 'dist'),
publicPath: 'auto',
clean: true,
},
mode: 'production',
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new (require('webpack')).container.ModuleFederationPlugin({
name: 'profile',
filename: 'remoteEntry.js',
exposes: {
'./UserProfile': './src/components/UserProfile',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};Với cấu hình trên, remote profile sẽ xuất ra một file remoteEntry.js chứa thông tin về các module được expose. Host có thể import chúng như sau:
import('profile/UserProfile').then(({ default: UserProfile }) => {
// Render hoặc sử dụng component
});Áp dụng TypeScript trong micro‑frontend
Định nghĩa giao diện chia sẻ (Shared Types)
Để tránh lỗi kiểu dữ liệu khi host và remote giao tiếp, chúng ta nên tạo một package shared-types chứa các interface, type và enum chung. Package này được publish lên npm riêng tư hoặc giữ trong monorepo.
Ví dụ, một interface mô tả dữ liệu người dùng:
export interface UserDto {
id: string;
name: string;
email: string;
avatarUrl?: string;
}
export type UserRole = 'admin' | 'editor' | 'viewer';Remote profile và host đều import { UserDto } from 'shared-types'; để đảm bảo cùng một định nghĩa.
Quản lý phiên bản và tương thích
Khi các team phát triển độc lập, việc cập nhật shared-types là một thách thức. Một số thực tiễn:
- Sử dụng semantic versioning cho package
shared-types. Khi thay đổi giao diện, tăng major version. - Trong
ModuleFederationPlugin, khai báorequiredVersioncho các shared modules, giúp Webpack phát hiện xung đột. - Triển khai
peerDependenciestrong các remote để bắt buộc host cung cấp đúng phiên bản.
Thực tiễn triển khai: ví dụ dự án đa team
Cấu trúc thư mục
Dưới đây là một cấu trúc monorepo sử dụng pnpm workspaces:
my-microfrontend/ ├─ apps/ │ ├─ host/ # Ứng dụng chính │ ├─ profile/ # Remote 1: trang hồ sơ người dùng │ └─ dashboard/ # Remote 2: bảng điều khiển ├─ packages/ │ ├─ shared-types/ # Giao diện, enum chung │ └─ ui-components/ # Component library chung └─ pnpm-workspace.yaml
Chi tiết các bước triển khai
- Khởi tạo monorepo và cài đặt
webpack,ts-loader,react,react-domcho mỗi package. - Viết
webpack.config.jscho mỗi remote như ví dụ ở trên, nhớ expose các component và khai báosharedchoreact,react-domvàshared-types. - Trong
host, cấu hìnhModuleFederationPluginvớiremotes:new ModuleFederationPlugin({ name: 'host', remotes: { profile: 'profile@http://localhost:3001/remoteEntry.js', dashboard: 'dashboard@http://localhost:3002/remoteEntry.js', }, shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, 'shared-types': { singleton: true, requiredVersion: '^1.0.0' }, }, }); - Trong code host, load remote component một cách lazy để tối ưu thời gian tải:
const RemoteUserProfile = React.lazy(() => import('profile/UserProfile')); function App() { return ( Loading...}> ); } - Chạy từng remote trên các cổng khác nhau (3001, 3002) và host trên cổng 3000. Kiểm tra console để xác nhận các module được tải thành công.
Hiệu năng và tối ưu
Lazy loading và chunking
Webpack tự động tách các remote thành các chunk dựa trên exposes. Khi host yêu cầu một module, chỉ chunk chứa module đó được tải. Để tối ưu hơn, chúng ta có thể:
- Sử dụng
dynamic import()kết hợp vớiReact.lazyđể tải khi cần. - Đặt
maxInitialRequestsvàsplitChunkstrongoptimizationđể kiểm soát kích thước bundle.
Cache và versioning
Mỗi bundle được đặt tên bằng [contenthash]. Khi nội dung thay đổi, hash mới sẽ được tạo, giúp CDN và trình duyệt cache hiệu quả. Đối với remote, chúng ta có thể thêm query string version vào URL, ví dụ profile@http://localhost:3001/remoteEntry.js?v=1.2.3, để buộc host tải lại khi có bản cập nhật.
Kiểm thử và CI/CD cho micro‑frontend
Unit test với Jest và ts-jest
Với TypeScript, cấu hình jest.config.js thường như sau:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapper: {
'^shared-types$': '/../packages/shared-types/src',
},
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
};Một test mẫu cho component UserProfile:
import React from 'react';
import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';
import { UserDto } from 'shared-types';
test('renders user name', () => {
const mockUser: UserDto = { id: '1', name: 'Nguyen Van A', email: '[email protected]' };
render(<UserProfile user={mockUser} />);
expect(screen.getByText(/Nguyen Van A/)).toBeInTheDocument();
});Integration test với Cypress
Cypress cho phép kiểm tra toàn bộ luồng tải remote và render UI. Một kịch bản đơn giản:
describe('Micro‑frontend integration', () => {
it('loads remote UserProfile component', () => {
cy.visit('http://localhost:3000');
cy.contains('Loading...').should('exist');
cy.get('[data-test="user-profile"]').should('be.visible');
});
});Pipeline CI/CD mẫu
Sử dụng GitHub Actions để build và deploy từng remote và host. Dưới đây là một workflow ngắn gọn:
name: CI/CD Micro‑frontend
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
app: [host, profile, dashboard]
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build ${{ matrix.app }}
run: pnpm --filter ${{ matrix.app }} run build
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.app }}-dist
path: apps/${{ matrix.app }}/dist
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
path: ./artifacts
- name: Deploy to server
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
run: |
rsync -avz -e "ssh -i $SSH_KEY" ./artifacts/host/dist/ user@server:/var/www/host/
rsync -avz -e "ssh -i $SSH_KEY" ./artifacts/profile/dist/ user@server:/var/www/profile/
rsync -avz -e "ssh -i $SSH_KEY" ./artifacts/dashboard/dist/ user@server:/var/www/dashboard/Kết luận và hướng học tiếp
Micro‑frontend kết hợp với TypeScript và Module Federation mang lại một kiến trúc linh hoạt, cho phép các team độc lập phát triển, kiểm thử và triển khai mà không gây xung đột. Việc định nghĩa shared-types giúp duy trì tính nhất quán về kiểu dữ liệu, giảm lỗi runtime và tăng tốc độ phát triển. Đồng thời, áp dụng các chiến lược lazy loading, cache versioning và CI/CD tự động sẽ tối ưu hiệu năng và độ tin cậy của hệ thống.
Để nắm vững các khái niệm trên, đồng thời nâng cao kỹ năng viết TypeScript chuyên nghiệp, Tham khảo khóa học "Lập trình TypeScript từ cơ bản đến nâng cao" tại đây.







