Next.js 与微前端架构如何结合使用?
Next.js 与微前端架构的结合是构建大型企业级应用的重要方案。微前端架构允许将大型应用拆分为多个独立开发、部署和维护的小型前端应用。微前端架构概述1. 微前端核心概念微前端是一种架构风格,将前端应用分解为更小、更简单的块,这些块可以由不同的团队独立开发和部署。核心优势:独立开发和部署技术栈无关增量升级团队自治代码隔离Next.js 微前端实现方案1. Module Federation(模块联邦)// 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 方案// 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 方案// 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)方案// 使用 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.yamlpackages: - '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. 跨应用状态管理// packages/shared-state/src/store.jsimport { 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. 事件总线通信// packages/event-bus/src/index.jsclass 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 隔离// 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.jsimport 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 隔离// 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 隔离// 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. 独立部署// 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 部署配置// DockerfileFROM node:18-alpine AS base# 依赖安装FROM base AS depsWORKDIR /appCOPY package.json pnpm-lock.yaml ./RUN npm install -g pnpm && pnpm install --frozen-lockfile# 构建FROM base AS builderWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY . .RUN pnpm build# 运行FROM base AS runnerWORKDIR /appENV NODE_ENV productionCOPY --from=builder /app/public ./publicCOPY --from=builder /app/.next/standalone ./COPY --from=builder /app/.next/static ./.next/staticEXPOSE 3000CMD ["node", "server.js"]2. CI/CD 流程// .github/workflows/deploy.ymlname: Deployon: 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最佳实践选择合适的方案: Module Federation 适合技术栈统一的项目,iframe 适合完全隔离的场景共享依赖: 使用 monorepo 管理共享代码和依赖状态管理: 使用事件总线或共享状态管理跨应用通信样式隔离: 使用 CSS Modules、CSS-in-JS 或 Shadow DOM 避免样式冲突独立部署: 每个子应用独立构建和部署版本管理: 使用语义化版本管理子应用依赖监控和日志: 统一监控和日志收集性能优化: 按需加载子应用,避免重复依赖测试策略: 集成测试覆盖跨应用场景文档和规范: 建立清晰的开发规范和文档Next.js 与微前端架构的结合为企业级应用提供了灵活、可扩展的解决方案。