Skip to content
 

Next.js 路由组:优雅的组织代码

更新: 11/26/2025字数: 0 字 时长: 0 分钟

什么是路由组?

路由组允许你将相关的路由组织在一起,不会影响实际的 URL 路径结构。通过将文件夹用括号括起来 (folder_name),你可以创建逻辑分组,让项目结构更清晰,同时保持 URL 的简洁性。

核心特性速览

特性说明示例
不影响 URL分组不会出现在浏览器地址栏(auth)/login/login
逻辑组织相关路由可以放在一起(shop)/products, (shop)/cart
共享布局组内路由可以共享特定布局(admin) 组使用管理后台布局
灵活嵌套支持多层级分组(platform)/(web)/dashboard

基础用法:快速入门

项目结构示例

app/
├── (auth)/
│   ├── login/
│   │   └── page.js
│   ├── register/
│   │   └── page.js
│   └── layout.js
├── (shop)/
│   ├── products/
│   │   └── page.js
│   ├── cart/
│   │   └── page.js
│   └── layout.js
├── layout.js
└── page.js

1. 认证路由组

jsx
// app/(auth)/layout.js - 认证相关布局
export default function AuthLayout({ children }) {
  return (
    <div className="auth-layout">
      <div className="auth-container">
        <div className="auth-background">
          <h1>欢迎使用我们的服务</h1>
        </div>
        <div className="auth-form-area">{children}</div>
      </div>
    </div>
  );
}
jsx
// app/(auth)/login/page.js
export default function LoginPage() {
  return (
    <div className="auth-form">
      <h2>用户登录</h2>
      <form>
        <input type="email" placeholder="邮箱地址" />
        <input type="password" placeholder="密码" />
        <button type="submit">登录</button>
      </form>
      <p>
        还没有账号? <a href="/register">立即注册</a>
      </p>
    </div>
  );
}

访问路径: /login (注意:URL 中没有 (auth)

2. 电商路由组

jsx
// app/(shop)/layout.js - 电商相关布局
export default function ShopLayout({ children }) {
  return (
    <div className="shop-layout">
      <header className="shop-header">
        <nav>
          <a href="/products">商品</a>
          <a href="/cart">购物车</a>
        </nav>
      </header>
      <main className="shop-main">{children}</main>
    </div>
  );
}

高级应用场景

场景 1:多布局系统

对于复杂的应用,你可能需要多种不同的布局:

app/
├── (marketing)/
│   ├── layout.js       # 营销页面布局
│   ├── page.js         # 首页 → /
│   ├── about/
│   │   └── page.js     # 关于我们 → /about
│   └── pricing/
│       └── page.js     # 价格页面 → /pricing
├── (app)/
│   ├── layout.js       # 应用内布局
│   ├── dashboard/
│   │   └── page.js     # 仪表盘 → /dashboard
│   └── settings/
│       └── page.js     # 设置 → /settings
└── layout.js           # 根布局
jsx
// app/(marketing)/layout.js
export default function MarketingLayout({ children }) {
  return (
    <div className="marketing-layout">
      <header className="marketing-header">
        <nav>
          <a href="/">首页</a>
          <a href="/about">关于</a>
          <a href="/pricing">价格</a>
          <a href="/dashboard">进入应用</a>
        </nav>
      </header>
      {children}
      <footer className="marketing-footer">
        <p>© 2024 公司名称</p>
      </footer>
    </div>
  );
}
jsx
// app/(app)/layout.js
export default function AppLayout({ children }) {
  return (
    <div className="app-layout">
      <aside className="sidebar">
        <nav>
          <a href="/dashboard">仪表盘</a>
          <a href="/settings">设置</a>
          <a href="/">返回官网</a>
        </nav>
      </aside>
      <main className="app-main">{children}</main>
    </div>
  );
}

场景 2:多租户 SaaS 应用

app/
├── (saas)/
│   ├── (admin)/
│   │   ├── layout.js
│   │   ├── users/
│   │   │   └── page.js      # /users
│   │   └── analytics/
│   │       └── page.js      # /analytics
│   ├── (user)/
│   │   ├── layout.js
│   │   ├── profile/
│   │   │   └── page.js      # /profile
│   │   └── projects/
│   │       └── page.js      # /projects
│   └── layout.js
└── page.js
jsx
// app/(saas)/layout.js - 根 SaaS 布局
export default function SaaSLayout({ children }) {
  return (
    <div className="saas-layout">
      <header className="saas-header">
        <h1>企业 SaaS 平台</h1>
        <div className="user-menu">用户菜单</div>
      </header>
      {children}
    </div>
  );
}
jsx
// app/(saas)/(admin)/layout.js
export default function AdminLayout({ children }) {
  const user = getCurrentUser();

  if (!user?.isAdmin) {
    redirect("/unauthorized");
  }

  return (
    <div className="admin-layout">
      <aside className="admin-sidebar">
        <h3>管理员面板</h3>
        <nav>
          <a href="/users">用户管理</a>
          <a href="/analytics">数据分析</a>
        </nav>
      </aside>
      <div className="admin-content">{children}</div>
    </div>
  );
}

场景 3:A/B 测试和特性开关

app/
├── (experiments)/
│   ├── (feature-a)/
│   │   └── dashboard/
│   │       └── page.js      # 新版本仪表盘
│   ├── (feature-b)/
│   │   └── dashboard/
│   │       └── page.js      # 另一个版本
│   └── dashboard/
│       └── page.js          # 默认版本
jsx
// 中间件:根据用户分组路由
export function middleware(request) {
  const userId = getUserId(request);
  const userGroup = getUserExperimentGroup(userId);

  if (request.nextUrl.pathname === "/dashboard") {
    if (userGroup === "feature-a") {
      return NextResponse.rewrite(new URL("/(experiments)/(feature-a)/dashboard", request.url));
    } else if (userGroup === "feature-b") {
      return NextResponse.rewrite(new URL("/(experiments)/(feature-b)/dashboard", request.url));
    }
  }

  return NextResponse.next();
}

路由组与平行路由的结合

路由组和平行路由可以强强联合,创建极其灵活的架构:

app/
├── (app)/
│   ├── @sidebar/
│   │   ├── default.js
│   │   └── navigation.js
│   ├── @header/
│   │   └── userinfo.js
│   ├── layout.js
│   └── dashboard/
│       └── page.js
jsx
// app/(app)/layout.js
export default function AppLayout({ children, sidebar, header }) {
  return (
    <div className="app-layout">
      <aside className="app-sidebar">{sidebar}</aside>
      <div className="app-main">
        <header className="app-header">{header}</header>
        <main className="app-content">{children}</main>
      </div>
    </div>
  );
}

最佳实践和技巧

1. 命名约定

bash
# 好的命名
(app)/
(marketing)/
(auth)/
(api)/

# 避免的命名
[group]/
{group}/
-group-/

2. 避免过度嵌套

bash
# 推荐 - 清晰的扁平结构
app/
├── (auth)/
├── (app)/
├── (marketing)/
└── (api)/

# 避免 - 过度嵌套
app/
├── (platform)/
   ├── (web)/
   ├── (app)/
   └── (marketing)/
   └── (mobile)/

3. 共享组件和工具

jsx
// 在组内共享组件
app/
├── (ecommerce)/
│   ├── components/
│   │   ├── ProductCard.js
│   │   └── ShoppingCart.js
│   ├── hooks/
│   │   └── useCart.js
│   ├── products/
│   └── cart/

4. 组特定的中间件

javascript
// middleware.js
export function middleware(request) {
  // 认证组的路由保护
  if (request.nextUrl.pathname.startsWith("/login") || request.nextUrl.pathname.startsWith("/register")) {
    return handleAuthRoutes(request);
  }

  // 管理组的路由保护
  if (request.nextUrl.pathname.startsWith("/admin")) {
    return handleAdminRoutes(request);
  }
}

常见问题解答

Q: 路由组会影响 SEO 吗?

A: 不会。路由组只是开发时的组织工具,不会影响最终的 URL 结构,因此对 SEO 完全没有影响。

Q: 可以在路由组中使用动态路由吗?

A: 当然可以!

app/
├── (shop)/
│   ├── products/
│   │   └── [id]/
│   │       └── page.js      # /products/123
│   └── categories/
│       └── [slug]/
│           └── page.js      # /categories/electronics

Q: 路由组支持 catch-all 路由吗?

A: 支持!

app/
├── (docs)/
│   └── [...slug]/
│       └── page.js          # 匹配 /docs/a/b/c 等

Q: 如何在不同组之间共享布局?

A: 使用根布局或在组外定义共享组件:

jsx
// app/layout.js - 根布局共享给所有组
export default function RootLayout({ children }) {
  return (
    <html lang="zh">
      <body className="global-styles">{children}</body>
    </html>
  );
}

我见青山多妩媚,料青山见我应如是。