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.js
│ export default function BlogLayout({ children }) {
│ return (
│ <div className="blog">
│ <aside>博客侧边栏</aside>
│ <main>{children}</main>
│ </div>
│ )
│ }
│
│ ├── loading.js
│ export 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.js
│ export default async function BlogPost({ params }) {
│ const post = await fetch(`/api/posts/${params.slug}`)
│ return <article>{post.content}</article>
│ }
│
│ └── loading.js
│ export default function PostLoading() {
│ return <div>文章加载中...</div>
│ }
最佳实践
- 合理使用布局:避免过度嵌套
- 错误边界:为关键路由段添加错误处理
- 加载状态:提供良好的用户体验
- 404 处理:友好的错误页面
- 模板使用:需要重新挂载时使用
总结
文件系统路由通过特殊文件提供了强大的功能:
- page.js:路由入口
- layout.js:共享 UI 和嵌套布局
- loading.js:加载状态管理
- error.js:错误边界处理
- not-found.js:404 页面
- template.js:页面模板
这些文件协同工作,提供了完整的路由解决方案,让开发者能够轻松构建复杂的应用结构。