配套两个示例工程:
spa-demo/:Vite + React 写的经典 SPAnext-demo/:Next.js 16(App Router)写的全栈项目把两个工程对着看,你会瞬间明白"服务端渲染"到底解决的是什么问题。
一、先分清三个长得很像的框架
刚入行时最容易被这三个名字绕晕,先用一张表钉死它们的身份:
| 框架 | 生态 | 定位 | 一句话记忆 |
|---|---|---|---|
| Next.js | React | 全栈框架 | 能写页面(前端),也能写 API(后端) |
| Nuxt | Vue | 全栈框架 | Vue 世界里的 Next.js |
| Nest | Node | 后端框架 | 纯后端,和"页面"没关系 |
📌 现在的主角是 Next.js:背靠 Vercel,SEO 做得极其出色,大量 AI 产品的官网都是它做的。
二、SPA 的黄金时代,和它的致命短板
✅ SPA 好在哪
- 体验极好 :组件在浏览器里挂载(
useEffect里请求数据),页面切换不刷新。 - 快得像原生 App:前端路由接管跳转,切换只换内容不换壳。
- 一套代码跑三端:HTML 只写一次,不用像原生那样 Android / iOS 写两套。
所以移动端时代,超级 App 里 **20% 原生 + 80% SPA(WebView)**已成为标配:App Store 的小红点、活动页、内容页,基本都是 WebView 里跑的 SPA。
❌ SPA 的短板
移动端它是王者,但一回到 PC + 搜索引擎的场景,它立刻失灵:
html
<!-- SPA 的 index.html 长这样,只有一个空壳 -->
<div id="root"></div>
<script src="/src/main.js"></script>
爬虫通过 URL 抓取页面时,拿到的只有这个空壳。真正的 <h1>、文章正文、商品信息,全部要等 main.js 在浏览器里跑完才出现------收录不了,也就没有 SEO。
在 PC 时代,搜索引擎是流量入口,SEO 就是命。做内容站(掘金、CSDN 这类)、做 AI 产品官网,流量几乎全部来自搜索。
如今又多了一层 GEO(Generative Engine Optimization):用户入口变成了豆包这类 AI 助手,你得让 AI 在生成答案时带上你的内容、甚至购买链接------而这同样依赖"内容能被机器读懂"。
结论:#root(SPA)无法承担 SEO,我们需要的是把 React JSX 直接编译成 HTML 的方案。
三、问题的本质:组件到底在哪儿渲染
所有 SEO 问题,最后都能归到一句话:组件在哪里渲染?
🖥️ CSR(Client Side Rendering)------组件在浏览器渲染
Server 前端项目所在服务器 → 只吐出 /index.html(空壳 #root)
Client 用户的浏览器 → main.js → App.jsx → Todos.jsx 依次执行
页面才真正出现
- 一条链路:
Server → index.html → Script → Client 挂载 - 爬虫爬到的:
#root+ 一行<script>,没有内容 - 典型代表:
spa-demo里的 Vite + React 项目
🌐 SSR(Server Side Rendering)------组件在服务端渲染
把服务端想象成一个"提前帮你把菜做好"的厨房:它不做 DOM,只做字符串拼接。
text
JSX + todos 数据 →(服务端编译)→ 完整的 HTML 字符串 → 直接返回给浏览器
对比一下传统 Java 全栈和 Next.js 全栈的分工差异:
| 环节 | 前后端分离(SPA + 后端 API) | 全栈(Next.js SSR) |
|---|---|---|
/todos 返回什么 |
一个空壳 HTML,数据靠二次请求 /api/todos |
已经渲染好的 HTML |
| 数据来源 | 浏览器里 fetch 拿 JSON 数组 |
服务端直接拿到数据参与渲染 |
| 爬虫看到什么 | 空壳 | 完整的页面内容 ✅ |
💡 一句话记住:CSR 是"客户端渲染",SSR 是"服务端渲染",SEO 的根本区别就在这两个词上。
四、Next.js:约定大于一切
Next.js 最大的特点不是 API 多,而是 约定大于配置 :不用手写路由表,文件就是路由。
📁 目录即路由
app/
├── layout.tsx # 根布局(所有页面的公共外壳)
├── page.tsx # 首页 → /
├── about/
│ └── page.tsx # 关于页 → /about
├── todos/
│ ├── page.tsx # 待办页 → /todos
│ └── type.ts # 类型定义(不参与路由)
├── dashboard/
│ ├── page.tsx # 后台 → /dashboard
│ └── settings/
│ ├── layout.tsx # 嵌套布局,只作用于 /dashboard/settings
│ └── page.tsx # 设置页 → /dashboard/settings
└── api/
└── todos/
└── route.ts # 数据接口 → /api/todos
创建项目只需要一条命令,然后一路回车选默认配置(TypeScript + Tailwind CSS + ESLint):
bash
npx create-next-app@latest
🧩 一个请求进来,Next.js 按什么顺序渲染?
以访问 /about 为例:
请求 /about
↓
① 先套上 layout.tsx(公共导航等)
↓
② 再渲染 about/page.tsx
↓
③ 组装成完整 HTML 返回
根布局:所有页面共用的外壳
app/layout.tsx 里写一次导航,全站生效------这就是"布局文件"的价值:
tsx
// app/layout.tsx
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
<body className="min-h-full flex flex-col">
<nav>
<ul>
<li><Link href="/">首页</Link></li>
<li><Link href="/about">关于</Link></li>
<li><Link href="/dashboard">后台管理系统</Link></li>
<li><Link href="/todos">待办事项</Link></li>
</ul>
</nav>
{children} {/* 具体页面在这里被塞进来 */}
</body>
</html>
);
}
默认的页面组件 = 服务端组件
tsx
// app/page.tsx
// 服务器端组件:在服务器端以 react node 的方式运行,jsx -> html
export default function Home() {
return (
<h1>Hello World</h1>
);
}
✨ 注意这里没有任何 "use client" ,也没有
useState------它就是纯纯的"函数 + 数据 = HTML",也是被搜索引擎收录的那部分。
嵌套路由同样靠目录结构表达,dashboard/settings 下的布局只影响它自己和子节点:
tsx
// app/dashboard/settings/layout.tsx
export default function DashboardLayout({ children }: LayoutProps<"/dashboard/settings">) {
return (
<div>
<nav>
<ul>
<li><Link href="/dashboard/settings">Settings</Link></li>
</ul>
</nav>
{children}
</div>
);
}
五、客户端组件:'use client' 与 hydration
SSR 解决了 SEO,但有些页面是强交互 的(输入、点击、请求接口)。这时候就要请出 'use client'。
🥟 用"速冻水饺"理解它
包装好的水饺,冻上,给你送过来 → 服务端先渲染出静态 HTML
你拿到手,下锅水煮 + 蘸水 → 浏览器挂载 JS、绑定事件、激活交互
'use client' 不等于"只在浏览器渲染",而是:
先在服务端把能渲染的部分渲染完(首屏 HTML 有了,SEO 有了),再到客户端渲染一次、打上补丁。
这个"补丁"过程就叫 hydration(注水) :浏览器拿到静态 HTML 后,挂载客户端 JS、绑定点击事件、激活交互。也因此,CSR 组件会执行两次------一次在服务器,一次在客户端。
一个完整的例子:待办事项页
tsx
// app/todos/page.tsx
'use client';
// 组件在前端渲染的标记:加了它才能用事件、useEffect、调用后端接口
import { useState, useEffect } from 'react';
import { type Todo } from './type';
export default function TodosPage() {
const [todos, setTodos] = useState<Todo[]>([]);
const [text, setText] = useState<string>("");
const fetchTodos = async () => {
const res = await fetch("/api/todos");
const data: Todo[] = await res.json();
setTodos(data);
};
const handleAdd = async () => {
if (!text.trim()) return;
const res = await fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: text, completed: false }),
});
const data: Todo = await res.json();
setTodos([...todos, data]);
setText("");
fetchTodos();
};
useEffect(() => {
fetchTodos();
}, []);
// ......渲染列表
}
配上一个类型文件,全链路都有类型保护:
ts
// app/todos/type.ts
export type Todo = {
id: number;
content: string;
completed: boolean;
}
六、全栈:route.ts 就是后端
Next.js 里,除了标了 use client 的,其余默认都在服务端。所以写后端接口不需要另起一个服务,一个文件就够。
约定:app/api/**/route.ts 导出 GET / POST 等函数,就是一个 RESTful 接口。
ts
// app/api/todos/route.ts
import { type Todo } from "../../todos/type";
let todos: Todo[] = [
{ id: 1, content: '学习App Router', completed: true },
{ id: 2, content: 'next.js 个人官网开发', completed: false },
];
// GET /api/todos
export async function GET() {
// next.js 封装了 Response 类,直接返回 json
return Response.json(todos);
}
// POST /api/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);
}
前端调用只需要一个相对路径,前后端在同一个工程里:
ts
const res = await fetch("/api/todos"); // 查询
await fetch("/api/todos", { method: "POST", ... }); // 新增
🎯 这就是"全栈项目"最直观的样子:
/todos返回编译好的 HTML,/api/todos返回 JSON,同一个 Next.js 服务同时提供两者。
七、SEO 的三层语法
SEO 不是玄学,它是有层次的:
第一层:说清"你是谁" ------ 元信息
标题做什么用?描述有什么价值?关键词是什么?
tsx
<title>名字</title>
<meta name="description" content="这是一个描述" />
<meta name="keywords" content="这是一个关键词" />
在 App Router 里,这些通过 metadata 导出,Next.js 会自动写入 <head>:
tsx
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
第二层:做内容 ------ 用户来的真正原因
元信息只是"招牌",真正决定排名的是页面里的内容。内容站(掘金、CSDN)的流量,本质上都来自这里。
第三层:SSR ------ 让机器读得懂内容
像 /post/:id 这种"一个模板 × 千万篇文章"的场景,SSR 让整站内容都能被爬虫收录,收录量本身就是搜索引擎给你的加权。
第一层 元信息 → 第二层 好内容 → 第三层 SSR 全量收录
你是谁 凭什么来 能被读到
八、总结:一张表收尾
| 维度 | SPA(spa-demo) |
Next.js 全栈(next-demo) |
|---|---|---|
| 渲染位置 | Client 浏览器 | Server 服务端 + Client hydration |
| SEO | ❌ 空壳 #root |
✅ 首屏就是完整 HTML |
| 路由 | 手写 <Route path="/todos" element={<Todos />} /> |
文件即路由,app/todos/page.tsx |
| 接口 | 另起后端服务 | app/api/todos/route.ts 一个文件 |
| 交互 | useState / useEffect |
同样是 React,但需要 'use client' 声明 |
| 适用场景 | App 内嵌页、强交互后台 | 官网、内容站、需要 SEO 的产品 |
三句话记住这套体系:
- 🧭 SEO 的根本是"组件在哪里渲染"------CSR 在浏览器,SSR 在服务端。
- 📁 Next.js 的核心 是"约定大于一切"------文件即路由,
layout管壳,page管内容。 - 🥟 客户端组件是"速冻水饺"------服务端先渲染好再送来,客户端注水(hydration)激活交互。
Next.js 就是给 React 开发者的开箱即用利器:写一套代码,同时拿到好体验和好 SEO。