微前端与 Multi-Zone

当产品线足够复杂——主应用、管理后台、文档站、营销官网——你可能不想把它们全部放在一个 Next.js 应用里。但你又希望用户感觉是在同一个网站。Next.js Multi-Zone 就是为这个场景设计的:多个独立 Next.js 应用通过 rewrites 合并成一个域名,独立开发、独立部署、统一访问。本章覆盖 Multi-Zone 和 Module Federation 两种微前端方案。

1. 微前端方案对比

1.1 三种方案

方案适用场景复杂度框架限制
Multi-Zone多个 Next.js 应用统一域名★★仅 Next.js
Module Federation跨应用共享组件,运行时加载★★★★框架无关
iframe完全隔离,遗留系统集成框架无关

推荐:Next.js 项目优先用 Multi-Zone(简单、原生支持);需要跨框架或运行时组件共享时用 Module Federation。

1.2 什么时候需要微前端

✅ 需要微前端:
- 团队 > 20 人,不同团队负责不同子产品
- 子产品有不同的发布节奏
- 需要渐进式迁移(从旧框架迁到 Next.js)
- 子产品技术栈不完全相同

❌ 不需要微前端:
- 团队 < 10 人
- 产品逻辑紧耦合
- 技术栈统一
- → 用 Monorepo 多应用即可

2. Multi-Zone 原理与实践

2.1 架构

用户视角(同一个域名 app.example.com):
  /              → 主应用(apps/web)
  /dashboard     → 主应用
  /admin         → 管理后台(apps/admin)
  /admin/users   → 管理后台
  /docs          → 文档站(apps/docs)
  /docs/api      → 文档站
  /blog          → 博客(apps/blog)

部署视角(独立应用、独立部署):
  apps/web   → web.vercel.app
  apps/admin → admin.vercel.app
  apps/docs  → docs.vercel.app
  apps/blog  → blog.vercel.app

2.2 主应用 rewrites 配置

// apps/web/next.config.ts
import type { NextConfig } from 'next'
 
const config: NextConfig = {
  async rewrites() {
    return {
      beforeFiles: [
        // /admin/* 路由代理到 admin 应用
        {
          source: '/admin',
          destination: `${process.env.ADMIN_URL}/admin`,
        },
        {
          source: '/admin/:path*',
          destination: `${process.env.ADMIN_URL}/admin/:path*`,
        },
        // /docs/* 路由代理到 docs 应用
        {
          source: '/docs',
          destination: `${process.env.DOCS_URL}/docs`,
        },
        {
          source: '/docs/:path*',
          destination: `${process.env.DOCS_URL}/docs/:path*`,
        },
        // /blog/* 路由代理到 blog 应用
        {
          source: '/blog',
          destination: `${process.env.BLOG_URL}/blog`,
        },
        {
          source: '/blog/:path*',
          destination: `${process.env.BLOG_URL}/blog/:path*`,
        },
      ],
      afterFiles: [],
      fallback: [],
    }
  },
}
 
export default config

2.3 子应用 basePath 配置

每个子应用设置自己的 basePath:

// apps/admin/next.config.ts
import type { NextConfig } from 'next'
 
const config: NextConfig = {
  basePath: '/admin',
  // admin 应用的所有路由自动带 /admin 前缀
  // app/page.tsx → /admin
  // app/users/page.tsx → /admin/users
}
 
export default config
// apps/docs/next.config.ts
const config: NextConfig = {
  basePath: '/docs',
}
// apps/blog/next.config.ts
const config: NextConfig = {
  basePath: '/blog',
}

2.4 环境变量

# apps/web/.env.local
ADMIN_URL=http://localhost:3001
DOCS_URL=http://localhost:3002
BLOG_URL=http://localhost:3003
 
# 生产环境
ADMIN_URL=https://admin.vercel.app
DOCS_URL=https://docs.vercel.app
BLOG_URL=https://blog.vercel.app

2.5 跨 Zone 导航

// Zone 内导航用 <Link>(客户端导航,快速)
import Link from 'next/link'
<Link href="/dashboard/projects">Projects</Link>
 
// 跨 Zone 导航用 <a>(完整页面加载)
<a href="/admin/users">Admin Users</a>
// 因为 /admin 是另一个 Next.js 应用,客户端导航不跨应用
 
// 或者封装一个智能 Link 组件
function AppLink({ href, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
  const isExternal = href?.startsWith('/admin') || 
                     href?.startsWith('/docs') || 
                     href?.startsWith('/blog')
  
  if (isExternal) {
    return <a href={href} {...props} />
  }
  return <Link href={href!} {...props} />
}

2.6 共享状态

Multi-Zone 的各应用运行在独立的 JavaScript 上下文中,不能直接共享状态。

共享方案:
1. Cookie(认证 token、用户偏好)
   → 同域名下自动共享
   
2. URL 参数(临时状态)
   → /admin?tenantId=xxx
   
3. localStorage(持久化状态)
   → 同域名下自动共享
   
4. 后端 API(业务数据)
   → 各应用调用相同的 API / 数据库

3. Module Federation

3.1 什么是 Module Federation

Module Federation 是 Webpack 5 / Rspack 的功能,允许一个应用在运行时加载另一个应用暴露的模块(组件、函数、类型)。

应用 A(Host):
  import Button from 'remote_b/Button'
  ← 运行时从应用 B 加载 Button 组件

应用 B(Remote):
  exposes: { './Button': './src/components/Button' }
  ← 声明暴露 Button 组件

3.2 Next.js + Module Federation

使用 @module-federation/nextjs-mf 插件:

// apps/shell/next.config.ts(Host 应用)
import { NextFederationPlugin } from '@module-federation/nextjs-mf'
 
const config: NextConfig = {
  webpack(config, options) {
    if (!options.isServer) {
      config.plugins.push(
        new NextFederationPlugin({
          name: 'shell',
          remotes: {
            dashboard: `dashboard@${process.env.DASHBOARD_URL}/_next/static/${options.isServer ? 'ssr' : 'chunks'}/remoteEntry.js`,
          },
          shared: {
            react: { singleton: true, eager: true },
            'react-dom': { singleton: true, eager: true },
          },
        })
      )
    }
    return config
  },
}
// apps/dashboard/next.config.ts(Remote 应用)
import { NextFederationPlugin } from '@module-federation/nextjs-mf'
 
const config: NextConfig = {
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'dashboard',
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
          './StatsCard': './components/stats-card.tsx',
          './ProjectList': './components/project-list.tsx',
        },
        shared: {
          react: { singleton: true, eager: true },
          'react-dom': { singleton: true, eager: true },
        },
      })
    )
    return config
  },
}

3.3 使用远程组件

// apps/shell/app/page.tsx
import dynamic from 'next/dynamic'
 
const RemoteStatsCard = dynamic(
  () => import('dashboard/StatsCard').catch(() => () => <div>Loading failed</div>),
  { ssr: false, loading: () => <Skeleton className="h-32" /> }
)
 
export default function HomePage() {
  return (
    <div>
      <h1>Shell App</h1>
      <RemoteStatsCard />  {/* 运行时从 dashboard 应用加载 */}
    </div>
  )
}

3.4 Module Federation 的代价

优点缺点
运行时组件共享配置复杂
框架无关版本冲突风险
独立部署运行时错误难调试
按需加载不支持 Server Component
渐进式迁移性能开销(额外网络请求)

结论:除非有明确的跨框架共享需求,否则优先用 Multi-Zone + Monorepo 共享包。

4. 独立部署

4.1 Vercel Multi-Zone 部署

1. 每个应用是 Vercel 上的独立项目
2. 主应用配置 rewrites 指向其他应用的 Vercel URL
3. 主应用绑定自定义域名(app.example.com)
4. 子应用无需绑定域名(通过 rewrites 访问)
# Vercel CLI 部署
cd apps/web && vercel deploy --prod
cd apps/admin && vercel deploy --prod
cd apps/docs && vercel deploy --prod

4.2 Docker 部署

# docker-compose.yml
services:
  web:
    build:
      context: .
      dockerfile: apps/web/Dockerfile
    ports: ["3000:3000"]
    environment:
      - ADMIN_URL=http://admin:3001
      - DOCS_URL=http://docs:3002
 
  admin:
    build:
      context: .
      dockerfile: apps/admin/Dockerfile
    ports: ["3001:3001"]
 
  docs:
    build:
      context: .
      dockerfile: apps/docs/Dockerfile
    ports: ["3002:3002"]
 
  nginx:
    image: nginx:alpine
    ports: ["80:80"]
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
# nginx.conf
upstream web { server web:3000; }
upstream admin { server admin:3001; }
upstream docs { server docs:3002; }
 
server {
    listen 80;
    
    location /admin { proxy_pass http://admin; }
    location /docs  { proxy_pass http://docs; }
    location /      { proxy_pass http://web; }
}

4.3 独立部署 vs Monorepo 部署

方式优点缺点
Monorepo + 统一部署版本一致部署慢
Monorepo + 选择性部署快速、一致需要 Turborepo filter
独立仓库 + 独立部署完全解耦共享代码需发 npm

推荐:Monorepo + Turborepo 选择性部署——既享受代码共享,又有独立部署的速度。

5. 方案选择决策树

你需要多个独立的 Next.js 应用吗?
├── 否 → 用单一 Next.js 应用 + Route Groups
│
├── 是 → 它们需要在同一个域名下吗?
│   ├── 否 → 独立部署,各自绑域名
│   │
│   └── 是 → 用 Multi-Zone(rewrites + basePath)
│       │
│       └── 需要跨应用运行时共享组件吗?
│           ├── 否 → Multi-Zone 就够了
│           └── 是 → 加 Module Federation

本章小结

  • Multi-Zone:多 Next.js 应用通过 rewrites + basePath 合并到一个域名,原生支持、配置简单
  • 跨 Zone 导航:Zone 内用 &lt;Link&gt;,跨 Zone 用 &lt;a&gt;(完整页面加载)
  • 共享状态:Cookie / localStorage / URL 参数 / 后端 API——不能直接共享 JS 内存状态
  • Module Federation:运行时加载远程组件,适合跨框架场景,但配置复杂且不支持 Server Component
  • 独立部署:Vercel 多项目 + Nginx 反代 / Docker Compose 都可实现
  • 决策:大多数场景 Multi-Zone + Monorepo 共享包就够了,Module Federation 是最后手段