Q4: 文件系统路由是如何工作的?解释 app/ 目录下 page.js, layout.js, loading.js, error.js, template.

45 阅读3分钟

Next.js 面试题详细答案 - Q4

Q4: 文件系统路由是如何工作的?解释 app/ 目录下 page.js, layout.js, loading.js, error.js, template.js, not-found.js 这些特殊文件的作用和优先级。

文件系统路由工作原理

Next.js App Router 基于文件系统自动生成路由,每个文件夹代表一个路由段,特殊文件提供特定功能。

目录结构示例

app/
├── layout.js          // 根布局
├── page.js            // 首页 (/)
├── loading.js         // 全局加载状态
├── error.js           // 全局错误处理
├── not-found.js       // 404 页面
├── template.js        // 全局模板
│
├── about/
│   ├── layout.js      // /about 布局
│   ├── page.js        // /about 页面
│   └── loading.js     // /about 加载状态
│
├── blog/
│   ├── layout.js      // /blog 布局
│   ├── page.js        // /blog 列表页
│   ├── [slug]/
│   │   ├── page.js    // /blog/[slug] 详情页
│   │   └── loading.js // /blog/[slug] 加载状态
│   └── category/
│       └── [name]/
│           └── page.js // /blog/category/[name]
│
└── dashboard/
    ├── layout.js      // /dashboard 布局
    ├── page.js        // /dashboard 首页
    └── settings/
        └── page.js    // /dashboard/settings

特殊文件详解

1. page.js - 页面组件

作用:定义路由的 UI 组件,是路由的入口点。

// app/page.js - 首页 (/)
export default function HomePage() {
  return <h1>欢迎来到首页</h1>
}

// app/about/page.js - 关于页面 (/about)
export default function AboutPage() {
  return <h1>关于我们</h1>
}

// app/blog/[slug]/page.js - 动态路由
export default function BlogPost({ params }) {
  return <h1>博客文章: {params.slug}</h1>
}

优先级:每个路由段必须有且仅有一个 page.js

2. layout.js - 布局组件

作用:为路由段及其子路由提供共享 UI,支持嵌套布局。

// app/layout.js - 根布局
export default function RootLayout({ children }) {
  return (
    <html lang="zh">
      <body>
        <header>全局导航</header>
        <main>{children}</main>
        <footer>全局页脚</footer>
      </body>
    </html>
  )
}

// app/blog/layout.js - 博客布局
export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      <aside>博客侧边栏</aside>
      <article>{children}</article>
    </div>
  )
}

嵌套特性

// 布局嵌套示例
// app/layout.js
<RootLayout>
  <header>全局导航</header>
  <main>
    {/* app/blog/layout.js */}
    <BlogLayout>
      <aside>博客侧边栏</aside>
      <article>
        {/* app/blog/[slug]/page.js */}
        <BlogPost />
      </article>
    </BlogLayout>
  </main>
  <footer>全局页脚</footer>
</RootLayout>
3. loading.js - 加载状态

作用:在页面加载时显示加载 UI,自动包装在 Suspense 中。

// app/loading.js - 全局加载状态
export default function Loading() {
  return (
    <div className="loading">
      <div className="spinner"></div>
      <p>加载中...</p>
    </div>
  )
}

// app/blog/loading.js - 博客加载状态
export default function BlogLoading() {
  return (
    <div className="blog-loading">
      <div className="skeleton-card"></div>
      <div className="skeleton-card"></div>
    </div>
  )
}

自动工作

// 当访问 /blog 时,会显示 BlogLoading
// 当访问 /blog/my-post 时,会显示 BlogLoading
// 当访问 /about 时,会显示全局 Loading
4. error.js - 错误处理

作用:捕获路由段的错误,提供错误边界功能。

// app/error.js - 全局错误处理
'use client'
export default function Error({ error, reset }) {
  return (
    <div className="error">
      <h2>出错了!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>重试</button>
    </div>
  )
}

// app/blog/error.js - 博客错误处理
'use client'
export default function BlogError({ error, reset }) {
  return (
    <div className="blog-error">
      <h2>博客加载失败</h2>
      <p>请稍后重试</p>
      <button onClick={() => reset()}>重新加载</button>
    </div>
  )
}

错误隔离

  • 子路由的错误不会影响父路由
  • 每个路由段可以有自己的错误处理
5. not-found.js - 404 页面

作用:处理 404 错误,当路由不匹配时显示。

// app/not-found.js - 全局 404
export default function NotFound() {
  return (
    <div className="not-found">
      <h1>404</h1>
      <p>页面未找到</p>
      <Link href="/">返回首页</Link>
    </div>
  )
}

// app/blog/not-found.js - 博客 404
export default function BlogNotFound() {
  return (
    <div className="blog-not-found">
      <h2>博客文章未找到</h2>
      <Link href="/blog">查看所有文章</Link>
    </div>
  )
}

触发条件

// 手动触发 404
import { notFound } from 'next/navigation'

async function BlogPost({ params }) {
  const post = await fetch(`/api/posts/${params.slug}`)

  if (!post) {
    notFound() // 显示 not-found.js
  }

  return <article>{post.content}</article>
}
6. template.js - 模板组件

作用:为路由段提供模板,每次导航都会重新挂载。

// app/template.js - 全局模板
export default function Template({ children }) {
  return (
    <div className="template">
      <div className="fade-in">
        {children}
      </div>
    </div>
  )
}

// app/blog/template.js - 博客模板
export default function BlogTemplate({ children }) {
  return (
    <div className="blog-template">
      <div className="page-transition">
        {children}
      </div>
    </div>
  )
}

与 layout.js 的区别

  • layout.js:保持状态,不重新挂载
  • template.js:每次导航都重新挂载

文件优先级和继承

1. 优先级顺序
1. page.js (必需)
2. layout.js (可选,但影响嵌套)
3. loading.js (可选)
4. error.js (可选)
5. not-found.js (可选)
6. template.js (可选)
2. 继承关系
// 路由 /blog/my-post 的文件继承
app/
├── layout.js          // 根布局 (应用)
├── loading.js         // 根加载状态 (应用)
├── error.js           // 根错误处理 (应用)
├── not-found.js       // 根 404 (应用)
├── template.js        // 根模板 (应用)
└── blog/
    ├── layout.js      // 博客布局 (继承根布局)
    ├── loading.js     // 博客加载状态 (覆盖根加载)
    ├── error.js       // 博客错误处理 (覆盖根错误)
    ├── not-found.js   // 博客 404 (覆盖根 404)
    ├── template.js    // 博客模板 (覆盖根模板)
    └── [slug]/
        ├── page.js    // 博客文章页面
        └── loading.js // 文章加载状态 (覆盖博客加载)

实际应用示例

// 完整的博客系统结构
app/
├── layout.js
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <nav>全局导航</nav>
        {children}
        <footer>全局页脚</footer>
      </body>
    </html>
  )
}

├── blog/
│   ├── layout.jsexport default function BlogLayout({ children }) {
│     return (
│       <div className="blog">
│         <aside>博客侧边栏</aside>
│         <main>{children}</main>
│       </div>
│     )
│   }
│
│   ├── loading.jsexport default function BlogLoading() {
│     return <div>博客加载中...</div>
│   }
│
│   ├── error.js'use client'export default function BlogError({ error, reset }) {
│     return (
│       <div>
│         <h2>博客加载失败</h2>
│         <button onClick={reset}>重试</button>
│       </div>
│     )
│   }
│
│   └── [slug]/
│       ├── page.jsexport default async function BlogPost({ params }) {
│         const post = await fetch(`/api/posts/${params.slug}`)
│         return <article>{post.content}</article>
│       }
│
│       └── loading.jsexport default function PostLoading() {
│         return <div>文章加载中...</div>
│       }

最佳实践

  1. 合理使用布局:避免过度嵌套
  2. 错误边界:为关键路由段添加错误处理
  3. 加载状态:提供良好的用户体验
  4. 404 处理:友好的错误页面
  5. 模板使用:需要重新挂载时使用

总结

文件系统路由通过特殊文件提供了强大的功能:

  • page.js:路由入口
  • layout.js:共享 UI 和嵌套布局
  • loading.js:加载状态管理
  • error.js:错误边界处理
  • not-found.js:404 页面
  • template.js:页面模板

这些文件协同工作,提供了完整的路由解决方案,让开发者能够轻松构建复杂的应用结构。