Giới thiệu

Trong những năm gần đây, kiến trúc micro‑frontend đã trở thành một xu hướng mạnh mẽ giúp các đội phát triển lớn chia nhỏ ứng dụng front‑end thành các phần độc lập, có thể triển khai và nâng cấp riêng biệt. Khi áp dụng vào môi trường React, Next.js nổi bật nhờ khả năng server‑side rendering (SSR), static site generation (SSG) và hệ sinh thái plugin phong phú. Bài viết này sẽ đi sâu vào cách kết hợp Next.js với Module Federation của Webpack 5 để xây dựng một hệ thống micro‑frontend thực tế, đồng thời đưa ra các chiến lược tối ưu hiệu năng, quản lý phụ thuộc và quy trình CI/CD.

Lý do chọn Next.js cho micro‑frontend

Next.js cung cấp các tính năng sau, khiến nó trở thành nền tảng lý tưởng cho micro‑frontend:

  • Routing tự động: Thư mục pages tạo ra các route mà không cần cấu hình.
  • Hỗ trợ SSR/SSG: Giúp cải thiện SEO và thời gian tải trang ban đầu.
  • API Routes: Cho phép triển khai logic back‑end nhẹ ngay trong cùng dự án.
  • Hệ sinh thái plugin: Webpack 5 được tích hợp sẵn, cho phép bật ModuleFederationPlugin mà không cần cấu hình phức tạp.

Những ưu điểm này giúp chúng ta tập trung vào việc tách các phần UI thành các remote (các micro‑frontend) và một host chịu trách nhiệm hợp nhất chúng.

Module Federation trong Webpack 5

Module Federation cho phép một bundle (remote) xuất khẩu các module và một bundle khác (host) nhập khẩu chúng tại thời điểm chạy. Điều này tạo ra khả năng tải động các phần của ứng dụng mà không cần rebuild toàn bộ. Các khái niệm chính:

  • Remote: Ứng dụng cung cấp các module qua exposes.
  • Host: Ứng dụng tiêu thụ các module qua remotes.
  • Shared: Các thư viện chung (React, React‑DOM, …) được chia sẻ để tránh tải lại.

Next.js mặc định sử dụng Webpack 5, vì vậy chúng ta chỉ cần mở next.config.js và chèn ModuleFederationPlugin vào.

Cấu hình Next.js làm host và remote

Thiết lập remote application

Giả sử chúng ta có một micro‑frontend Dashboard sẽ được triển khai độc lập. Đầu tiên tạo dự án Next.js:

npx create-next-app@latest dashboard-remote --typescript
cd dashboard-remote

Tiếp theo, cài đặt Webpack 5 (đã có sẵn) và tạo file next.config.js:

const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  webpack(config, { isServer }) {
    if (!isServer) {
      config.plugins.push(
        new ModuleFederationPlugin({
          name: 'dashboard',
          filename: 'remoteEntry.js',
          exposes: {
            './Header': './components/Header.tsx',
            './Stats': './components/Stats.tsx',
          },
          shared: {
            react: { singleton: true, requiredVersion: require('react/package.json').version },
            'react-dom': { singleton: true, requiredVersion: require('react-dom/package.json').version },
          },
        })
      );
    }
    return config;
  },
};

Trong components/Header.tsxcomponents/Stats.tsx viết các component đơn giản:

import React from 'react';

export const Header: React.FC = () => (
  <header style={{ background: '#0070f3', color: '#fff', padding: '1rem' }}>
    <h1>Dashboard Remote</h1>
  </header>
);
import React from 'react';

export const Stats: React.FC = () => (
  <section style={{ padding: '1rem' }}>
    <h2>Statistics</h2>
    <p>Số liệu mẫu: 12345</p>
  </section>
);

Chạy npm run dev để kiểm tra, server sẽ tạo file remoteEntry.js trong thư mục .next/static/chunks. Đảm bảo cấu hình output.publicPath trong next.config.js để file này có thể truy cập công khai (ví dụ /remoteEntry.js).

Thiết lập host application

Tạo một dự án Next.js mới làm host:

npx create-next-app@latest host-app --typescript
cd host-app

Thêm cấu hình Module Federation để tiêu thụ remote:

const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  webpack(config, { isServer }) {
    if (!isServer) {
      config.plugins.push(
        new ModuleFederationPlugin({
          name: 'host',
          remotes: {
            dashboard: 'dashboard@http://localhost:3001/_next/static/chunks/remoteEntry.js',
          },
          shared: {
            react: { singleton: true, requiredVersion: require('react/package.json').version },
            'react-dom': { singleton: true, requiredVersion: require('react-dom/package.json').version },
          },
        })
      );
    }
    return config;
  },
};

Trong pages/index.tsx import các module từ remote bằng dynamic của Next.js để thực hiện lazy loading:

import dynamic from 'next/dynamic';
import React from 'react';

const RemoteHeader = dynamic(() => import('dashboard/Header'), { ssr: false });
const RemoteStats = dynamic(() => import('dashboard/Stats'), { ssr: false });

const Home: React.FC = () => (
  <div>
    <RemoteHeader />
    <main style={{ margin: '2rem' }}>
      <h2>Welcome to Host Application</h2>
      <RemoteStats />
    </main>
  </div>
);

export default Home;

Khởi động remote trên cổng 3001 (có thể thay đổi package.json scripts để chạy next dev -p 3001), sau đó khởi động host trên cổng 3000. Khi truy cập http://localhost:3000, bạn sẽ thấy header và stats được tải từ remote mà không cần build lại host.

Quản lý chia sẻ phụ thuộc (shared modules)

Việc chia sẻ các thư viện như reactreact-dom là bắt buộc để tránh việc React được tải hai lần, gây lỗi Invalid hook call. Module Federation cho phép định nghĩa singleton: truerequiredVersion. Ngoài ra, nếu dự án sử dụng UI library (ví dụ antd), chúng ta có thể thêm:

shared: {
  antd: { singleton: true, eager: true },
  // các thư viện khác
}

Đối với các thư viện lớn, việc bật eager: true giúp chúng được tải ngay trong bundle host, giảm thời gian khởi tạo. Tuy nhiên, cần cân nhắc kích thước bundle.

Chiến lược tải động và tối ưu hiệu năng

Next.js cung cấp dynamic để lazy load các component. Khi kết hợp với Module Federation, chúng ta có thể:

  • Prefetch remote entry trong _app.tsx bằng cách chèn <link rel="prefetch" href="/remoteEntry.js" /> để trình duyệt tải trước.
  • Chunk splitting tự động: mỗi remote component sẽ trở thành một chunk riêng, giảm thời gian tải ban đầu.
  • Cache control: cấu hình Cache‑Control headers trên CDN để remote entry được cache lâu, giảm tải mạng.

Dưới đây là ví dụ cấu hình next.config.js để bật assetPrefix khi deploy trên CDN:

module.exports = {
  assetPrefix: process.env.CDN_URL || '',
  // các cấu hình khác
};

Triển khai CI/CD cho micro‑frontend Next.js

Với kiến trúc micro‑frontend, mỗi remote và host có thể được triển khai độc lập. Dưới đây là một pipeline GitHub Actions mẫu cho remote:

name: Deploy Remote Dashboard
on:
  push:
    branches: [main]
    paths:
      - 'dashboard-remote/**'
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      - name: Install dependencies
        run: npm ci
        working-directory: dashboard-remote
      - name: Build
        run: npm run build
        working-directory: dashboard-remote
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v20
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID_DASHBOARD }}
          working-directory: dashboard-remote

Host application có pipeline tương tự, chỉ thay đổi working-directoryproject-id. Khi một remote được cập nhật, host sẽ tự động nhận phiên bản mới thông qua remoteEntry.js được cache‑busting (sử dụng query string ?v=timestamp).

Kiểm thử tích hợp

Đối với micro‑frontend, việc kiểm thử tích hợp giữa host và remote là quan trọng. Cypress cung cấp khả năng chạy test trên môi trường thực tế. Một test mẫu:

describe('Micro‑frontend integration', () => {
  it('should render remote Header and Stats', () => {
    cy.visit('http://localhost:3000');
    cy.contains('Dashboard Remote').should('be.visible');
    cy.contains('Statistics').should('be.visible');
  });
});

Chạy Cypress trong CI bằng npm run cypress:run và báo cáo kết quả trong GitHub Actions.

Kết luận

Việc kết hợp Next.js với Module Federation mang lại một giải pháp mạnh mẽ cho kiến trúc micro‑frontend, giúp các team độc lập phát triển, triển khai và mở rộng các phần UI mà không gây xung đột phiên bản. Bằng cách quản lý chia sẻ thư viện, tối ưu tải động và thiết lập quy trình CI/CD tự động, chúng ta có thể đạt được hiệu năng cao, khả năng mở rộng linh hoạt và giảm thiểu rủi ro khi cập nhật. Nếu muốn nâng cao kỹ năng và đi sâu hơn vào việc xây dựng ứng dụng Next.js thực tế, Tham khảo khóa học "Lập trình Front-End với NextJS + TypeScript" tại đây.