乐闻世界logo
搜索文章和话题

How to combine Next.js with micro-frontend architecture?

2月17日 22:52

The combination of Next.js and micro-frontend architecture is an important solution for building large-scale enterprise applications. Micro-frontend architecture allows large applications to be decomposed into smaller, simpler blocks that can be developed and deployed independently by different teams.

Micro-Frontend Architecture Overview

1. Core Concepts of Micro-Frontends

Micro-frontends is an architectural style that decomposes frontend applications into smaller, simpler blocks that can be developed and deployed independently by different teams.

Core Benefits:

  • Independent development and deployment
  • Technology stack agnostic
  • Incremental upgrades
  • Team autonomy
  • Code isolation

Next.js Micro-Frontend Implementation Solutions

1. Module Federation

javascript
// next.config.js - Main app configuration const NextFederationPlugin = require('@module-federation/nextjs-mf'); module.exports = { webpack(config, options) { const { isServer } = options; config.plugins.push( new NextFederationPlugin({ name: 'main_app', filename: 'static/chunks/remoteEntry.js', remotes: { productApp: 'product_app@https://product.example.com/_next/static/chunks/remoteEntry.js', cartApp: 'cart_app@https://cart.example.com/_next/static/chunks/remoteEntry.js', userApp: 'user_app@https://user.example.com/_next/static/chunks/remoteEntry.js', }, shared: { react: { singleton: true, requiredVersion: false, }, 'react-dom': { singleton: true, requiredVersion: false, }, next: { singleton: true, requiredVersion: false, }, }, extraOptions: { automaticAsyncBoundary: true, }, }) ); return config; }, }; // next.config.js - Sub-app configuration (productApp) const NextFederationPlugin = require('@module-federation/nextjs-mf'); module.exports = { webpack(config, options) { const { isServer } = options; config.plugins.push( new NextFederationPlugin({ name: 'product_app', filename: 'static/chunks/remoteEntry.js', exposes: { './ProductList': './components/ProductList', './ProductDetail': './components/ProductDetail', './ProductSearch': './components/ProductSearch', }, shared: { react: { singleton: true, requiredVersion: false, }, 'react-dom': { singleton: true, requiredVersion: false, }, next: { singleton: true, requiredVersion: false, }, }, }) ); return config; }, }; // Using remote components in main app // app/products/page.js 'use client'; import dynamic from 'next/dynamic'; const ProductList = dynamic(() => import('productApp/ProductList'), { loading: () => <div>Loading products...</div>, ssr: false, }); const ProductSearch = dynamic(() => import('productApp/ProductSearch'), { loading: () => <div>Loading search...</div>, ssr: false, }); export default function ProductsPage() { return ( <div> <h1>Products</h1> <ProductSearch /> <ProductList /> </div> ); }

2. iframe Solution

javascript
// components/IframeWrapper.js 'use client'; import { useState, useEffect, useRef } from 'react'; export default function IframeWrapper({ src, title, onMessage }) { const iframeRef = useRef(null); const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { const iframe = iframeRef.current; const handleMessage = (event) => { // Verify message origin if (event.origin !== new URL(src).origin) return; onMessage?.(event.data); }; window.addEventListener('message', handleMessage); return () => { window.removeEventListener('message', handleMessage); }; }, [src, onMessage]); const handleLoad = () => { setIsLoaded(true); }; const sendMessage = (message) => { if (iframeRef.current && iframeRef.current.contentWindow) { iframeRef.current.contentWindow.postMessage(message, new URL(src).origin); } }; return ( <div className="iframe-container"> {!isLoaded && <div className="loading">Loading...</div>} <iframe ref={iframeRef} src={src} title={title} onLoad={handleLoad} style={{ border: 'none', width: '100%', height: '100%', display: isLoaded ? 'block' : 'none' }} allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" /> </div> ); } // Using iframe to integrate sub-app // app/dashboard/page.js 'use client'; import IframeWrapper from '@/components/IframeWrapper'; export default function DashboardPage() { const handleMessage = (data) => { console.log('Message from iframe:', data); if (data.type === 'NAVIGATION') { // Handle navigation event } else if (data.type === 'AUTH') { // Handle authentication event } }; return ( <div className="dashboard"> <nav> <a href="/">Home</a> <a href="/dashboard">Dashboard</a> </nav> <main> <IframeWrapper src="https://cart.example.com" title="Shopping Cart" onMessage={handleMessage} /> </main> </div> ); }

3. Web Components Solution

javascript
// components/MicroFrontendWrapper.js 'use client'; import { useEffect, useRef } from 'react'; export default function MicroFrontendWrapper({ name, host, history, onNavigate, onUnmount }) { const ref = useRef(null); useEffect(() => { const scriptId = `micro-frontend-script-${name}`; const renderMicroFrontend = () => { window[name] = { mount: (container, history) => { console.log(`Mounting ${name}`); // Call sub-app's mount method }, unmount: (container) => { console.log(`Unmounting ${name}`); onUnmount?.(); }, }; if (window[name] && window[name].mount) { window[name].mount(ref.current, history); } }; const loadScript = () => { if (document.getElementById(scriptId)) { renderMicroFrontend(); return; } const script = document.createElement('script'); script.id = scriptId; script.src = `${host}/main.js`; script.onload = renderMicroFrontend; document.head.appendChild(script); }; loadScript(); return () => { if (window[name] && window[name].unmount) { window[name].unmount(ref.current); } }; }, [name, host, history, onUnmount]); return <div ref={ref} />; } // Using Web Components integration // app/micro/page.js 'use client'; import MicroFrontendWrapper from '@/components/MicroFrontendWrapper'; export default function MicroFrontendPage() { const handleNavigate = (location) => { console.log('Navigate to:', location); window.history.pushState({}, '', location); }; const handleUnmount = () => { console.log('Micro frontend unmounted'); }; return ( <div> <h1>Micro Frontend Integration</h1> <MicroFrontendWrapper name="productApp" host="https://product.example.com" history={window.history} onNavigate={handleNavigate} onUnmount={handleUnmount} /> </div> ); }

4. Monorepo Solution

javascript
// Using Turborepo to manage monorepo // turbo.json { "$schema": "https://turbo.build/schema.json", "globalDependencies": ["**/.env.*local"], "pipeline": { "build": { "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, "dev": { "cache": false, "persistent": true }, "lint": { "dependsOn": ["^lint"] }, "test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] } } } // pnpm-workspace.yaml packages: - 'apps/*' - 'packages/*' // Directory structure // apps/ // main-app/ # Main app // product-app/ # Product sub-app // cart-app/ # Cart sub-app // user-app/ # User sub-app // packages/ // ui/ # Shared UI components // utils/ # Shared utility functions // types/ # Shared type definitions // config/ # Shared configuration // apps/main-app/package.json { "name": "main-app", "dependencies": { "next": "^14.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", "@workspace/ui": "workspace:*", "@workspace/utils": "workspace:*" } } // apps/product-app/package.json { "name": "product-app", "dependencies": { "next": "^14.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", "@workspace/ui": "workspace:*", "@workspace/utils": "workspace:*" } }

State Management and Communication

1. Cross-Application State Management

javascript
// packages/shared-state/src/store.js import { createStore } from 'zustand/vanilla'; export const createSharedStore = (initialState) => { return createStore((set, get) => ({ ...initialState, update: (key, value) => set({ [key]: value }), reset: () => set(initialState), })); }; // Create shared state export const userStore = createSharedStore({ user: null, isAuthenticated: false, cart: [], }); export const productStore = createSharedStore({ products: [], filters: {}, sortBy: 'name', }); // Using in main app // app/layout.js 'use client'; import { userStore } from '@workspace/shared-state'; import { useEffect } from 'react'; export default function RootLayout({ children }) { useEffect(() => { // Listen to user state changes const unsubscribe = userStore.subscribe((state) => { console.log('User state changed:', state); // Notify other apps window.postMessage({ type: 'USER_STATE_CHANGE', state }, '*'); }); return () => unsubscribe(); }, []); return ( <html lang="en"> <body>{children}</body> </html> ); } // Using in sub-app // product-app/components/UserInfo.js 'use client'; import { userStore } from '@workspace/shared-state'; import { useEffect, useState } from 'react'; export default function UserInfo() { const [user, setUser] = useState(null); useEffect(() => { // Subscribe to user state const unsubscribe = userStore.subscribe((state) => { setUser(state.user); }); return () => unsubscribe(); }, []); if (!user) { return <div>Please login</div>; } return <div>Welcome, {user.name}</div>; }

2. Event Bus Communication

javascript
// packages/event-bus/src/index.js class EventBus { constructor() { this.events = {}; } on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); } off(event, callback) { if (!this.events[event]) return; this.events[event] = this.events[event].filter(cb => cb !== callback); } emit(event, data) { if (!this.events[event]) return; this.events[event].forEach(callback => { callback(data); }); } once(event, callback) { const onceCallback = (data) => { callback(data); this.off(event, onceCallback); }; this.on(event, onceCallback); } } export const eventBus = new EventBus(); // Define event types export const Events = { USER_LOGIN: 'USER_LOGIN', USER_LOGOUT: 'USER_LOGOUT', CART_UPDATE: 'CART_UPDATE', PRODUCT_ADD: 'PRODUCT_ADD', NAVIGATION: 'NAVIGATION', }; // Listen to events in main app // app/_components/EventListeners.js 'use client'; import { useEffect } from 'react'; import { eventBus, Events } from '@workspace/event-bus'; import { useRouter } from 'next/navigation'; export default function EventListeners() { const router = useRouter(); useEffect(() => { const handleNavigation = (data) => { console.log('Navigation event:', data); router.push(data.path); }; const handleCartUpdate = (data) => { console.log('Cart updated:', data); // Update cart UI }; eventBus.on(Events.NAVIGATION, handleNavigation); eventBus.on(Events.CART_UPDATE, handleCartUpdate); return () => { eventBus.off(Events.NAVIGATION, handleNavigation); eventBus.off(Events.CART_UPDATE, handleCartUpdate); }; }, [router]); return null; } // Send events in sub-app // product-app/components/AddToCart.js 'use client'; import { eventBus, Events } from '@workspace/event-bus'; export default function AddToCart({ product }) { const handleAddToCart = () => { eventBus.emit(Events.PRODUCT_ADD, { product }); eventBus.emit(Events.CART_UPDATE, { type: 'ADD', product }); }; return ( <button onClick={handleAddToCart}> Add to Cart </button> ); }

Style Isolation

1. CSS Modules Isolation

javascript
// product-app/components/ProductCard.module.css .productCard { border: 1px solid #ddd; padding: 16px; border-radius: 8px; background: white; } .productCard__title { font-size: 18px; font-weight: bold; margin-bottom: 8px; } .productCard__price { color: #e44d26; font-size: 20px; font-weight: bold; } // product-app/components/ProductCard.js import styles from './ProductCard.module.css'; export default function ProductCard({ product }) { return ( <div className={styles.productCard}> <h3 className={styles.productCard__title}>{product.name}</h3> <p className={styles.productCard__price}>${product.price}</p> </div> ); }

2. CSS-in-JS Isolation

javascript
// product-app/components/ProductCard.js 'use client'; import styled from 'styled-components'; const Card = styled.div` border: 1px solid #ddd; padding: 16px; border-radius: 8px; background: white; `; const Title = styled.h3` font-size: 18px; font-weight: bold; margin-bottom: 8px; `; const Price = styled.p` color: #e44d26; font-size: 20px; font-weight: bold; `; export default function ProductCard({ product }) { return ( <Card> <Title>{product.name}</Title> <Price>${product.price}</Price> </Card> ); }

3. Shadow DOM Isolation

javascript
// components/ShadowDOMWrapper.js 'use client'; import { useEffect, useRef } from 'react'; export default function ShadowDOMWrapper({ children, styles }) { const containerRef = useRef(null); const shadowRootRef = useRef(null); useEffect(() => { if (!containerRef.current) return; // Create Shadow DOM shadowRootRef.current = containerRef.current.attachShadow({ mode: 'open' }); // Add styles if (styles) { const styleElement = document.createElement('style'); styleElement.textContent = styles; shadowRootRef.current.appendChild(styleElement); } // Add content const content = document.createElement('div'); content.className = 'shadow-content'; shadowRootRef.current.appendChild(content); return () => { if (shadowRootRef.current) { containerRef.current.removeChild(shadowRootRef.current); } }; }, [styles]); useEffect(() => { if (shadowRootRef.current) { const content = shadowRootRef.current.querySelector('.shadow-content'); if (content) { // Use ReactDOM to render to Shadow DOM import('react-dom/client').then(({ createRoot }) => { const root = createRoot(content); root.render(children); }); } } }, [children]); return <div ref={containerRef} />; } // Using Shadow DOM // app/micro/page.js 'use client'; import ShadowDOMWrapper from '@/components/ShadowDOMWrapper'; const shadowStyles = ` .product-card { border: 1px solid #ddd; padding: 16px; border-radius: 8px; background: white; } .product-title { font-size: 18px; font-weight: bold; } `; export default function MicroFrontendPage() { return ( <ShadowDOMWrapper styles={shadowStyles}> <div className="product-card"> <h3 className="product-title">Product Name</h3> <p>$99.99</p> </div> </ShadowDOMWrapper> ); }

Deployment Strategies

1. Independent Deployment

javascript
// Vercel configuration - Main app // vercel.json { "framework": "nextjs", "buildCommand": "pnpm build", "outputDirectory": ".next", "routes": [ { "src": "/(.*)", "dest": "/$1" } ] } // Vercel configuration - Sub-app // product-app/vercel.json { "framework": "nextjs", "buildCommand": "pnpm build", "outputDirectory": ".next", "routes": [ { "src": "/(.*)", "dest": "/$1" } ] } // Docker deployment configuration // Dockerfile FROM node:18-alpine AS base # Dependency installation FROM base AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN npm install -g pnpm && pnpm install --frozen-lockfile # Build FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN pnpm build # Run FROM base AS runner WORKDIR /app ENV NODE_ENV production COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static EXPOSE 3000 CMD ["node", "server.js"]

2. CI/CD Pipeline

javascript
// .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy-main: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build run: pnpm --filter main-app build - name: Deploy to Vercel uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.ORG_ID }} vercel-project-id: ${{ secrets.PROJECT_ID }} working-directory: ./apps/main-app deploy-product: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build run: pnpm --filter product-app build - name: Deploy to Vercel uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.ORG_ID }} vercel-project-id: ${{ secrets.PRODUCT_PROJECT_ID }} working-directory: ./apps/product-app

Best Practices

  1. Choose the right solution: Module Federation for projects with unified tech stack, iframe for completely isolated scenarios
  2. Shared dependencies: Use monorepo to manage shared code and dependencies
  3. State management: Use event bus or shared state management for cross-app communication
  4. Style isolation: Use CSS Modules, CSS-in-JS, or Shadow DOM to avoid style conflicts
  5. Independent deployment: Build and deploy each sub-app independently
  6. Version management: Use semantic versioning for sub-app dependencies
  7. Monitoring and logging: Unified monitoring and log collection
  8. Performance optimization: Load sub-apps on demand, avoid duplicate dependencies
  9. Testing strategy: Integration tests covering cross-app scenarios
  10. Documentation and standards: Establish clear development standards and documentation

The combination of Next.js and micro-frontend architecture provides a flexible, scalable solution for enterprise applications.

标签:TypeORM