从零理解 Next.js:SPA 的 SEO 短板与 SSR 全栈方
很多前端同学第一次接触 Next.js,都会被两个问题绕晕:"SPA 不是体验很好吗?为什么还要搞个 Next.js?" 以及 "
'use client'到底是什么意思,组件到底在哪里渲染?" 答案分别藏在两个词里------SEO 和 水合(Hydration)。这篇文章用大白话 + 一个能跑通的前后端 Demo,把 SPA 的 SEO 短板、SSR 的原理、'use client'的真相,以及一个完整的全栈闭环(route.ts接口 +page.tsx页面)一次讲清。
前言
最近在学 Next.js,老师在开讲之前先花了一大段讲 SEO。我一开始很不理解:"我又不做运营,学个框架跟 SEO 有啥关系?" 后来才明白,理解不了 SEO,就理解不了 Next.js 为什么存在。踩了不少坑,写下来希望能帮到和我一样刚入门的同学。
你将会收获:
- 🎯 SEO 到底是什么,为什么 PC 时代它是"命"
- 🔑 SPA 为什么天生没有 SEO(
#root空壳的原理) - ⚡ CSR 和 SSR 的本质区别------组件到底在哪里渲染
- 🧩 Next.js 的"文件即路由"和"嵌套布局"
- 🔍
'use client'的真相------Client Component 到底是不是"只在浏览器渲染" - 🥟 水合(Hydration)是什么,用"饺子"比喻一次讲透
- 🛠️ 一个完整可跑的全栈 Demo:
route.ts接口 +page.tsx页面 + 共享类型打通 - 📌 SEO 的三层做法
技术栈: Next.js(App Router)+ TypeScript + Tailwind CSS + React
目录
- [一、SEO 到底是什么?用大白话讲清楚](#一、SEO 到底是什么?用大白话讲清楚 "#%E4%B8%80seo-%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E7%94%A8%E5%A4%A7%E7%99%BD%E8%AF%9D%E8%AE%B2%E6%B8%85%E6%A5%9A")
- [二、SPA 为什么没有 SEO](#二、SPA 为什么没有 SEO "#%E4%BA%8Cspa-%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B2%A1%E6%9C%89-seo")
- [三、Next.js 怎么补上:SSR 的原理](#三、Next.js 怎么补上:SSR 的原理 "#%E4%B8%89nextjs-%E6%80%8E%E4%B9%88%E8%A1%A5%E4%B8%8Assr-%E7%9A%84%E5%8E%9F%E7%90%86")
- [四、Next.js 语法:约定大于一切](#四、Next.js 语法:约定大于一切 "#%E5%9B%9Bnextjs-%E8%AF%AD%E6%B3%95%E7%BA%A6%E5%AE%9A%E5%A4%A7%E4%BA%8E%E4%B8%80%E5%88%87")
- [五、'use client' 到底是什么意思?](#五、'use client' 到底是什么意思? "#%E4%BA%94use-client-%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E6%84%8F%E6%80%9D")
- [六、一个完整的全栈 Demo:待办事项](#六、一个完整的全栈 Demo:待办事项 "#%E5%85%AD%E4%B8%80%E4%B8%AA%E5%AE%8C%E6%95%B4%E7%9A%84%E5%85%A8%E6%A0%88-demo%E5%BE%85%E5%8A%9E%E4%BA%8B%E9%A1%B9")
- [七、SEO 的三层做法](#七、SEO 的三层做法 "#%E4%B8%83seo-%E7%9A%84%E4%B8%89%E5%B1%82%E5%81%9A%E6%B3%95")
- [八、常见坑:
{children}丢了](#八、常见坑:{children} 丢了 "#%E5%85%AB%E5%B8%B8%E8%A7%81%E5%9D%91children-%E4%B8%A2%E4%BA%86") - [九、面试高频 4 问](#九、面试高频 4 问 "#%E4%B9%9D%E9%9D%A2%E8%AF%95%E9%AB%98%E9%A2%91-4-%E9%97%AE")
- 总结
一、SEO 到底是什么?用大白话讲清楚
先讲痛点:没有 SEO 会怎样
想象你开了一家饭店,但开在一条没有招牌、地图上搜不到的小巷子里。菜做得再好,也没人找得到你,只能靠老顾客口口相传。
网站没有 SEO 就是这个处境:你的内容再好,搜索引擎(百度、谷歌)搜不到、排不上,就等于没流量。
SEO 是什么
SEO(Search Engine Optimization,搜索引擎优化):让搜索引擎"看懂"你的页面、"信任"你的网站,从而在用户搜索时把你排在前面。
arduino
用户搜"怎么做红烧肉"
↓
搜索引擎抓取全网网页 → 打分排序
↓
排前面的 → 点进来的人多 → 免费流量大
💡 一句话记住:SEO = 让搜索引擎"看懂"你 + "信任"你。
为什么 PC 时代 SEO 是"命"
PC 时代,用户上网的入口是搜索引擎。打开电脑 → 打开百度/谷歌 → 搜 → 点进去。所以:
流量 = 搜索排名 = 网站的命。
像掘金、CSDN 这些老牌内容网站,主要流量都来自 SEO------你搜技术问题,它们排在前面,你就点进去了。
到了移动端时代,入口变成了超级 App(微信、抖音),才催生了大量"App 化"的 SPA 应用。但做内容、做官网、做 AI 产品站,SEO 依然是命脉。
🔥 现在 AI 时代的 To C 产品多如牛毛 ,各种 AI Agent 产品站点,都靠官网 + SEO 引流。甚至出现了 GEO(生成式引擎优化)------让豆包、Kimi 这类 AI 在回答时"带上你的内容和购买链接"。入口变了,但"被搜到"的本质没变。
二、SPA 为什么没有 SEO
SPA(Single Page Application,单页应用)体验确实好:不刷新页面、切换快、像原生 App。但它天生就跟 SEO 八字不合。
SPA 的工作方式
SPA 的 HTML 文件,长这样(这是 Vite 项目的 index.html):
html
<!DOCTYPE html>
<html lang="en">
<head>
<title>spa-demo</title> <!-- 只有一个默认标题 -->
</head>
<body>
<div id="root"></div> <!-- ⭐ 空的! -->
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
看那个 <div id="root"></div>------它是空的 。真正的文字内容(文章、商品、链接)都在 main.jsx 引用的 React 组件里,要等浏览器执行 JS 之后才会被"塞"进这个 div。
关键:爬虫不执行 JS
less
浏览器(会执行 JS):
拿到 HTML → 执行 main.jsx → 内容渲染出来 ✅
爬虫(不执行 JS):
拿到 HTML → 不执行 JS → 只看到 <div id="root"></div> ❌
🏠 一句话类比 :SPA 的 HTML 是一个空房子 + 一张"装修图纸在 JS 里"的纸条。浏览器会照着图纸把家具摆好;爬虫只看了看空房子,就走了。
用一张图看清 CSR
arduino
CSR(Client Side Rendering,客户端渲染):
┌──────────┐ ①请求 ┌──────────┐
│ 浏览器 │ ───────> │ 服务器 │
│ (客户端) │ <─────── │ │
└──────────┘ ②返回空壳 └──────────┘
│ (index.html #root)
│ ③执行 JS
│ ④才渲染出内容
▼
用户看到页面(但爬虫早走了)
所以 SPA 的渲染方式叫 CSR(客户端渲染) ------组件在浏览器里渲染。
| 谁来渲染 | 爬虫看到 | SEO | |
|---|---|---|---|
| SPA / CSR | 浏览器(客户端) | 空壳 #root |
❌ 差 |
| Next.js / SSR | 服务器 | 完整 HTML | ✅ 好 |
⚠️ 面试考点 :CSR 和 SSR 的根本区别,不是"速度快慢",而是组件到底在哪里渲染------在浏览器就是 CSR,在服务器就是 SSR。SEO 好不好,全看这一条。
三、Next.js 怎么补上:SSR 的原理
先回到 Java 全栈的"老办法"
其实 SSR 很早就有了。回想 Java 全栈的流程:
bash
浏览器 → server:3000 → /todos 路由 → controller → service → MySQL
↓
拿到 todos 数据
↓
塞进模板(JSP/Thymeleaf)渲染
↓
返回完整 HTML ✅
因为 HTML 是服务器拼好了再发的,所以爬虫一来就能抓到完整内容 → SEO 好。
React 本来做不了这件事
问题在于:React 天生设计成只能在浏览器里跑。它没有"在服务器上先渲染成 HTML"这个能力,所以 React 开发者一直拿不到 SSR 的好处。
Next.js 干的事,就是把 SSR 的能力补给了 React。
关键认知:React 组件其实"也是模板"
一个 React 组件本质上是一个纯函数:
tsx
// 输入:数据 输出:模板(JSX)
function TodosPage({ todos }) {
return (
<ul>
{todos.map(t => <li key={t.id}>{t.title}</li>)}
</ul>
);
}
进去的是 todos 数据,出来的是模板。这跟 Java 里的模板引擎(JSP/Thymeleaf)是同一个东西 。所以------既然它是纯函数,为什么不能在服务器(Node.js)上跑?
Next.js 就是这么干的:
css
浏览器访问 /todos
│
▼
Next.js 服务器 (Node.js) ← 全在服务器上,浏览器没参与
│
├─ ① 路由匹配:app/todos/page.tsx
├─ ② 执行组件函数,查 todos 数据
├─ ③ JSX + 数据 → 编译成 HTML
└─ ④ 套进 layout,拼成完整 HTML
│
▼
返回完整 HTML 给浏览器 ✅
│
▼
浏览器直接显示(不用跑 JS 就有内容)→ 爬虫也能抓到 → SEO 好
🍜 一句话类比:CSR 是"给你一包方便面,自己泡";SSR 是"餐厅现炒现卖,端上来就能吃"。
但有个前提:服务器上没有"浏览器"
服务器上跑的是 Node.js,没有用户点击、没有 DOM、没有窗口。所以:
| ✅ 能用在服务器组件里 | ❌ 不能用在服务器组件里(浏览器专属) |
|---|---|
| 纯函数、查数据、写 JSX | onClick 事件监听 |
fetch 请求数据 |
useEffect |
await 查数据库 |
useState 交互状态 |
所以 Next.js 把组件分成两类:
arduino
Server Component(服务器组件) Client Component(客户端组件)
- 默认就是 - 文件顶部写 'use client'
- 能查数据、直接出 HTML - 能用 onClick / useEffect / useState
- 负责 SEO - 负责交互
四、Next.js 语法:约定大于一切
Next.js 最反直觉的一点是:它靠"约定"而不是"配置" 。你不用写路由表,文件夹结构本身就是路由表。
文件即路由
规则就两条:
arduino
规则1:app/ 文件夹是"路由的根"
规则2:文件夹名 → 网址路径,page.tsx → 页面内容
tsx
// app/page.tsx ------ 服务器端组件
// 在服务器端运行,把 JSX 编译成 HTML
export default function Home() {
return (
<>
<h1>Hello World</h1>
</>
);
}
tsx
// app/about/page.tsx
export default function About() {
return (
<>
<h1>About Us</h1>
</>
);
}
项目结构对网址的映射关系:
bash
app/
├─ layout.tsx → 所有页面的"外壳"
├─ page.tsx → 网址 / → "Hello World"
├─ about/
│ └─ page.tsx → 网址 /about → "About Us"
└─ dashboard/
├─ layout.tsx → dashboard 的"外壳"
├─ page.tsx → 网址 /dashboard
└─ settings/
└─ page.tsx → 网址 /dashboard/settings
| 你在浏览器访问 | Next.js 去找 | 渲染出 |
|---|---|---|
/ |
app/page.tsx |
Hello World |
/about |
app/about/page.tsx |
About Us |
/dashboard |
app/dashboard/page.tsx |
后台管理系统 |
🔑 为什么"加了 /about 就会变" :网址
/about由文件夹名about决定;这个文件夹里必须有个叫page.tsx的文件,Next.js 才认它是个页面并渲染出来。两个约定缺一不可。
对比 SPA 的手写路由
SPA 要手动写路由表:
jsx
// ❌ SPA:每个网址和组件的对应关系,要自己一条条写
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
Next.js 不需要这张表 。它启动时自动扫描 app/ 文件夹,看到 about/page.tsx,就自动知道"哦,/about 对应这个页面"。
嵌套布局:一组页面共用的"外壳"
layout.tsx 是"外壳",它自己会渲染,{children} 是留的洞,子页面内容填进来:
tsx
// app/layout.tsx ------ 根布局,套所有页面
import Link from "next/link";
export const metadata = {
title: "我的 Next.js Demo",
description: "一个 SEO 友好的全栈示例",
};
export default function RootLayout({ children }) {
return (
<html lang="zh">
<body>
<nav>
<ul>
<li><Link href="/">首页</Link></li>
<li><Link href="/about">关于</Link></li>
<li><Link href="/dashboard">后台</Link></li>
</ul>
</nav>
{children}
</body>
</html>
);
}
布局是可以"套娃"的。比如给 dashboard 这一组页面加个专属导航栏:
tsx
// app/dashboard/layout.tsx ------ 第二层外壳,只套 dashboard 这组页面
import Link from "next/link";
export default function DashboardLayout({ children }) {
return (
<div>
<nav>
Nav
<Link href="/dashboard/settings">Settings</Link>
</nav>
{children}
</div>
);
}
渲染后的效果:
| 访问网址 | 渲染结果 |
|---|---|
/dashboard |
Nav + 后台管理系统 |
/dashboard/settings |
Nav + settings 后台管理系统 |
/about |
只有 About Us(没有 Nav) |
看到了吗------nav 只出现在 dashboard 这组页面里,不用在每个页面重复写。这就是嵌套布局的意义:一组页面共用的东西,写一次就够了。
⚠️ layout 里的东西也会"打印"出来 :
<html>、<body>、<nav>这些不是配置,是真的会被输出成 HTML 的。只有{children}不是它自己,而是子页面内容填进来。
一个容易懵的点:"服务器"在哪?
很多同学会懵:"我明明在本地写代码,哪来的服务器?"
真相是:你运行 npm run dev 的那一刻,Next.js 会在你自己的电脑上启动一个 Node.js 进程,这个进程就是"服务器"。
arduino
你的电脑
├─ ① 浏览器(客户端) ← 打开 localhost:3000 看网页
└─ ② Node.js 进程(服务器端) ← npm run dev 启动的,负责渲染 HTML
浏览器访问 localhost:3000/about,请求在你自己电脑内部转了一圈:浏览器 → Node 进程(渲染 HTML)→ 返回浏览器。所谓"服务器端",指的就是"代码在 ② 这个 Node 进程里运行,而不是在 ① 浏览器里运行"。
五、'use client' 到底是什么意思?
一个最常见的误解
第三节讲过,Next.js 把组件分成两类:Server Component(默认,负责出 HTML / SEO) 和 Client Component(顶部写 'use client',负责交互) 。但很多同学一看到 'use client',第一反应就错了:
❌ "这个组件只在浏览器里渲染,服务器根本不碰它。"
这个理解错得很关键。老师上课反复强调的那句话,才是对的:
✅
'use client'不是"只在浏览器渲染",而是"这个组件有交互、需要在浏览器运行"------它先在服务器把能渲染的渲染完,再交给客户端接着渲染。
它的完整生命周期
arduino
'use client' 组件的完整生命周期:
① 服务器端 先把 JSX 渲染成 HTML(能提前渲染的先渲染完)
↓
② 浏览器 拿到静态 HTML,立刻有内容可看
↓
③ 浏览器 加载 JS,把事件、状态"接上去"
↓
④ 页面"活"了,可以点击、可以交互
水合(Hydration):页面"活"起来的那一步
第 ③ 步有个专业名词叫 Hydration(水合),老师的注释写得很到位:
水合(hydration):浏览器拿到静态 HTML 之后,挂载客户端 JS,绑定点击事件、激活交互。
用老师的"饺子"比喻最好懂:
| 步骤 | 饺子比喻 | 对应技术 |
|---|---|---|
| 包好饺子、冻上 | 服务器把 JSX 渲染成 HTML | SSR 服务端渲染 |
| 给你送过来 | 把静态 HTML 发给浏览器 | 返回 HTML |
| 你拿去水煮 | 浏览器绑定事件、激活交互 | Hydration 水合 |
饺子既不是"生"的,也不是"熟"的------服务器把饺子包好冻上送过来,浏览器负责最后煮熟。 这就是 Next.js 组件的运行真相。
组件会"执行两次"
所以一个 'use client' 组件,代码会跑两次:
CSR 组件会执行两次:第一次在服务器(生成 HTML),第二次在客户端(挂载事件、激活交互),像打补丁的感觉。
| 执行位置 | 干什么 | 产出 |
|---|---|---|
| 第 1 次:服务器 | 执行组件函数,JSX → HTML | 静态 HTML(SEO 友好) |
| 第 2 次:客户端 | 跑 JS,绑 onClick、跑 useEffect | 可交互页面 |
⚠️ 面试考点 :问"Client Component 是不是只在浏览器渲染?"------不是。它先服务器渲染(SSR)再客户端水合(Hydration),所以 Next.js 的 Client Component 首屏 HTML 也是完整的,照样吃得到 SEO 的好处。
什么时候必须写 'use client'
一句话判断(第三节那张表的延伸):
只要用了浏览器专属的能力------
useState、useEffect、onClick事件、fetch调接口------就必须在文件顶部写'use client'。 不写,Next.js 会直接报错,因为这些 Hook 只能在浏览器里跑。
六、一个完整的全栈 Demo:待办事项
光讲概念太虚。下面用老师课上这个"待办事项"例子,把 Server Component(接口)+ Client Component(页面)+ 共享类型 串成一个完整的前后端应用。
三个文件的分工
scss
┌─────────────────────────────────────────────┐
│ app/todos/type.ts 共享类型(前后端通用) │
│ id / content / completed │
└─────────────────────────────────────────────┘
↑ 被下面两个文件 import
┌──────────────────┐ fetch("/api/todos") ┌──────────────────┐
│ app/todos/ │ ─────────────────────▶ │ app/api/todos/ │
│ page.tsx │ │ route.ts │
│ 'use client' │ ◀───────────────────── │ GET() / POST() │
│ 前端 UI(交互) │ res.json() → setTodos │ 后端接口(数据) │
└──────────────────┘ └──────────────────┘
Client Component(有交互) Server Component(默认,无 'use client')
第一步:共享类型 type.ts
前后端都要用到 Todo 这个结构,抽出来放一起,两边 import,避免各写一份:
ts
// app/todos/type.ts ------ 前后端共享的数据结构
export type Todo = {
id: number; // 唯一标识,React 列表渲染要当 key 用
content: string; // 待办内容
completed: boolean; // 是否完成
};
第二步:后端接口 route.ts
ts
// app/api/todos/route.ts ------ 纯后端,没有 'use client'
// 注意:文件放在 app/api/ 下,就成了数据接口,而不是页面
import { type Todo } from '../../todos/type';
// 内存里的"假数据库",真实项目会换成查 MySQL
let todos: Todo[] = [
{ id: 1, content: '学习AppRouter', completed: true },
{ id: 2, content: 'next.js 个人官网开发', completed: false },
];
// 约定:导出一个叫 GET 的函数 = 处理 HTTP 的 GET 请求(RESTful)
export async function GET() {
// Response.json 是 Next.js 封装好的:把数组转成 JSON 并带上响应头
return Response.json(todos);
}
// 约定:导出一个叫 POST 的函数 = 处理 HTTP 的 POST 请求
export async function POST(req: Request) {
const body = await req.json(); // 拿到前端 POST 过来的 JSON 体
const newTodo: Todo = {
id: +Date.now(), // 用时间戳当 id(简陋但够用)
content: body.content, // 前端传来的内容
completed: false, // 新加的默认未完成
};
todos.push(newTodo); // 塞进"假数据库"
return Response.json(newTodo); // 把新建的这条返回给前端
}
关键点:导出的函数名就是 HTTP 方法名 。GET 处理 GET 请求,POST 处理 POST 请求,这是 Next.js 的 RESTful 约定,非常直白。
第三步:前端页面 page.tsx
tsx
// app/todos/page.tsx ------ 有交互,所以顶部写 'use client'
'use client';
import { useEffect, useState } 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"); // 请求我们刚写的 GET 接口
const data: Todo[] = await res.json();
setTodos(data); // 存进 state → 触发重新渲染
};
// 新增一条待办
const handleAdd = async () => {
if (!text.trim()) return; // 空内容直接忽略
await fetch("/api/todos", { // 请求 POST 接口
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: text }), // 把输入框内容发给后端
});
setText(""); // 清空输入框
await fetchTodos(); // 重新拉一遍,让新条目显示出来
};
useEffect(() => {
fetchTodos(); // 页面挂载时拉一次数据
}, []);
return (
<div style={{ marginTop: "12px" }}>
<h1>待办事项</h1>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="请输入新的待办事项"
/>
<button onClick={handleAdd} style={{ marginLeft: "8px" }}>添加</button>
<ul style={{ paddingLeft: "0", listStyle: "none" }}>
{todos.map((item) => (
<li key={item.id} style={{ margin: "8px 0", display: "flex", gap: "10px" }}>
<span style={{
textDecoration: item.completed ? "line-through" : "none",
cursor: "pointer",
}}>
{item.content}
</span>
<button>删除</button>
</li>
))}
</ul>
</div>
);
}
逐行拆解:一个请求是怎么"跑一圈"的
加一条待办的完整流程(这正是"客户端 ↔ 服务端"协作的缩影):
scss
用户在输入框打字
↓ onChange → setText (存进 state)
点击"添加"
↓ handleAdd
fetch POST /api/todos (发请求给后端)
↓
route.ts 的 POST 函数 (后端收数据)
↓ todos.push(newTodo) (存进"假数据库")
↓ 返回 newTodo
前端拿到响应
↓ setText("") 清空输入框
↓ fetchTodos() 重新拉列表
↓ setTodos(data) (更新 state)
↓ React 重新渲染
页面出现新的一条待办 ✅
几个关键的"为什么":
| 代码 | 为什么这样写 |
|---|---|
'use client' |
用了 useState/useEffect/onClick,这些只能在浏览器跑 |
useEffect(..., []) |
第二个参数是空数组 = 只在页面挂载时执行一次 |
text.trim() |
防止提交全是空格的内容 |
key={item.id} |
React 列表渲染要求每项唯一 key,否则会警告 |
completed ? "line-through" : "none" |
三元表达式:完成了加删除线,没完成不加 |
POST 后再 fetchTodos() |
让新加的那条立刻显示出来(否则要手动刷新) |
🍜 一句话记住 :
route.ts是后端出数据,page.tsx是前端画界面,中间靠fetch+ 共享的type打通------这就是 Next.js 全栈开发的完整闭环。
七、SEO 的三层做法
知道原理后,实操上 SEO 分三层:
第一层:你是谁?(元信息)
html
<title>我的 Next.js 教程</title>
<meta name="description" content="这是一个介绍 Next.js 的网站">
<meta name="keywords" content="Next.js, SEO, SSR">
告诉搜索引擎:你是谁(title)、做什么的(description)、有什么价值(keywords)。
在 Next.js 里,写在 layout.tsx 或 page.tsx 的 metadata 里:
tsx
export const metadata = {
title: "我的 Next.js Demo",
description: "一个 SEO 友好的全栈示例",
};
第二层:做内容
用户来的原因。再好的 title,没有真正有用的内容,也留不住人、排不上前。内容为王,这是 SEO 最根本的一层。
第三层:SSR 服务端渲染
这是技术层面最狠的一招。设想一个博客站 /post/:id,一篇文章一个页面、千万篇文章:
- 如果靠 CSR,爬虫抓到的全是空壳,千万篇等于零
- 如果靠 SSR,每篇文章都返回完整 HTML,整站内容都被 SEO 收录,给你加权
📌 一句话记住三层:第一层说"我是谁",第二层给"用户来的理由",第三层靠 SSR 让"内容被爬虫抓到"。
八、常见坑:{children} 丢了
写嵌套布局时,最容易犯的错就是忘了把 {children} 放进 JSX:
tsx
// ❌ 错误:children 拿到了,但没放进 JSX,子页面内容会消失
export default function DashboardLayout({ children }) {
return (
<div>
<nav>Nav</nav>
{/* 这里漏了 {children}! */}
</div>
);
}
结果是:访问 /dashboard,只显示 "Nav",页面内容"后台管理系统"不见了。
tsx
// ✅ 正确:必须把 {children} 放进去,子页面内容才会显示
export default function DashboardLayout({ children }) {
return (
<div>
<nav>Nav</nav>
{children}
</div>
);
}
🐛 原因 :
children是 Next.js 传给你的"子页面内容",你不在 JSX 里渲染它,它自然就丢了。layout 自己的内容(nav)会正常显示,丢的只是洞里的子页面。
九、面试高频 4 问
1. CSR 和 SSR 有什么区别?
根本区别是组件在哪里渲染:
- CSR(客户端渲染):服务器返回空壳 HTML + JS,浏览器执行 JS 才渲染内容。SEO 差。
- SSR(服务端渲染):服务器把组件编译成 HTML 再返回。SEO 好。
一句话:CSR 在浏览器渲染,SSR 在服务器渲染。
2. 为什么 SPA 对 SEO 不友好?
因为 SPA 的 HTML 只有一个空的 <div id="root"></div>,真正内容要靠 JS 执行才出现。而搜索引擎爬虫不执行 JS(或执行不稳定),所以抓到的是空壳,没内容可收录。
3. Next.js 里 Server Component 和 Client Component 怎么区分?
- Server Component(默认) :在服务器渲染,能查数据、直接出 HTML,负责 SEO。不能用
useState/useEffect/onClick。 - Client Component :文件顶部写
'use client',能用交互相关的 Hook,负责交互。
判断标准:凡是用了浏览器专属 API(事件、生命周期、DOM)的,就必须是 Client Component。
4. Client Component 是不是"只在浏览器渲染"?
不是。'use client' 组件会执行两次:第一次在服务器(SSR,生成静态 HTML),第二次在客户端(Hydration,挂载事件、激活交互) 。所以它的首屏 HTML 依然是完整的,SEO 友好。判断标准不是"在哪渲染",而是"用了哪些能力"------凡是用 useState/useEffect/onClick 的,文件顶部必须写 'use client'。
总结
核心概念速查表
| 概念 | 一句话 |
|---|---|
| SEO | 让搜索引擎"看懂"你 + "信任"你 |
| SPA | 一个页面 + JS 切换,体验好但没 SEO |
| CSR | 组件在浏览器渲染,爬虫抓不到 |
| SSR | 组件在服务器渲染,返回完整 HTML |
| 文件即路由 | 文件夹名 = 网址,page.tsx = 页面 |
| layout | 外壳,{children} 是留的洞 |
| Server Component | 能查数据出 HTML,负责 SEO |
'use client' |
标记交互组件:先 SSR 再客户端水合,不是"只在浏览器渲染" |
| Hydration 水合 | 浏览器拿到 HTML 后挂载 JS、绑定事件、激活交互 |
route.ts 接口 |
导出的 GET/POST 函数 = 对应 HTTP 方法的数据接口 |
| 全栈闭环 | 页面 fetch /api → route.ts 出数据 → setState 重新渲染 |
一句口诀
SPA 体验好但没 SEO,Next.js 靠"服务器渲染组件"把 SEO 补上------文件即路由、布局套娃;要交互就标
'use client',先 SSR 再水合;前端fetch+route.ts接口,就是一个完整的全栈闭环。
核心代码骨架
tsx
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="zh">
<body>
<nav>导航栏</nav>
{children} {/* 别忘了这一行 */}
</body>
</html>
);
}
// app/about/page.tsx ------ 服务器组件,直接出 HTML
export default function About() {
return <h1>About Us</h1>; // 服务器端渲染成 HTML,爬虫直接抓到
}
ts
// app/api/todos/route.ts ------ 后端接口
export async function GET() {
return Response.json(todos);
}
export async function POST(req: Request) {
const body = await req.json();
todos.push({ id: +Date.now(), content: body.content, completed: false });
return Response.json(todos.at(-1));
}
tsx
// app/todos/page.tsx ------ 前端页面,要交互就标 'use client'
'use client';
// ... useState / useEffect / fetch("/api/todos") / todos.map(...)
希望这篇文章对你有帮助!尤其是刚开始接触 Next.js、被 SEO 和 SSR 绕晕的同学,看完能有个清晰的整体图景。有问题欢迎在评论区交流~ 觉得有用的话,求个点赞收藏 🔥