Next.js App Router 实战入门:从 SPA 到 SSR 的全栈思维转变

12 阅读6分钟

摘要

从SPA的CSR困境出发,深入Next.js约定式路由、Server/Client组件边界、嵌套布局、route.ts全栈API及hydration水合机制,用Todo示例打通前后端一体化开发。


React 开发者从 SPA 转向 Next.js,最需要跨越的障碍不是语法,而是思维模型——从"一切都发生在浏览器里"切换到"代码在服务端和客户端各执行一次"。Next.js 将 React 从纯前端框架升级为全栈框架,既能写页面,也能写 API,背后是 Vercel 对 SSR 和 SEO 的深度优化。大量 AI 产品选择 Next.js 做官网,正是因为它在搜索引擎可见性上的天然优势。

SPA 的 SEO 困境与 SSR 的解法

传统 React SPA 的渲染流程是:浏览器请求 index.html → 解析 <div id="root"></div> → 加载 main.js → React 在客户端挂载组件树 → 页面可见。这个过程称为 CSR(Client Side Rendering)。

搜索引擎爬虫访问 SPA 页面时,看到的是一个空的 #root 节点和一堆 <script> 标签。虽然 Google 宣称能执行 JavaScript,但实际效果远不如直接抓取 HTML 内容。对于内容型网站(掘金、CSDN 等),流量高度依赖 SEO,CSR 是致命短板。

Next.js 的解法是 SSR(Server Side Rendering):请求到达服务端后,React 组件在 Node.js 环境中执行,JSX 被编译为完整的 HTML 字符串返回给浏览器。爬虫拿到的就是一个包含所有内容的 HTML 文档,和传统多页应用一样友好。

从 CSR 到 SSR 的转变,本质上是组件渲染位置的变化——之前是浏览器,现在是服务端

App Router:约定大于配置的文件路由

Next.js 13 引入的 App Router 用文件夹和文件替代了 React Router 的 <Routes> + <Route> 配置。一个路径对应一个文件夹,文件夹内的 page.tsx 自动成为该路径的页面组件:

app/
├── layout.tsx          # 根布局(全局导航、字体、metadata)
├── page.tsx            # / 首页
├── about/
│   └── page.tsx        # /about
├── dashboard/
│   ├── layout.tsx      # /dashboard 的嵌套布局
│   ├── page.tsx        # /dashboard
│   └── settings/
│       └── page.tsx    # /dashboard/settings
├── todos/
│   ├── page.tsx        # /todos
│   └── types.ts        # 类型定义
└── api/
    └── todos/
        └── route.ts    # /api/todos(API 接口)

不需要 react-router-dom,不需要手动配置路由表。文件结构本身就是路由表。layout.tsx 定义共享布局——根布局中的 <nav> 和字体配置对所有页面生效,dashboard/layout.tsx 中的导航只对 /dashboard/* 子路由生效。

根布局:SEO 元数据与全局框架

根布局不仅是 UI 的骨架,也是 SEO 的入口。metadata 导出定义了标题、描述等搜索引擎抓取的关键信息:

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }: LayoutProps<"/">) {
  return (
    <html lang="en">
      <body>
        <nav>
          <Link href="/">首页</Link>
          <Link href="/about">关于</Link>
          <Link href="/dashboard">后台</Link>
        </nav>
        {children}
      </body>
    </html>
  );
}

metadata 对象在服务端渲染时被编译为 <title><meta> 标签,直接输出到 HTML 中。这就是 SEO 的第一层——告诉搜索引擎你是谁、做什么、提供什么价值。<Link> 组件来自 next/link,在 App Router 中替代了 React Router 的 <Link>,同样支持客户端导航,不会触发整页刷新。

Server Component:默认在服务端渲染

App Router 中所有组件默认都是服务端组件。它们不标记 'use client',在服务端执行,JSX 被编译为 HTML 后直接返回:

// page.tsx —— 服务端组件,默认行为
export default function Home() {
  return <h1>Hello World</h1>;
}

这个组件没有 useState、没有 useEffect、没有事件监听。它就是一个纯函数——接收数据,返回 JSX,在服务端编译为 <h1>Hello World</h1> 字符串。服务端组件不能使用浏览器 API(windowdocument),不能包含交互逻辑,但可以直接访问数据库、文件系统等 Node.js 能力。

Client Component:需要交互时声明 'use client'

当页面需要状态管理、事件监听或浏览器 API 时,在文件顶部添加 'use client' 指令:

'use client';

import { useState, useEffect } from 'react';
import { type Todo } from './types';

export default function TodosPage() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [text, setText] = useState("");

  const fetchTodos = async () => {
    const res = await fetch('/api/todos');
    const data: Todo[] = await res.json();
    setTodos(data);
  };

  useEffect(() => {
    fetchTodos();
  }, []);

  const handleAdd = async () => {
    if (!text.trim()) return;
    await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content: text, completed: false }),
    });
    fetchTodos();
  };

  return (
    <div>
      <h1>待办事项</h1>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={handleAdd}>添加</button>
      <ul>
        {todos.map((item) => (
          <li key={item.id}>
            <span style={{ textDecoration: item.completed ? 'line-through' : 'none' }}>
              {item.content}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

'use client' 不意味着组件只在浏览器运行。Next.js 仍然会先在服务端预渲染这个组件的初始 HTML(静态内容部分),然后发送到客户端,客户端 JavaScript 接管后绑定事件、激活状态——这个过程称为 hydration(水合)。

把 hydration 比作速冻水饺:服务端把"馅"(数据)包进"皮"(HTML 结构),冻结后送到客户端;客户端用 JavaScript 的"火"把水饺煮熟,激活所有交互。组件实际上执行了两次——一次在服务端生成静态 HTML,一次在客户端"打补丁"绑定事件。

API Route:在同一个项目中写后端

Next.js 的全栈能力体现在 route.ts 文件上。app/api/todos/route.ts 自动映射为 /api/todos 接口,导出按 HTTP 方法命名的函数:

import { type Todo } from '../../todos/types';

let todos: Todo[] = [
  { id: 1, content: '学习AppRouter', completed: true },
  { id: 2, content: 'next.js 个人官网开发', completed: false },
];

export async function GET() {
  return Response.json(todos);
}

export async function POST(req: Request) {
  const body = await req.json();
  const newTodo: Todo = {
    id: +Date.now(),
    content: body.content,
    completed: false,
  };
  todos.push(newTodo);
  return Response.json(newTodo);
}

export async function GET() 处理 GET 请求,export async function POST() 处理 POST 请求。Response.json() 是 Next.js 封装的便捷方法,等价于 new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })

前端组件通过 fetch('/api/todos') 调用这个接口。注意这里用的是相对路径——因为 Next.js 的前后端运行在同一个服务上,不存在跨域问题。以前用 React + Vite 开发时,前端在 localhost:5173,后端在 localhost:3000,需要配置代理或 CORS;在 Next.js 中,前端和后端在同一个项目、同一个端口下协作,这就是全栈框架的核心优势。

嵌套布局:分区域复用 UI 结构

/dashboard 目录下有自己的 layout.tsx,为子路由页面提供独立的导航区域:

import Link from "next/link";

export default function DashboardLayout({ children }: LayoutProps<"/dashboard">) {
  return (
    <div>
      <nav>
        <Link href="/dashboard/settings">Settings</Link>
      </nav>
      {children}
    </div>
  );
}

当访问 /dashboard 时,渲染顺序是:RootLayoutDashboardLayoutDashboardPage。当访问 /dashboard/settings 时,DashboardLayout 保持不变,只有 {children} 部分替换为 SettingsPage。这种嵌套布局机制让页面共享 UI 变得极其自然——不需要像 React Router 那样手动嵌套 <Outlet>

CSR vs SSR vs 全栈:渲染位置的全景对比

维度CSR(SPA)SSR(Next.js)Next.js API Route
组件渲染位置浏览器服务端不涉及
首屏 HTML空的 #root完整的 JSX→HTMLJSON 响应
SEO不直接面向用户
数据获取useEffect + fetch组件内直接 awaitreq.json()
需要 use client默认就是客户端按需声明不需要
文件约定page.tsxroute.ts

总结

Next.js App Router 用"约定大于配置"的设计,将 React 从纯前端框架扩展为全栈框架。服务端组件默认在服务端渲染 JSX→HTML 解决 SEO,客户端组件通过 'use client' 和 hydration 保留交互能力,route.ts 让前后端在同一项目中协作,嵌套 layout.tsx 让 UI 复用自然发生。

对于 React 开发者而言,最大的转变是从"所有代码都在浏览器跑"切换到"先想清楚这段代码应该在哪里跑"。服务端组件负责静态内容和数据获取,客户端组件负责交互和状态——这个边界的清晰划分,是 Next.js 全栈开发的核心思维。