React: Next.js 入门

最后更新:2026-08-26

Tom 用 create-react-app 搭建了一个博客网站,上线后发现首屏加载要 3 秒,SEO 抓取不到内容,文章更新还得手动刷新 CDN 缓存。他意识到纯客户端渲染已经不够用了——需要一个能服务端渲染、静态生成、自动优化的框架。这就是 Next.js 登场的时刻。

Next.js 由 Vercel 开发维护,是目前最流行的 React 全栈框架。它解决了 React 作为纯客户端 UI 库无法处理的核心问题:路由系统、服务端渲染、静态站点生成、API 路由、图片优化、字体优化、中间件等。无论你是构建博客、电商、SaaS 还是企业级应用,Next.js 都提供了开箱即用的最佳实践。

本课先厘清 Next.js 的核心概念与架构设计。你将理解四种渲染模式的区别、App Router 的工作原理,以及 Server/Client 组件的分工策略。这些基础概念是后续深入学习数据获取、部署、综合项目实战的根基。通过 Tom 的真实迁移案例,你可以直观感受从 CRA 到 Next.js 的完整技术升级路径。


1. 你将学到



2. 概念图解

Tom 在学习 Next.js 时画了一张流程图来理解请求的处理路径。用户请求到达后,Next.js 会根据页面的配置(静态/动态/增量)选择不同的渲染路径,最终返回 HTML 并在浏览器端完成交互激活。

这张图展示了四个关键决策节点:首先判断页面类型(静态/动态/增量/客户端),然后选择对应的渲染引擎,生成 HTML 返回给浏览器,最后通过 Hydration 过程让页面中的 Client Component 获得交互能力。

100%
flowchart LR
    A[用户请求] --> B{Next.js 路由匹配}
    B -->|静态页面| C[SSG<br/>构建时生成]
    B -->|动态页面| D[SSR<br/>请求时渲染]
    B -->|增量更新| E[ISR<br/>按需重新验证]
    B -->|客户端交互| F[CSR<br/>浏览器执行]
    C --> G[返回 HTML]
    D --> G
    E --> G
    F --> G
    G --> H[Hydration<br/>激活交互]
    H --> I[Client Component<br/>运行 JS]


3. 一个真实场景

Tom 的博客最初用 create-react-app 开发,所有页面都是客户端渲染。用户打开文章时,先下载一个空的 HTML 壳子,然后等待 JS 加载完再去 API 取数据渲染内容。这对内容型网站来说是灾难——搜索引擎爬虫只看到了空白页,首屏加载时间长达 3 秒。

他调研后发现需要一种"混合渲染"方案:博客文章用静态生成(构建时生成 HTML,CDN 分发),用户档案页用服务端渲染(每次请求生成最新数据),评论区用客户端渲染(浏览器端交互)。Next.js 恰好提供了这一切。

他花了一周时间将博客从 CRA 迁移到 Next.js,具体做了三件事:第一,将文章页面改为 SSG,通过 generateStaticParams 在构建时生成所有文章页面的 HTML,部署到 Vercel 后 CDN 直接返回,首屏加载降至 0.3 秒;第二,用户档案页改为 SSR,每次请求从数据库获取最新数据;第三,评论区用 Client Component 实现,只在浏览器端加载交互 JS。迁移完成后,网站在 Google Lighthouse 的 SEO 评分从 45 分提升到 98 分,首屏加载时间缩短了 90%。

迁移过程中 Tom 也遇到了不少坑。比如他最初在 pages/ 目录下使用 Pages Router,后来发现 App Router 更好用,花了额外时间做迁移。他建议新项目直接使用 App Router,避免二次迁移。他还发现 Server Component 中不能直接使用 useRouterusePathname,需要将这些路由逻辑提取到 Client Component 中。这些经验教训让他深刻理解了 Next.js 的设计哲学——"服务端优先,客户端按需"。

(1) 渲染模式对比

Next.js 提供了四种渲染模式,每一种对应不同的场景需求。理解它们的区别是选择正确架构的关键。

CSR(客户端渲染):浏览器下载空的 HTML 壳子,然后执行 JavaScript 渲染内容。Tom 的博客最初就是这种模式。优点是交互流畅、后端压力小;缺点是首屏慢、SEO 差。适合仪表盘、管理后台等需要用户登录的交互型应用。

SSR(服务端渲染):每次用户请求时,服务端动态生成完整的 HTML 返回。Tom 用这种方式解决了博客的 SEO 问题,但每次请求都要等待服务端渲染完毕才能返回,服务器压力较大。适合新闻网站、电商商品页等需要实时数据的页面。

SSG(静态生成):在构建阶段就生成所有页面的 HTML 文件,部署到 CDN 后用户直接获取静态文件。速度最快,SEO 最好,但内容更新需要重新构建。Tom 的博客文章页面就适合这种模式——文章写完后内容就固定了。适合博客、文档站、营销页面。

ISR(增量静态生成):是 SSG 的增强版,允许在构建后按需重新验证并更新特定页面,无需重建整个站点。例如 Tom 的产品目录页设置每 60 秒重新验证一次,有新商品时最多延迟 60 秒就能展示。适合电商产品页、内容频繁更新的站点。

选择策略可以概括为:内容优先用 SSG,数据实时用 SSR,交互密集用 CSR,更新频繁但不想重建全部用 ISR。

渲染模式性能对比表

指标 SSG ISR SSR CSR
首屏速度 最快 中等
SEO 友好度
数据实时性 构建时固定 按需更新 每次请求最新 浏览器加载后
服务器负载
适用场景 博客/文档 电商/新闻 个性化页面 后台管理
典型 TTFB <50ms <100ms 200-500ms 200-500ms

▶ 示例 1:四种渲染模式在 Next.js 中的实现

下面的代码展示了 Tom 博客中三种渲染模式的实现。注意 generateStaticParams 在构建时被调用,返回所有可能的参数组合,Next.js 会据此生成所有页面的静态 HTML。dynamic = 'force-dynamic' 则强制页面在每次请求时重新渲染。

TSX 📖 仅展示
// === 静态生成 SSG(默认行为) ===
// app/blog/[slug]/page.tsx
// 构建时自动扫描所有文章 slug,生成对应的 HTML 页面
// 生成后 CDN 直接返回静态文件,不需要服务端渲染

export async function generateStaticParams() {
  // 构建时调用:获取所有文章列表
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  // 返回 slug 数组,Next.js 会为每个 slug 生成一个 page
  return posts.map((post: any) => ({ slug: post.slug }))
}

async function BlogPost({ params }: { params: { slug: string } }) {
  // 每个页面在构建时独立获取数据
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json())
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}
export default BlogPost

// === 服务端渲染 SSR(动态渲染) ===
// app/dashboard/page.tsx
// 每次用户请求都从 API 获取最新数据,确保数据的实时性

export const dynamic = 'force-dynamic' // 强制关闭静态缓存,每次都走 SSR

async function DashboardPage() {
  // 每次请求都会执行这个 fetch
  const stats = await fetch('https://api.example.com/dashboard/stats').then(r => r.json())
  return (
    <div>
      <h1>实时仪表盘</h1>
      <p>当前在线用户:{stats.onlineUsers}</p>
      <p>今日订单:{stats.todayOrders}</p>
      <p>本月收入:${stats.monthlyRevenue}</p>
    </div>
  )
}
export default DashboardPage

// === 增量静态生成 ISR ===
// app/products/[id]/page.tsx
// 构建时生成静态页面,但每 60 秒后首次访问会触发重新生成

async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 60 } // ISR:60 秒后重新验证
  }).then(r => r.json())
  return (
    <div>
      <h1>{product.name}</h1>
      <p>价格:${product.price}</p>
      <p>库存:{product.stock}</p>
      <p>描述:{product.description}</p>
    </div>
  )
}
export default ProductPage
逻辑代码 42 行(超过 40 行限制,仅展示)

Tom 在博客中选择了 SSG + ISR 的混合方案:文章页面用 SSG 确保首页速度,产品列表页用 ISR 每 60 秒更新一次价格和库存。这样既保证了 SEO 评分,又兼顾了数据时效性。

(2) App Router 文件路由

Next.js 13+ 引入了 App Router,基于文件系统自动生成路由。你只需要在 app/ 目录下创建文件夹和 page.tsx 文件,Next.js 就会自动映射对应的 URL 路径。不再需要手动维护路由配置文件,目录结构就是路由结构。

核心文件类型

动态路由与高级模式

使用 [param] 语法创建动态路由,例如 app/blog/[slug]/page.tsx 匹配 /blog/hello-world。使用 [...param] 的 catch-all 语法匹配多级路径,例如 app/docs/[...slug]/page.tsx 匹配 /docs/guide/getting-started/installation

Layout 文件可以实现深度嵌套布局——父 Layout 包裹子页面的内容。导航栏、侧边栏、页脚等公共部分只需写一次,Next.js 会自动维护 Layout 的持久化状态。切换页面时 Layout 不会被重新渲染,只有子 page 会更新。

▶ 示例 2:完整的博客路由结构

这里展示了 Tom 博客的完整 App Router 目录结构。注意每个文件夹下的特殊文件名(pagelayoutloadingerrornot-found)都是 Next.js 的约定——框架会自动识别这些文件并赋予它们特定的路由行为。dashboard/layout.tsx 会嵌套在根 layout.tsx 内部,形成两层布局:最外层是全局导航+页脚,内层是仪表盘专属侧边栏。

BASH
app/
├── page.tsx                # → / 首页
├── layout.tsx              # → 全局布局(导航 + 页脚)
├── loading.tsx             # → 全局加载态(页面过渡时的骨架屏)
├── error.tsx               # → 全局错误页(500 错误的兜底 UI)
├── not-found.tsx           # → 404 页面
├── about/
│   └── page.tsx            # → /about 关于页
├── blog/
│   ├── page.tsx            # → /blog 文章列表页
│   └── [slug]/             # → 动态路由:匹配 /blog/任意值
│       ├── page.tsx        # → /blog/hello-world 文章详情
│       └── loading.tsx     # → 文章详情页的专属加载态
├── dashboard/
│   ├── page.tsx            # → /dashboard 仪表盘总览
│   ├── layout.tsx          # → 仪表盘专属布局(带侧边栏导航)
│   └── settings/
│       └── page.tsx        # → /dashboard/settings 设置页面
└── api/
    └── hello/
        └── route.ts        # → /api/hello API 端点

路由映射对照表

文件路径 对应 URL 说明
app/page.tsx / 首页
app/about/page.tsx /about 静态路由
app/blog/[slug]/page.tsx /blog/:slug 动态路由
app/blog/[slug]/loading.tsx /blog/:slug 同一路由的加载态
app/dashboard/layout.tsx /dashboard/* 嵌套布局
app/api/hello/route.ts /api/hello API 端点
TSX
// app/layout.tsx - 全局布局
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh">
      <body>
        <header>
          <nav>
            <a href="/">首页</a>
            <a href="/blog">博客</a>
            <a href="/about">关于</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>&copy; 2026 Tom 的博客. All rights reserved.</footer>
      </body>
    </html>
  )
}

// app/dashboard/layout.tsx - 仪表盘专属布局(嵌套在全局布局内)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div style={{ display: 'flex' }}>
      <aside style={{ width: 240, background: '#f5f5f5', padding: 16 }}>
        <nav>
          <a href="/dashboard">总览</a>
          <a href="/dashboard/settings">设置</a>
        </nav>
      </aside>
      <section style={{ flex: 1, padding: 16 }}>{children}</section>
    </div>
  )
}

// app/blog/[slug]/loading.tsx - 文章详情加载态
export default function Loading() {
  return (
    <div style={{ padding: 24 }}>
      <div style={{ height: 32, width: '60%', background: '#eee', borderRadius: 4, marginBottom: 16 }} />
      <div style={{ height: 16, width: '30%', background: '#eee', borderRadius: 4, marginBottom: 24 }} />
      <div style={{ height: 200, background: '#eee', borderRadius: 4 }} />
    </div>
  )
}

(3) Server Component 与 Client Component

Next.js 的一个革命性设计是 Server Component。这是在 React 生态中第一次将组件分为服务端和客户端两个执行环境。默认情况下,App Router 中的所有组件都是 Server Component——它们在服务端执行,可以直接访问数据库、文件系统、敏感环境变量,最重要的是不会发送任何 JavaScript 到浏览器。

当你需要一个交互功能时(点击按钮、输入文本、使用 useState/useEffect、监听窗口事件等),单纯的服务端组件就不够了。这时需要在文件顶部加上 'use client' 声明,Next.js 会将该组件及其所有子组件标记为 Client Component,打包发送到浏览器端执行。

Server Component 的特点

Client Component 的特点

最佳实践模式

采用"Server Component 做容器、Client Component 做叶子"的分层架构。外层 Server Component 负责获取数据和布局,内层 Client Component 只负责有交互需求的小部件。这样大部分逻辑和数据获取都在服务端完成,浏览器只运行最小必要的 JavaScript。

▶ 示例 3:Server/Client 组件的最佳分工

下面代码展示了 Tom 博客中"数据获取在服务端、交互在浏览器"的分层模式。PostsPage 是 Server Component——它在服务端 fetch 数据,渲染成 HTML 后直接发送给浏览器,用户看到的是完整页面,不需要等待 JS 加载。PostList 是 Client Component——它接收服务端准备好的数据,只负责浏览器端的搜索过滤和删除交互。注意 'use client' 只在需要交互的"叶子组件"上标注,而不是整个页面。

TSX 📖 仅展示
// app/posts/page.tsx - Server Component(默认)
// 这个组件在服务端执行,不发送任何 JS 到浏览器
import PostList from './PostList'

async function PostsPage() {
  // 服务端直接 fetch,数据在构建 HTML 时就已准备好
  // 用户看到的是完整页面,没有 loading 状态
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())

  return (
    <div>
      <h1>全部文章</h1>
      <PostList posts={posts} />
    </div>
  )
}
export default PostsPage

// app/posts/PostList.tsx - 标注 'use client' 成为 Client Component
// 只有这个文件会发送 JS 到浏览器,包含它的父组件(Server Component)不会
'use client'
import { useState } from 'react'

interface Post {
  id: number
  title: string
  body: string
}

function PostList({ posts: initialPosts }: { posts: Post[] }) {
  // useState 只在 'use client' 组件中可用
  const [posts, setPosts] = useState(initialPosts)
  const [search, setSearch] = useState('')

  const filtered = posts.filter(p =>
    p.title.toLowerCase().includes(search.toLowerCase())
  )

  function handleDelete(id: number) {
    setPosts(prev => prev.filter(p => p.id !== id))
  }

  return (
    <div>
      <input
        type="text"
        placeholder="搜索文章..."
        value={search}
        onChange={e => setSearch(e.target.value)}
        style={{ width: '100%', padding: 8, marginBottom: 16, border: '1px solid #ddd', borderRadius: 4 }}
      />
      {filtered.map(post => (
        <div key={post.id} style={{
          border: '1px solid #ddd', padding: 16, marginBottom: 8, borderRadius: 8,
          display: 'flex', justifyContent: 'space-between', alignItems: 'center'
        }}>
          <div>
            <h2 style={{ margin: 0 }}>{post.title}</h2>
            <p style={{ margin: '8px 0', color: '#666' }}>{post.body}</p>
          </div>
          <button
            onClick={() => handleDelete(post.id)}
            style={{ padding: '4px 12px', background: '#ff4444', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}
          >
            删除
          </button>
        </div>
      ))}
      {filtered.length === 0 && <p>没有找到匹配的文章</p>}
    </div>
  )
}
export default PostList

// pages/old-posts.js - Pages Router 模式(老项目兼容)
// 如果你维护的是 Next.js 12 或更早的项目,Pages Router 使用 getStaticProps/getServerSideProps
export async function getStaticProps() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  return { props: { posts }, revalidate: 60 }
}

export default function OldPostsPage({ posts }: { posts: Post[] }) {
  return (
    <div>
      <h1>Pages Router 模式</h1>
      {posts.map(p => <p key={p.id}>{p.title}</p>)}
    </div>
  )
}
逻辑代码 70 行(超过 40 行限制,仅展示)

Server Component 与 Client Component 的限制

能力 Server Component Client Component
使用 useState/useEffect
使用 onClick/onChange
直接访问数据库
访问环境变量(无前缀)
发送 JS 到浏览器
使用 async/await
使用 useRouter/usePathname
使用 requestAnimationFrame

选择流程图:Server Component 还是 Client Component?

TEXT 📖 仅展示
组件需要交互(点击、输入、滚动等)?
├── 是 → 需要 useState/useEffect?
│   ├── 是 → 加 'use client' → Client Component
│   └── 否 → 是否只接收 props 渲染?
│       ├── 是 → 保留 Server Component
│       └── 否 → 加 'use client'
└── 否 → 保留 Server Component


4. Tom 的迁移路线图

前面学完了 Next.js 的全部核心概念。接下来把这些知识整合起来,看看 Tom 如何将他的 CRA 博客一步步迁移到 Next.js。这个过程也是你在实际项目中采用 Next.js 的标准路线。

Tom 将博客从 CRA 迁移到 Next.js 的完整过程分为五个步骤,每一步都对应本课的核心知识点:


▶ 示例 4:App Router loading.tsx 和 error.tsx

TSX 📖 仅展示
// app/dashboard/loading.tsx - 自动显示的加载状态
export default function DashboardLoading() {
  return (
    <div style={{ padding: 24 }}>
      <div style={{ height: 32, width: 200, background: '#f0f0f0', borderRadius: 4, marginBottom: 16 }} />
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
        {[1, 2, 3].map(i => (
          <div key={i} style={{
            height: 120, background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
            backgroundSize: '200% 100%', borderRadius: 8, animation: 'shimmer 1.5s infinite',
          }} />
        ))}
      </div>
    </div>
  )
}

// app/dashboard/error.tsx - 自动显示的错误状态
'use client'
export default function DashboardError({ error, reset }) {
  return (
    <div style={{ padding: 40, textAlign: 'center' }}>
      <h2>Something went wrong!</h2>
      <p style={{ color: '#999' }}>{error.message}</p>
      <button onClick={reset} style={{ padding: '8px 24px', cursor: 'pointer' }}>Try again</button>
    </div>
  )
}

// app/dashboard/page.tsx - 会自动使用上面的 loading 和 error
async function DashboardPage() {
  const data = await fetch('https://api.example.com/dashboard').then(r => r.json())
  return (
    <div>
      <h1>Dashboard</h1>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
        {data.stats.map(stat => (
          <div key={stat.label} style={{ padding: 16, border: '1px solid #eee', borderRadius: 8 }}>
            <p style={{ color: '#999', margin: 0 }}>{stat.label}</p>
            <p style={{ fontSize: 24, fontWeight: 'bold', margin: '4px 0 0' }}>{stat.value}</p>
          </div>
        ))}
      </div>
    </div>
  )
}
export default DashboardPage
逻辑代码 42 行(超过 40 行限制,仅展示)

▶ 示例 5:Layout 嵌套与模板

TSX 📖 仅展示
// app/layout.tsx - 根布局(必须有 html 和 body)
export default function RootLayout({ children }) {
  return (
    <html lang="zh">
      <body style={{ margin: 0, fontFamily: 'sans-serif' }}>
        {children}
      </body>
    </html>
  )
}

// app/(dashboard)/layout.tsx - 仪表盘布局(路由组不影响 URL)
import Sidebar from '@/components/Sidebar'

export default function DashboardLayout({ children }) {
  return (
    <div style={{ display: 'flex', minHeight: '100vh' }}>
      <Sidebar />
      <main style={{ flex: 1, padding: 24 }}>
        {children}
      </main>
    </div>
  )
}

// app/(auth)/layout.tsx - 认证页面布局(居中卡片)
export default function AuthLayout({ children }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#f5f5f5' }}>
      <div style={{ background: 'white', padding: 32, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.1)', width: 400 }}>
        {children}
      </div>
    </div>
  )
}

// app/(auth)/login/page.tsx - 登录页
function LoginPage() {
  return (
    <>
      <h2 style={{ textAlign: 'center' }}>Sign In</h2>
      <form style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <input type="email" placeholder="Email" style={{ padding: 8, borderRadius: 4 }} />
        <input type="password" placeholder="Password" style={{ padding: 8, borderRadius: 4 }} />
        <button type="submit" style={{ padding: 10, background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
          Sign In
        </button>
      </form>
    </>
  )
}
export default LoginPage
逻辑代码 44 行(超过 40 行限制,仅展示)

❓ 常见问题

Q Next.js 和 React 是什么关系?
A React 是 UI 库,负责组件渲染和状态管理;Next.js 是基于 React 的全栈框架,内置了路由、SSR/SSG/ISR、API 路由、图片优化、字体优化、中间件等功能。可以理解为 React 提供了基础构建块,Next.js 提供了将这些构建块组装成生产级应用的完整工具链。React 专注于"如何渲染 UI",Next.js 解决"如何构建完整的 Web 应用"。
Q Server Component 和 Client Component 怎么选?
A 优先用 Server Component(默认)。只有当组件需要 useState/useEffect/onClick/浏览器 API 时,才加 'use client'。一个常见模式是:Server Component 负责获取数据,把数据传给内部的 Client Component 处理交互。这样数据获取在服务端完成,交互逻辑在浏览器端运行,两全其美。初学者的常见错误是在整个页面加 'use client'——应该只给最小的交互叶子节点加。如果你发现一个组件既不需要交互也不需要状态,就不要加 'use client'
Q SSR 和 SSG 哪个性能更好?
A SSG 性能更好,因为 HTML 在构建时生成,CDN 直接返回静态文件,没有服务端计算开销。但 SSG 不适合内容频繁变化的页面。SSR 每次请求都重新渲染,延迟更高,但数据永远是实时的。折中方案是 ISR——大部分时间像 SSG 一样快,需要更新时按需重新生成。实际项目中通常混合使用:营销页用 SSG,产品页用 ISR,用户中心用 SSR。
Q App Router 和 Pages Router 有什么区别?
A Pages Router 是 Next.js 12 及之前的路由方式,使用 pages/ 目录,通过文件名映射路由。App Router 是 Next.js 13 引入的新路由系统,使用 app/ 目录,支持 Layout 嵌套、Server Component、流式渲染等新特性。新项目建议直接用 App Router,老项目可以逐步迁移。Pages Router 的 getStaticProps/getServerSideProps 在 App Router 中已经被 async component + fetch 替代。两个路由系统可以共存,迁移时可以逐步将页面从 Pages Router 移到 App Router。
Q Next.js 项目一定要部署到 Vercel 吗?
A 不一定。虽然 Vercel 是 Next.js 的创建者,提供最无缝的部署体验(一键部署、自动扩缩容、边缘网络),但 Next.js 也可以部署到其他平台。你可以使用 next build && next start 部署到任何 Node.js 服务器,或使用 Docker 容器化部署。Netlify、AWS Amplify、Cloudflare Pages 也都支持 Next.js 部署。但如果你使用 ISR 和 Middleware 等高级特性,Vercel 的兼容性最好。
Q 如果我先用 Pages Router 之后想迁移到 App Router 怎么办?
A 两个路由系统可以在同一个项目中共存。你可以将 pages/ 中的页面逐步搬到 app/,每搬一个验证一个,不必一次性全部迁移。迁移时注意:getStaticProps 改为 Server Component 中直接 fetch;Layout 改为在 app/ 目录下创建 layout.tsxpages/_app.tsx 的逻辑迁移到根 layout.tsx。建议从最简单的页面开始迁移,积累经验后再处理复杂页面。
Q 在 Server Component 中如何获取 URL 参数?
A Server Component 通过 params prop 获取动态路由参数,通过 searchParams prop 获取查询字符串参数。例如 function Page({ params, searchParams }: { params: { slug: string }, searchParams: { q: string } })。但注意 Server Component 中不能使用 useRouterusePathname——这些 hooks 只在 Client Component 中可用。

📖 小节


📝 作业

  1. 使用 npx create-next-app@latest my-blog 创建一个新项目,确认使用了 App Router 目录结构。在 app/ 下创建 /about/blog/blog/[slug] 三个页面路由,每个页面至少包含一个标题和一段说明文字。运行 npm run dev 并访问每个路由(/about/blog/blog/test-article),验证映射是否正确。

  2. 实现一个全局 Layout(app/layout.tsx),包含顶部导航栏(首页、博客、关于三个链接)和底部页脚。再为 /blog/* 路由创建嵌套 Layout,在文章详情页左侧显示文章目录列表。打开浏览器的 React DevTools,确认 Layout 的嵌套层级是否正确,切换页面时导航栏和侧边栏是否保持状态而不会重新渲染。

  3. 将博客首页改为 Server Component,直接从 JSONPlaceholder API(https://jsonplaceholder.typicode.com/posts)获取文章列表并渲染。然后创建一个 Client Component 实现关键词搜索过滤功能,让用户在浏览器端可以实时搜索过滤文章。确保 Server Component 负责数据获取,Client Component 只负责交互过滤逻辑。完成后再添加一个 loading.tsx(展示骨架屏)和 error.tsx(显示错误提示和重试按钮),体验完整的路由段配置。

  4. 选做:将博客部署到 Vercel(免费),在 Vercel 控制台观察 ISR 的重新验证效果——修改 API 返回的数据后,观察页面在 revalidate 时间到期后是否自动更新。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏