摘要
从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 自动成为该路径的页面组件:
bash
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 导出定义了标题、描述等搜索引擎抓取的关键信息:
tsx
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 后直接返回:
tsx
// page.tsx ------ 服务端组件,默认行为
export default function Home() {
return <h1>Hello World</h1>;
}
这个组件没有 useState、没有 useEffect、没有事件监听。它就是一个纯函数------接收数据,返回 JSX,在服务端编译为 <h1>Hello World</h1> 字符串。服务端组件不能使用浏览器 API(window、document),不能包含交互逻辑,但可以直接访问数据库、文件系统等 Node.js 能力。
Client Component:需要交互时声明 'use client'
当页面需要状态管理、事件监听或浏览器 API 时,在文件顶部添加 'use client' 指令:
tsx
'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 方法命名的函数:
ts
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,为子路由页面提供独立的导航区域:
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 时,渲染顺序是:RootLayout → DashboardLayout → DashboardPage。当访问 /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→HTML | JSON 响应 |
| SEO | 差 | 好 | 不直接面向用户 |
| 数据获取 | useEffect + fetch |
组件内直接 await |
req.json() |
需要 use client |
默认就是客户端 | 按需声明 | 不需要 |
| 文件约定 | 无 | page.tsx |
route.ts |
总结
Next.js App Router 用"约定大于配置"的设计,将 React 从纯前端框架扩展为全栈框架。服务端组件默认在服务端渲染 JSX→HTML 解决 SEO,客户端组件通过 'use client' 和 hydration 保留交互能力,route.ts 让前后端在同一项目中协作,嵌套 layout.tsx 让 UI 复用自然发生。
对于 React 开发者而言,最大的转变是从"所有代码都在浏览器跑"切换到"先想清楚这段代码应该在哪里跑"。服务端组件负责静态内容和数据获取,客户端组件负责交互和状态------这个边界的清晰划分,是 Next.js 全栈开发的核心思维。