切换语言
切换主题

Next.js App Router 入门指南:核心概念与基础使用详解

引言

说实话,我第一次打开 Next.js 官方文档的时候是懵的。

左边侧栏里,“Pages Router” 和 “App Router” 并排站着,像是在说”随你选”。可问题来了——选哪个?两个有啥区别?我该学哪个?文档也没直接告诉我答案,反而越看越乱。教程里有的用 pages 文件夹,有的用 app 文件夹,代码写法还完全不一样。

后来我才搞明白:Next.js 其实有两套完全不同的路由系统。老的叫 Pages Router,稳定可靠,但有些新功能用不了。新的叫 App Router,从 v13 开始推出,v13.4 正式稳定,现在已经是官方推荐的方向了。

你可能会问:“那我还要学 App Router 吗?会不会又是一个折腾人的新东西?”

这篇文章就是想帮你解决这个困惑。我会用最简单的方式,把 App Router 的核心概念讲清楚——什么是 Server Components、特殊文件怎么用、跟 Pages Router 到底差在哪。看完你就能快速上手,少走弯路。

什么是 App Router?为什么要用它?

简单来说,App Router 是 Next.js 在 v13 推出的新路由系统。它基于 React 的最新特性——Server Components(服务器组件),设计更现代、更灵活。

跟老的 Pages Router 比,有三个明显的好处:

1. 性能更好
App Router 默认使用服务器组件。这意味着大部分代码在服务器端运行,用户浏览器下载的 JavaScript 变少了,页面加载自然快。根据 Vercel 2024 年的报告,超过 60% 的顶级 Next.js 应用已经切换到 App Router 了。

"超过 60% 的顶级 Next.js 应用已经切换到 App Router"

2. 布局系统更灵活
Pages Router 里,要实现嵌套布局比较麻烦。App Router 直接用 layout.js 文件就能搞定,页面切换时布局还不会重新渲染——体验丝滑。

3. 错误处理和加载状态更强大
你可以用 loading.js 文件定义加载动画,用 error.js 捕获错误并显示降级UI。这些在 Pages Router 里要自己手写,App Router 直接给你约定好了。

那 Pages Router 还能用吗?能。
两套系统可以共存,但如果你现在要学 Next.js,我建议直接上手 App Router。它是官方推荐的方向,新项目脚手架(v14.1.4 开始)默认就用 App Router 了。

文件系统路由:从目录到页面

App Router 最核心的概念是:你的文件夹结构就是你的路由结构

听起来有点抽象,看个例子就懂了:

app/
├── page.js              # 首页,对应 /
├── about/
│   └── page.js          # 关于页,对应 /about
└── blog/
    ├── page.js          # 博客列表,对应 /blog
    └── [slug]/
        └── page.js      # 博客详情,对应 /blog/:slug

有几个关键点要记住:

1. page.js 是路由的入口
只有叫 page.js 的文件才会成为可访问的页面。其他文件(比如 layout.jsloading.js)都是配套的功能文件,不会直接被访问。

2. 动态路由用方括号
想要 /blog/hello-world 这样的动态路由?创建 app/blog/[slug]/page.js,参数 slug 会自动传给你的组件:

// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
  return <h1>文章:{params.slug}</h1>
}

3. 捕获所有路由用 [...slug]
有时候你需要匹配多层路径,比如 /docs/a/b/c。用 app/docs/[...slug]/page.js 就行,params.slug 会是个数组 ['a', 'b', 'c']

对比 Pages Router
如果你之前用过 Pages Router,会发现它用的是 pages/blog/[id].js。App Router 改成了 app/blog/[id]/page.js,多了一层文件夹。为啥?为了给每个路由留出空间放 layout.jsloading.js 这些特殊文件。

刚开始可能觉得麻烦,但习惯了就会发现——整个项目结构清晰多了。

Server Components vs Client Components:核心概念

这可能是 App Router 里最让人困惑的概念了。我刚开始学的时候也懵圈了好久。

说白了就一句话:App Router 里的组件默认在服务器端运行,只在需要交互时才在浏览器端运行

默认是 Server Component

app/ 目录下创建的组件,默认都是 Server Component(服务器组件)。它们在服务器端渲染完,直接把 HTML 发给浏览器。

好处很明显:

  • JavaScript 体积小:代码不用发给浏览器,用户下载的 JS 文件更小
  • 可以直接访问后端资源:数据库查询、API 密钥这些敏感信息可以放心用
  • 首屏加载快:服务器渲染好直接给你,FCP(首次内容渲染)时间短

举个例子,这就是个典型的 Server Component:

// app/products/page.js
// 这是 Server Component,在服务器端运行
async function getProducts() {
  const res = await fetch('https://api.example.com/products')
  return res.json()
}

export default async function ProductsPage() {
  const products = await getProducts()

  return (
    <div>
      <h1>产品列表</h1>
      {products.map(p => (
        <div key={p.id}>{p.name}</div>
      ))}
    </div>
  )
}

看到没?可以直接 async/await 获取数据,不需要 useEffect 或者 getServerSideProps

什么时候用 Client Component?

但有些场景必须在浏览器里运行,比如:

  • 要用 React hooks(useStateuseEffect
  • 要处理用户交互(onClickonChange
  • 要用浏览器 API(localStoragewindow

这时候就需要 Client Component(客户端组件)。标记很简单,文件顶部加一行 'use client'

// components/AddToCartButton.js
'use client'  // 标记为 Client Component

import { useState } from 'react'

export default function AddToCartButton({ productId }) {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      加入购物车 ({count})
    </button>
  )
}

混合使用:最佳实践

真正厉害的是,你可以把两种组件混着用。

比如一个产品页面:

  • 产品列表用 Server Component(服务器端获取数据,减少 JS 体积)
  • 加购按钮用 Client Component(需要处理点击交互)
// app/products/page.js (Server Component)
import AddToCartButton from '@/components/AddToCartButton' // Client Component

async function getProducts() {
  // 服务器端获取数据
}

export default async function ProductsPage() {
  const products = await getProducts()

  return (
    <div>
      <h1>产品列表</h1>
      {products.map(p => (
        <div key={p.id}>
          {p.name}
          <AddToCartButton productId={p.id} />
        </div>
      ))}
    </div>
  )
}

记住一个原则:默认用 Server Component,够用了。只在确实需要交互时才用 'use client'

别一上来就给所有组件加 'use client'——那跟没用 App Router 有啥区别?

特殊文件:让项目更专业

App Router 定义了一堆特殊文件名——layout.jsloading.jserror.js 这些。刚看到时可能觉得麻烦,但用起来真的香。

layout.js:共享布局

这是最常用的特殊文件。它定义了一个路由段的布局,会包裹同级和子级的所有页面。

比如你想给整个应用加个导航栏和页脚:

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

更厉害的是,你可以嵌套布局:

app/
├── layout.js          # 全局布局(导航+页脚)
├── page.js            # 首页
└── dashboard/
    ├── layout.js      # 仪表盘布局(侧边栏)
    ├── page.js        # /dashboard
    └── settings/
        └── page.js    # /dashboard/settings

当你从 /dashboard 跳到 /dashboard/settings 时,全局布局和仪表盘布局都不会重新渲染——只有 page.js 会更新。丝滑。

loading.js:加载状态

不用自己写 useState 管理 loading 了。创建一个 loading.js,App Router 会自动用 Suspense 包裹你的页面:

// app/dashboard/loading.js
export default function Loading() {
  return <div>加载中...</div>
}

页面数据获取时,loading.js 的内容会自动显示。就这么简单。

error.js:错误边界

用来捕获页面错误并显示降级UI:

// app/dashboard/error.js
'use client'  // Error boundaries 必须是 Client Component

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>出错了:{error.message}</h2>
      <button onClick={reset}>重试</button>
    </div>
  )
}

有个坑要注意error.js 无法捕获同级 layout.js 的错误。为啥?React Error Boundary 的限制——它只能捕获子组件的错误,不能捕获自己或父级的错误。

要捕获 layout.js 的错误,得在父级目录放 error.js,或者用根目录的 global-error.js

not-found.js:404 页面

当路由不存在时显示:

// app/not-found.js
export default function NotFound() {
  return <h1>页面不存在</h1>
}

你也可以在代码里主动触发 404:

import { notFound } from 'next/navigation'

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)
  if (!post) notFound()  // 触发 not-found.js

  return <article>{post.title}</article>
}

文件层级关系

这些特殊文件有固定的层级关系:

layout.js
├── loading.js  (Suspense 边界)
│   └── page.js
└── error.js    (Error 边界)

layout 在最外层,error.js 包不住它。loading.js 负责加载状态,error.js 负责错误处理。

搞清楚这个层级,就不会踩坑了。

数据获取:告别 getServerSideProps

如果你用过 Pages Router,肯定写过 getServerSidePropsgetStaticProps。老实说,那套 API 挺别扭的——要单独导出一个函数,数据传递也不直观。

App Router 把这一切简化了。

直接用 async/await

在 Server Component 里,你可以直接在组件函数里获取数据:

// app/posts/page.js
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

看到没?就是普通的 async/await,不需要特殊的 API。

并行数据获取

更厉害的是,你可以并行获取多个数据源:

export default async function Dashboard() {
  // 并行获取,不会阻塞
  const [user, posts, stats] = await Promise.all([
    getUser(),
    getPosts(),
    getStats()
  ])

  return (
    <div>
      <h1>{user.name}</h1>
      <Posts data={posts} />
      <Stats data={stats} />
    </div>
  )
}

数据缓存和重新验证

Next.js 会自动缓存 fetch 请求。你可以控制缓存策略:

// 缓存 60 秒后重新验证
fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
})

// 不缓存,每次都获取最新数据
fetch('https://api.example.com/data', {
  cache: 'no-store'
})

对比 Pages Router

  • Pages Router:getServerSideProps + getStaticProps,要单独导出函数
  • App Router:直接 async/await,组件内获取数据

简单多了吧?

初学者常见问题与解决方案

学 App Router 的时候,我踩过不少坑。这里整理了几个最常见的问题,希望能帮你少走弯路。

问题1:什么时候用 ‘use client’?

困惑点:看到教程里到处都是 'use client',不知道什么时候该加。

解决方案
记住一个原则——默认不加,需要时再加

只在以下情况加 'use client'

  • 用了 React hooks(useStateuseEffectuseContext
  • 有用户交互(onClickonChange
  • 用了浏览器 API(windowlocalStorage

其他时候别加。Server Component 性能更好,能直接访问后端资源。

问题2:layout.js 和 page.js 的关系?

困惑点:这俩文件放在同一个文件夹,谁包裹谁?

解决方案
layout.js 包裹 page.js 和子路由。

app/
├── layout.js       # 包裹下面所有页面
├── page.js         # 首页,被上面的 layout 包裹
└── about/
    └── page.js     # 关于页,也被上面的 layout 包裹

页面切换时,layout.js 不会重新渲染——只有 page.js 更新。这就是为什么导航栏不会闪一下。

问题3:动态路由参数怎么拿?

困惑点:创建了 [slug]/page.js,但不知道怎么获取 slug 的值。

解决方案
通过 params 属性获取:

// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
  console.log(params.slug)  // 就是 URL 里的那个值
  return <h1>文章:{params.slug}</h1>
}

如果是嵌套动态路由,比如 app/blog/[category]/[slug]/page.js

export default function Post({ params }) {
  console.log(params.category, params.slug)
  return <h1>{params.category} - {params.slug}</h1>
}

问题4:error.js 不生效?

困惑点:创建了 error.js,但布局出错时没有捕获。

解决方案
error.js 无法捕获同级 layout.js 的错误。这是 React Error Boundary 的限制。

要捕获布局错误,有两个办法:

  1. 在父级目录放 error.js
  2. 在根目录用 global-error.js(需要包含 <html><body> 标签)
// app/global-error.js
'use client'

export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <h2>全局错误:{error.message}</h2>
        <button onClick={reset}>重试</button>
      </body>
    </html>
  )
}

问题5:我的旧项目要不要迁移?

困惑点:看到 App Router 这么多新东西,担心旧项目要全部重写。

解决方案
不用急。

Pages Router 和 App Router 可以共存。你可以:

  • 旧功能继续用 pages/
  • 新功能用 app/

Vercel 官方也说了,Pages Router 会长期支持,不会废弃。

但如果是新项目,直接用 App Router。它是未来方向,生态会越来越好。

结论

说了这么多,咱们快速回顾一下 App Router 的五个核心概念:

  1. 文件系统路由:文件夹结构就是路由结构,page.js 是入口
  2. Server Components:默认在服务器端运行,性能更好
  3. Client Components:需要交互时用 'use client' 标记
  4. 特殊文件layout.jsloading.jserror.js 让项目更专业
  5. 数据获取:直接用 async/await,告别 getServerSideProps

App Router 确实是 Next.js 的未来方向。Vercel 在持续投入,社区也在积极跟进。如果你现在开始学 Next.js,直接上手 App Router 准没错。

接下来该干嘛?

动手试试。创建一个小项目,试着用 App Router 做个博客或者待办事项应用。概念再多,不如自己敲一遍代码来得清楚。

遇到问题别慌——Pages Router 和 App Router 可以共存,实在搞不定就先用 Pages Router,慢慢再迁移。

最后,Next.js 官方文档虽然有点乱,但 App Router 部分写得还算详细。遇到具体问题,记得去翻翻文档,或者上 GitHub Discussions 搜搜看。

祝你学习顺利!

常见问题

什么时候应该使用 'use client'?
只在需要交互时使用:使用 React hooks(useState、useEffect)、处理用户交互(onClick、onChange)、使用浏览器 API(window、localStorage)。默认使用 Server Component 性能更好。
layout.js 和 page.js 的关系是什么?
layout.js 包裹 page.js 和子路由。页面切换时,layout.js 不会重新渲染,只有 page.js 更新,这样导航栏等共享元素不会闪烁。
如何获取动态路由参数?
通过组件的 params 属性获取。例如 app/blog/[slug]/page.js 中,使用 { params } 参数,通过 params.slug 访问 URL 中的 slug 值。
error.js 为什么无法捕获 layout.js 的错误?
这是 React Error Boundary 的限制,它只能捕获子组件的错误,不能捕获同级或父级的错误。要捕获布局错误,需要在父级目录放 error.js 或使用根目录的 global-error.js。
旧项目需要迁移到 App Router 吗?
不需要立即迁移。Pages Router 和 App Router 可以共存,旧功能继续用 pages/,新功能用 app/。Vercel 官方承诺 Pages Router 会长期支持。但新项目建议直接使用 App Router。
App Router 和 Pages Router 的主要区别是什么?
App Router 基于 Server Components,默认服务器端渲染,性能更好;使用文件系统路由,支持嵌套布局;数据获取直接用 async/await。Pages Router 使用客户端组件为主,需要 getServerSideProps 获取数据。

11 分钟阅读 · 发布于: 2025年12月18日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章