Next.js教程

67 阅读8分钟

快速开始

官方文档:nextjs.org/docs

路由

App Router(新版)

  • 文件都在app路径下
  • 获取数据的方式:服务器组件中直接从外部数据源获取数据,或者在客户端组件中使用 fetch 或 use 钩子
  • 元数据:直接使用 export const metadata 导出

Page Router(旧版)

  • 文件都在src/pages路径下
  • 获取数据的方式:使用 getStaticProps 或 getStaticPaths 获取数据
  • 元数据: 通过 next/head 导出的 Head 组件包裹
getStaticProps: // 在构建时生成静态页面
    return {
      props: {
        data: await getData(),
      },
    }
}
getStaticPaths: // 在构建时生成静态页面
    return {
      paths: [{ params: { id: '1' } }],
    }
}

预渲染(Pre Rendering)

客户端直接拿到html内容,直接渲染,不需要等待js加载完成

分类

服务器渲染(SSR)

在每个请求上生成html内容

// app/form-client/page.tsx(客户端组件)
'use client';

export default function ClientFormPage() {
  return (
    <div>
      <h1>表单页面</h1>
    </div>
  );
}
渲染流程
用户请求
  ↓
Next.js 服务器接收请求
  ↓
服务器执行 React 组件(虽然是客户端组件,但第一次在服务器执行)
  ↓
生成 HTML
  ↓
返回 HTML 给浏览器
  ↓
浏览器显示 HTML
  ↓
React 客户端代码加载
  ↓
Hydration(接管 HTML

静态生成(SSG)

在构建时生成静态页面

// app/form/page.tsx(服务端组件)
import FormComponent from './FormComponent';

export default function FormPage() {
  return (
    <div>
      <h1>表单页面</h1>
      <FormComponent /> {/* 客户端组件 */}
    </div>
  );
}
渲染流程
构建时(npm run build)
  ↓
服务器执行 React,生成 HTMLHTML 保存到文件系统
  ↓
用户请求
  ↓
直接返回预生成的 HTML(极快!)
  ↓
浏览器加载 HTML
  ↓
React 客户端代码加载
  ↓
Hydration(激活客户端组件)

增量生成(ISR)

在构建时生成静态页面,在运行时生成静态页面

客户端渲染(CSR)

// 如果完全禁用 SSR(不推荐)
'use client';

export default function CSRPage() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    // 完全在客户端获取数据
    fetch('/api/data').then(r => r.json()).then(setData);
  }, []);
  
  if (!data) return <div>Loading...</div>;
  
  return <div>{data}</div>;
}
特性SSGSSRCSR
生成时机构建时每次请求浏览器中
返回 HTML✅ 是(预生成)✅ 是(实时生成)❌ 否(空壳)
可以缓存✅ 是❌ 否✅ 是(JS)
响应速度⚡ 最快🐢 较慢⚡ 快(交互后)
SEO✅ 友好✅ 友好❌ 不友好
适用场景静态内容、博客需要实时数据不需要 SEO 的页面

⚠️注意

  • next.js 默认使用静态生成(SSG)
  • next.js 允许选择每个页面使用哪种预渲染方式

什么是 Hydration?

Hydration(水合) 是 React 的一个过程:

  1. 服务器预渲染生成 HTML(静态的)
  2. 浏览器加载这个 HTML(用户立即看到内容)
  3. React 在浏览器中加载并"接管"这些 HTML 元素
  4. 这个过程就是 Hydration

Hydration Point 在哪里?

Hydration Point 是客户端组件的边界,在 HTML 中表现为:

<!-- 预渲染的 HTML -->
<div>
  <h1>服务端组件内容</h1>
  
  <!-- 这是 Hydration Point -->
  <div data-reactroot="">
    <form>
      <input type="text" />
      <button>提交</button>
    </form>
  </div>
  <!-- 客户端组件结束 -->
</div>

<!-- 对应的 React 代码 -->
<script>
  // React 会在这里"hydrate"上面的表单元素
  // 让它变成可交互的组件
</script>

App Router 中的渲染模式

  1. SSG(静态生成)
  • 没有使用动态函数(cookies(), headers(), searchParams)
  • 没有设置 dynamic = 'force-dynamic'
  • 没有设置 revalidate
  1. SSR(服务端渲染) - 强制方式
  • 方式一:使用 dynamic = 'force-dynamic'
  • 方式二:使用动态函数(自动触发 SSR)
  1. ISR(增量静态再生)
  • 判断条件:设置了 revalidate

动态函数

  cookies() - 读取和设置 Cookies
  headers() - 读取 HTTP 请求头
  searchParams - 读取 URL 中的查询字符串参数(仅在页面组件中)
  dynamicParams - 读取动态路由段(如 [id], [slug]

验证方式

  • 运行 npm run build,查看输出:

    • ○ = 静态生成(SSG)
    • λ = 动态渲染(SSR)
    • ● = 部分预渲染(PPR)
  • 在开发模式下,查看网络请求:

    • SSG:响应头包含 x-nextjs-cache: HIT
    • SSR:响应头包含 x-nextjs-cache: MISS 或没有缓存头
    • ISR:响应头包含 x-nextjs-cache: UPDATE
渲染模式数据获取时机配置方式适用场景
SSG构建时默认(无需配置)静态内容、博客文章
SSR每次请求时dynamic = 'force-dynamic' 或使用动态函数需要实时数据的页面
ISR构建时 + 定期更新revalidate = 秒数需要定期更新的内容

组件类型

服务组件

在服务器端渲染的组件,默认都是服务端组件

  • 默认类型,无需标记
  • 在服务器上运行
  • 可以 async,直接使用 Node.js API
  • 可以访问数据库、文件系统等
  • 不包含客户端 JavaScript,减少包体积

客户端组件

在客户端渲染的组件

  • 必须在文件顶部添加 'use client' 指令
  • 在浏览器中运行
  • 可以使用 React Hooks(useState, useEffect 等)
  • 可以处理用户交互(onClick, onChange 等)
  • 可以使用浏览器 API(window, localStorage 等)

混合使用

  • 服务端组件可以导入客户端组件
  • 客户端组件不能导入服务端组件
  • 客户端组件的子组件也会成为客户端组件

客户端组件和服务端组件与 SSG/SSR/ISR 的关系

核心理解

SSG/SSR/ISR 是针对页面(Route)的渲染模式,不是针对组件本身。

服务端组件

  • 可以 SSG、SSR、ISR
  • 原因:在服务器上运行,可在构建时或请求时执行

客户端组件

手动在文件顶部指定 'use client'

'use client'; // 这是客户端页面

export default function ClientFormPage() {
  return (
    <div className="container mx-auto p-8">
      <h1 className="text-2xl font-bold mb-4">客户端页面示例</h1>
      <p className="mb-4">
        这个页面整个是客户端组件('use client')。
        虽然会返回 HTML,但这是 SSR(服务端渲染),不是 SSG(静态生成)。
      </p>
      <p className="mb-4 text-red-600">
        注意:这个页面无法在构建时预渲染(不能 SSG),只能在请求时渲染(SSR)。
      </p>
    </div>
  );
}

注意: 在 Next.js App Router 中,即使页面是 'use client',Next.js 仍然会在服务器上执行一次并生成 HTML(这是 SSR,不是 CSR)。

客户端组件能被预渲染
客户端组件/页面为什么还会被预渲染呢?

关键理解:SSR vs SSG 当你给页面加上 'use client' 时,表示 这不是预渲染(SSG),而是服务端渲染(SSR)!

渲染方式时机是否返回 HTML能否静态生成
SSG(静态生成)构建时✅ 是✅ 可以
SSR(服务端渲染)每次请求时✅ 是❌ 不能
CSR(客户端渲染)浏览器中❌ 否(只有空壳)❌ 不能
✅ 会被预渲染的
  1. HTML 结构
    // 客户端组件
    return (
      <form>
        <input type="text" />
        <button>提交</button>
      </form>
    );
    
    → HTML 中的 <form>, <input>, <button> 都会被预渲染
  2. 初始值
    const [count, setCount] = useState(0);
    return <div>{count}</div>;
    
    → HTML 中会显示 <div>0</div>
  3. Props 传递的值
    <ClientComponent title="Hello" />
    
    → HTML 中会显示传递的值
  4. 条件渲染的结果(基于 props)
    function ClientComponent({ show }) {
      return show ? <div>显示</div> : null;
    }
    
    → 如果 showtrue,HTML 中会有 <div>显示</div>
❌ 不会被预渲染的
  1. JavaScript 逻辑
    const handleClick = () => {
      console.log('clicked'); // 不会执行
    };
    
  2. 事件处理器
    <button onClick={handleClick}> // onClick 不会工作(直到 hydration)
    
  3. useEffect 的效果
    useEffect(() => {
      console.log('mounted'); // 只在浏览器中执行
    }, []);
    
  4. 浏览器 API
    const width = window.innerWidth; // 错误,无法在服务器执行
    
  5. 动态状态变化
    const [count, setCount] = useState(0);
    // 只有初始值 0 会被预渲染
    // 用户点击后的变化不会预渲染
    
客户端页面('use client')的渲染流程
用户请求
  ↓
Next.js 服务器接收请求
  ↓
服务器执行 React 组件(虽然是客户端组件,但第一次在服务器执行)
  ↓
生成 HTML
  ↓
返回 HTML 给浏览器(这就是为什么你看到 HTML)
  ↓
浏览器显示 HTML
  ↓
React 客户端代码加载
  ↓
Hydration(接管 HTML 元素)
  ↓
页面完全可交互

不能预渲染的场景

  1. 整个页面是客户端组件
  2. 客户端组件中使用服务端 API
  3. 客户端组件中使用 Node.js API

总结

  1. 客户端组件会被预渲染 HTML 结构,但 JavaScript 逻辑不会
  2. Hydration Point 是客户端组件的边界,在 HTML 中可以找到
  3. 客户端页面('use client') 仍然返回 HTML,但这是 SSR,不是 SSG
  4. SSR 和 SSG 都返回 HTML,区别在于生成的时机:
    • SSG:构建时生成,可以缓存
    • SSR:每次请求时生成,无法缓存

API

文件命名规则

在 App Router 中,API 路由必须使用 app/api/route.tsapp/api/route.js 文件名:

  • route.ts / route.js - API 路由文件
  • api.ts / api.js - 不是 API 路由(会被忽略)

文件和路径的映射关系

文件路径                              → 请求路径
─────────────────────────────────────────────────────
app/api/route.ts                     → /api
app/api/users/route.ts               → /api/users
app/api/users/[id]/route.ts          → /api/users/:id
app/api/posts/[slug]/route.ts       → /api/posts/:slug
app/api/users/[id]/posts/route.ts   → /api/users/:id/posts

支持多种 HTTP 方法

请求路径:

  • GET /api/users
  • POST /api/users
  • PUT /api/users
  • DELETE /api/users
// app/api/users/route.ts

export async function GET() {
  return Response.json({ users: [] });
}

export async function POST(request: Request) {
  const body = await request.json();
  return Response.json({ created: true, data: body });
}

export async function PUT(request: Request) {
  const body = await request.json();
  return Response.json({ updated: true, data: body });
}

export async function DELETE(request: Request) {
  return Response.json({ deleted: true });
}

动态路由

// app/api/users/[id]/route.ts

// GET /api/users/:id - 获取单个用户
export async function GET(
  request: Request
) {
  // 从 URL 路径中获取 id
  const url = new URL(request.url);
  const pathnameParts = url.pathname.split('/');
  const id = pathnameParts[pathnameParts.length - 1];

  // 这里应该从数据库获取用户
  // const user = await getUserById(id);
  console.log(id);
  // 模拟数据
  if (id === '1') {
    return Response.json({
      user: { id: 1, name: 'Alice', email: 'alice@example.com' }
    });
  }

  return Response.json(
    { error: 'User not found' },
    { status: 404 }
  );
}

请求和响应处理

读取请求数据

// app/api/example/route.ts

export async function POST(request: Request) {
  // 1. 读取 JSON body
  const body = await request.json();
  
  // 2. 读取 URL 参数
  const { searchParams } = new URL(request.url);
  const query = searchParams.get('query');
  
  // 3. 读取 headers
  const contentType = request.headers.get('content-type');
  const authorization = request.headers.get('authorization');
  
  // 4. 读取 cookies(需要从 headers 手动解析或使用 next/headers)
  const cookieHeader = request.headers.get('cookie');
  
  return Response.json({ 
    body, 
    query, 
    contentType,
    authorization 
  });
}

返回响应

// app/api/example/route.ts

export async function GET() {
  // 1. 返回 JSON
  return Response.json({ message: 'Hello' });
  
  // 2. 返回 JSON 并设置状态码
  return Response.json({ error: 'Not found' }, { status: 404 });
  
  // 3. 返回文本
  return new Response('Hello, world!', {
    headers: { 'Content-Type': 'text/plain' },
  });
  
  // 4. 返回 HTML
  return new Response('<h1>Hello</h1>', {
    headers: { 'Content-Type': 'text/html' },
  });
  
  // 5. 重定向
  return Response.redirect('https://example.com');
  
  // 6. 设置自定义 headers
  return Response.json({ data: 'secret' }, {
    headers: {
      'Cache-Control': 'no-store',
      'X-Custom-Header': 'value',
    },
  });
}