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

Next.js 与微前端架构如何结合使用?

2月17日 22:52

Next.js 与微前端架构的结合是构建大型企业级应用的重要方案。微前端架构允许将大型应用拆分为多个独立开发、部署和维护的小型前端应用。

微前端架构概述

1. 微前端核心概念

微前端是一种架构风格,将前端应用分解为更小、更简单的块,这些块可以由不同的团队独立开发和部署。

核心优势:

  • 独立开发和部署
  • 技术栈无关
  • 增量升级
  • 团队自治
  • 代码隔离

Next.js 微前端实现方案

1. Module Federation(模块联邦)

javascript
// next.config.js - 主应用配置 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 - 子应用配置(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; }, }; // 主应用中使用远程组件 // 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 方案

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) => { // 验证消息来源 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> ); } // 使用 iframe 集成子应用 // 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') { // 处理导航事件 } else if (data.type === 'AUTH') { // 处理认证事件 } }; 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 方案

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}`); // 调用子应用的 mount 方法 }, 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} />; } // 使用 Web Components 集成 // 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)方案

javascript
// 使用 Turborepo 管理 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/*' // 目录结构 // apps/ // main-app/ # 主应用 // product-app/ # 产品子应用 // cart-app/ # 购物车子应用 // user-app/ # 用户子应用 // packages/ // ui/ # 共享 UI 组件 // utils/ # 共享工具函数 // types/ # 共享类型定义 // config/ # 共享配置 // 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:*" } }

状态管理和通信

1. 跨应用状态管理

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), })); }; // 创建共享状态 export const userStore = createSharedStore({ user: null, isAuthenticated: false, cart: [], }); export const productStore = createSharedStore({ products: [], filters: {}, sortBy: 'name', }); // 主应用中使用 // app/layout.js 'use client'; import { userStore } from '@workspace/shared-state'; import { useEffect } from 'react'; export default function RootLayout({ children }) { useEffect(() => { // 监听用户状态变化 const unsubscribe = userStore.subscribe((state) => { console.log('User state changed:', state); // 通知其他应用 window.postMessage({ type: 'USER_STATE_CHANGE', state }, '*'); }); return () => unsubscribe(); }, []); return ( <html lang="en"> <body>{children}</body> </html> ); } // 子应用中使用 // 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(() => { // 订阅用户状态 const unsubscribe = userStore.subscribe((state) => { setUser(state.user); }); return () => unsubscribe(); }, []); if (!user) { return <div>Please login</div>; } return <div>Welcome, {user.name}</div>; }

2. 事件总线通信

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(); // 定义事件类型 export const Events = { USER_LOGIN: 'USER_LOGIN', USER_LOGOUT: 'USER_LOGOUT', CART_UPDATE: 'CART_UPDATE', PRODUCT_ADD: 'PRODUCT_ADD', NAVIGATION: 'NAVIGATION', }; // 主应用中监听事件 // 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); // 更新购物车 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; } // 子应用中发送事件 // 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> ); }

样式隔离

1. CSS Modules 隔离

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 隔离

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 隔离

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; // 创建 Shadow DOM shadowRootRef.current = containerRef.current.attachShadow({ mode: 'open' }); // 添加样式 if (styles) { const styleElement = document.createElement('style'); styleElement.textContent = styles; shadowRootRef.current.appendChild(styleElement); } // 添加内容 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) { // 使用 ReactDOM 渲染到 Shadow DOM import('react-dom/client').then(({ createRoot }) => { const root = createRoot(content); root.render(children); }); } } }, [children]); return <div ref={containerRef} />; } // 使用 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> ); }

部署策略

1. 独立部署

javascript
// Vercel 配置 - 主应用 // vercel.json { "framework": "nextjs", "buildCommand": "pnpm build", "outputDirectory": ".next", "routes": [ { "src": "/(.*)", "dest": "/$1" } ] } // Vercel 配置 - 子应用 // product-app/vercel.json { "framework": "nextjs", "buildCommand": "pnpm build", "outputDirectory": ".next", "routes": [ { "src": "/(.*)", "dest": "/$1" } ] } // Docker 部署配置 // Dockerfile FROM node:18-alpine AS base # 依赖安装 FROM base AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN npm install -g pnpm && pnpm install --frozen-lockfile # 构建 FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN pnpm build # 运行 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 流程

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

最佳实践

  1. 选择合适的方案: Module Federation 适合技术栈统一的项目,iframe 适合完全隔离的场景
  2. 共享依赖: 使用 monorepo 管理共享代码和依赖
  3. 状态管理: 使用事件总线或共享状态管理跨应用通信
  4. 样式隔离: 使用 CSS Modules、CSS-in-JS 或 Shadow DOM 避免样式冲突
  5. 独立部署: 每个子应用独立构建和部署
  6. 版本管理: 使用语义化版本管理子应用依赖
  7. 监控和日志: 统一监控和日志收集
  8. 性能优化: 按需加载子应用,避免重复依赖
  9. 测试策略: 集成测试覆盖跨应用场景
  10. 文档和规范: 建立清晰的开发规范和文档

Next.js 与微前端架构的结合为企业级应用提供了灵活、可扩展的解决方案。

标签:TypeORM