深入理解 Next.js:从 SPA 的 SEO 问题到约定式路由与 SSR 原理
摘要:本文用一条完整的故事线讲清 Next.js:为什么主流 SPA 之外需要它(SEO 之痛)→ 它靠什么解决(SSR)→ 它怎么写页面和布局(约定式路由)→ 怎么处理强交互页面(客户端组件与水合)→ 怎么写后端接口(route.ts)→ 最后落到 SEO 的三层做法与 GEO。附带一个真实最小项目的逐行解析。
一、大前端手里的 Next.js
先理清三个名字像、但定位完全不同的框架:
| 框架 | 定位 | 技术栈 |
|---|---|---|
| Next.js | React 全栈框架 | React |
| Nuxt.js | Vue 全栈框架 | Vue |
| Nest.js | 纯后端框架 | Node.js |
「全栈」是理解 Next.js 的第一把钥匙:它既能写页面(前端),也能写 API(后端) 。也就是说,一个项目里你能同时搞定「用户看到的界面」和「提供数据的接口」,不用再单独起一个后端服务。
Next.js 背靠 Vercel,SEO 做得非常出色,所以现在大量 AI 产品官网(OPC 产品、AI Agent 站点)都用它来搭------因为它们的流量高度依赖搜索引擎。
二、SPA 的 SEO 之痛:从 #root 说起
要理解 Next.js 的价值,得先理解 SPA(单页应用)到底「痛」在哪。
SPA 的好处
SPA 体验很好:组件在前端挂载 ,用 useEffect 异步请求数据,不刷新页面 ;前端路由让页面切换又快又顺。
本质上,SPA 是在浏览器里复刻原生 App 的体验(Android/iOS 上切页面丝滑不刷新)。所以移动端「超级 App」里 80% 的页面都是 SPA (20% 原生)。对开发者来说,html 只需写一次,不用像原生那样 iOS、Android 各写一套。
SPA 的短板:没有 SEO
但 SPA 有个致命短板------SEO 非常差。原因很底层:
SPA 服务器返回的 index.html 长这样:
xml
<div id="root"></div>
<script src="main.js"></script>
#root 节点是空的 。真正的内容,是等浏览器下载并执行 main.js、App.jsx、Todos.jsx 后,由 React 在前端动态挂载 进去的。这就叫 CSR(Client Side Rendering,客户端渲染) ------组件在用户浏览器里渲染。
而搜索引擎的爬虫(百度、谷歌)基本不执行 JS 。它顺着 URL 爬过来,只看到空 #root 和一个 <script> 标签------没有内容可收录。
在 PC 时代,浏览器搜索引擎就是流量入口,「SEO 就是命」。掘金、CSDN 这些老牌内容站的流量几乎全部来自 SEO。如果内容靠 JS 渲染,爬虫抓不到,你就永远「搜不到」。
一句话串起主线:
less
#root (SPA,空壳) → 需要 SEO → 让 React 组件编译成 html (Next.js)
三、CSR vs SSR:SEO 的根本
核心结论呼之欲出------SEO 的根本,就是「组件到底在哪里渲染」。
CSR:在浏览器渲染
ini
浏览器请求 /todos
│
▼
服务器返回 index.html
┌──────────────────────────────┐
│ <div id="root"></div> │ ← 空的
│ <script src="main.js"> │
└──────────────────────────────┘
│ 浏览器下载并执行 JS
▼
React 把 <Todos/> 挂载进 #root
▼
用户看到页面
爬虫视角:空 #root → 没内容可收录 ❌
SPA 的前端路由长这样:#/todos、Routes 里 Route path="/todos" element={<Todos />},Todos 组件懒加载 ,最终挂载到前端 #root。
SSR:在服务端渲染
反过来,把「跑 React 组件」这一步放到服务器 上。React 组件本质是函数,只要不做事件监听、不做 useEffect ,这个函数 + 数据就能在 Node 环境跑一遍,得到一个字符串 ------服务器没有 DOM,「渲染」本质就是字符串的格式化。
css
浏览器请求 /todos
│
▼
服务器运行 React 组件函数
jsx + todos 数据 = html 字符串
▼
返回完整 HTML
┌──────────────────────────────┐
│ <ul> │
│ <li>买牛奶</li> │ ← 有真实内容
│ <li>写代码</li> │
│ </ul> │
└──────────────────────────────┘
▼
爬虫看到完整内容 → 可收录 ✅
这就是 SSR(Server Side Rendering,服务端渲染) 。
用一个对比彻底说清:
- 前后端分离 (传统 Java 全栈):
server:3000→/todos后端路由 → controller 处理 → service 查 mysql → 返回todos的 JSON 数组,浏览器自己渲染成界面。爬虫抓不到。 - 全栈项目 (Next.js):
/todos返回的就是 React 组件编译后的 html 。jsx + todos 数据 = 服务端 UI html。爬虫直接拿到成品。
| CSR | SSR | |
|---|---|---|
| 组件在哪里渲染 | Client(浏览器) | Server(服务端) |
| 代表 | SPA | Next.js |
| 爬虫看到的内容 | 空 #root ❌ |
完整 HTML ✅ |
Next.js 做的事,本质就一句:在服务器上把 React 组件编译成 html 字符串,再发给浏览器。
四、创建全栈项目
一条命令:
lua
npx create-next-app@latest
向导问你装什么,默认配置通常够用。它装的几样,正好是现代 React 全栈项目的标配:
- react / react-dom :
react负责「想」(组件逻辑),react-dom负责「画」(渲染到浏览器)。这是 React 界面的核心。 - typescript:给 JS 加类型,写代码时发现错误,而非运行时。
- tailwindcss:原子化 CSS 框架,直接在 JSX 上组合类名写样式。
- eslint:代码风格规范 + 质量检查(这个会单独开一篇深挖,见第二篇)。
提示:
create-next-app是交互式的,若终端提问被取消(报npm error canceled),可用参数一次性指定,如npx create-next-app@latest my-app --yes。
五、约定大于一切:App Router
Next.js 最核心的设计哲学就五个字------约定大于一切 。你不需要配置路由表,文件放哪,路由就是哪 。这套机制叫 App Router。
文件即路由
app/page.tsx→ 首页/app/about/page.tsx→/aboutapp/dashboard/page.tsx→/dashboardapp/dashboard/settings/page.tsx→/dashboard/settings
嵌套路由 = 建立嵌套文件夹。文件夹层级直接决定 URL 层级。
两个特殊文件
page.tsx:一个页面,对应一个 URL。layout.tsx:布局文件,放共用的部分 (如导航nav),子页面通过children注入。
渲染规则
访问 /about 时:
先渲染 layout.tsx(布局)
└─ 再渲染 page.tsx(页面)
/about/page.tsx 里的组件先被编译成 html (tsx -> html),再套进 layout 返回。
六、逐行拆解:布局与页面
真实项目结构:
bash
app/
├── layout.tsx ← 根布局(最外层)
├── page.tsx ← 首页 /
├── about/
│ └── page.tsx ← /about
└── dashboard/
├── page.tsx ← /dashboard
└── settings/
├── layout.tsx ← settings 专属布局
└── page.tsx ← /dashboard/settings
1. 根布局 app/layout.tsx
javascript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Link from "next/link";
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
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>
</ul>
</nav>
{children}
</body>
</html>
);
}
逐行理解:
next/font/google的Geist/Geist_Mono:Next.js 内置字体优化,构建时下载并自托管字体 ,通过variable生成 CSS 变量(--font-geist-sans等),避免字体加载时布局抖动;subsets:["latin"]只加载拉丁字符子集。export const metadata:Metadata API 。你不需要手动写<title>、<meta>,导出这个对象,Next.js 自动生成到<head>,还处理了去重、流式渲染。这直接对应后面 SEO 的第一层。<html>、<body>:根布局必须包含这两个标签,是硬性约定。LayoutProps<"/">:新版本 Next.js 提供的全局类型助手 (无需 import),"/"是当前布局对应的路由路径,能根据目录结构自动推导params和命名插槽的强类型。<Link>:来自next/link,客户端路由跳转------点击不刷新页面,只切换内容(保留了 SPA 的体验优势)。{children}:布局的「洞」,子页面填到这里。
2. 首页 app/page.tsx
javascript
// 服务器端组件
// 在服务器端 react node 的方式运行
// jsx->html
export default function Home() {
return <h1>Hello World</h1>;
}
这三行注释点出了本质:这是一个「服务器组件」 ,默认在服务器上、以 React + Node 方式运行,JSX 被编译成 html 字符串 直接发给浏览器。没有 useEffect、没有事件监听,所以能安全地在服务器执行。
3. /about 页面 app/about/page.tsx
javascript
function About(){
return (<h1>About us</h1>)
}
export default About
文件名 about/page.tsx 决定路由 /about------没有一行路由配置,URL 天然存在,这就是「约定大于一切」。
4. /dashboard 页面 app/dashboard/page.tsx
javascript
export default function Page(){
return(<h1>Hello, Dashboard! 后台管理系统</h1>)
}
dashboard/page.tsx → 路由 /dashboard。它没有自己的 layout,会直接套进根布局。
5. 嵌套布局 app/dashboard/settings/layout.tsx
javascript
import Link from "next/link";
export default function DashboardLayout({children}){
return(
<div>
<nav>Nav
<Link href="/dashboard/settings">Settings</Link>
</nav>
{children}
</div>
);
}
这是一个嵌套布局 (套在根布局内部),只作用于 /dashboard/settings。<nav> 是 settings 页面「共用」的部分,{children} 填入 settings 的 page。
6. /dashboard/settings 页面
javascript
export default function Page(){
return(<h1>Hello, Settings!</h1>)
}
嵌套布局的渲染顺序
访问 /dashboard/settings,布局从外到内层层包裹:
css
RootLayout ← 最外层(<html><body> + 顶部导航)
└─ SettingsLayout ← 中间层(Settings 导航)
└─ Page ← 最内层(<h1>Hello, Settings!</h1>)
渲染成 html 后:
css
<html>
<body>
<nav>首页 | 关于 | 后台</nav> ← 根布局
<div>
<nav>Nav Settings</nav> ← settings 布局
<h1>Hello, Settings!</h1> ← page
</div>
</body>
</html>
布局负责「壳」,页面负责「内容」,通过 children 一层层嵌套------这是 Next.js 最核心的布局心智模型。
七、客户端组件与水合:'use client' 的真相
前面说的都是「服务器组件」------默认、能 SSR、SEO 好。但现实里有些页面是强交互的 (要 useState、useEffect、事件监听),这些 API 在服务器上没有 DOM、没法用。
服务器组件的硬限制
| 能用的 | 不能用的(会报错) |
|---|---|
渲染静态内容、直接 await 读数据 |
useState(状态) |
metadata |
useEffect(副作用) |
| --- | onClick 等事件监听 |
所以需要一个开关: 'use client' 。它写在文件最顶部 ,是一道边界标记,告诉 Next.js「从这个文件开始(含它 import 的所有模块),代码在浏览器里跑」。
一个常见误区(很重要)
'use client' ≠ 只在浏览器渲染。 真相是------先在服务器端把能渲染的渲染完,再到客户端去渲染。readme 里的「水饺」比喻绝了:
css
第一步(服务器):包好水饺,冻上,送过来
服务器把组件渲染成静态 HTML(包好的饺子),发到浏览器。
页面"有壳有肉",但还没熟。
第二步(浏览器):水煮 = 水合
浏览器拿到静态 HTML(冻饺子),下锅水煮------
加载客户端 JS、绑定点击事件、激活交互。煮熟才能吃。
这个「水煮」的学名叫 hydration(水合) ,readme 的精确定义:
水合(hydration):浏览器拿到静态 HTML 之后,挂载客户端 JS、绑定点击事件、激活交互。
底层结论:客户端组件执行两次
CSR 组件会执行两次:一次在服务器,第二次在客户端,打补丁。
| 执行位置 | 干什么 | |
|---|---|---|
| 第一次 | 服务器 | 渲染出静态 HTML(首屏有内容、SEO 不空白) |
| 第二次 | 浏览器 | 挂载事件、注入状态、激活交互(「打补丁」) |
来看这个客户端组件 app/todos/page.tsx:
typescript
// react 可以在后端运行
'use client';
// 组件在前端渲染的标记
// 添加事件, useEffect, 调用后端接口 csr next.js 也支持
import { useEffect, useState } from 'react';
import { type Todo } from './types';
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);
}
useEffect(() => {
fetchTodos();
}, []);
return (
<div style={{marginTop:"12px"}}>
<h1>待办事项</h1>
<ul style={{paddingLeft:"0",listStyle:"none"}}>
{todos.map((item)=>(
<li key={item.id} style={{...}}>
<span style={{textDecoration:item.completed?"line-through":"none", cursor:"pointer"}}>
{item.content}
</span>
<button>删除</button>
</li>
))}
</ul>
</div>
);
}
逐行理解:
- 第 1 行注释
// react 可以在后端运行:这是 Next.js 的默认状态------服务器组件。 - 第 2 行
'use client':手动切换成客户端组件(对应注释「组件在前端渲染的标记」)。 - 第 4 行注释:既然在前端跑,就能用
useState、useEffect、调后端接口------这就是 CSR 模式,Next.js 同样支持。 useState<Todo[]>([]):给组件加「待办列表」状态,初始为空数组[],类型是Todo[]。fetchTodos:await fetch("/api/todos")调后端接口 →await res.json()解析 →setTodos(data)写入状态。注意res.json()是异步的(响应体是二进制流,要读进来再解析)。useEffect(()=>{ fetchTodos(); }, []):组件挂载后执行一次,去拉数据。依赖数组[]表示「只在首次挂载时执行」。todos.map(...):把每条 todo 渲染成<li>,key={item.id}帮助 React 高效 diff;completed为真时加line-through删除线。
关键细节 :这个页面里 <h1>待办事项</h1> 是静态的,第一次(服务器)就渲染出来了 ,你打开页面立刻看到;而列表内容靠 useEffect 请求,属于**第二次(浏览器)**才补上的。这就是「先骨架、后填充」。
八、写后端接口:route.ts
「全栈」的另一半------写 API 。还记得那句话:Next.js 除了 'use client' 标注的,其余默认都是后端 。落地方式是一个特殊文件 route.ts。
约定式路由,同样管 API
bash
app/api/todos/route.ts → 接口地址 /api/todos
page.tsx 是「页面」,route.ts 是「数据接口」。规则一样:文件放哪,URL 就是哪 ,放 api/ 目录下接口就自动存在。
导出 HTTP 方法名对应的函数
javascript
// next.js 除了 use client 都是后端
// /api 数据接口 任然满足 app router 约定
// route.ts 返回json 数据接口的
import { type Todo } from '../../todos/types';
let todos: Todo[] = [
{ id: 1, content: '学习AppRouter', completed: true},
{ id: 2, content: 'next.js 个人官网开发', completed: false},
]
// /api/todos get 请求 restful
export async function GET() {
// 返回json 数据接口 next.js 封装好了Response
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():函数名 = HTTP 方法 。导出GET处理 GET 请求,导出POST处理 POST,同理PUT/DELETE/PATCH。这就是注释说的 RESTful------一个 URL 用不同方法表达「查/增/改/删」。Response.json(todos):Response是 Web 标准(Fetch API 那个),Next.js 已封装好,自动返回application/json,不用手动设Content-Type。await req.json():req是标准Request对象,req.json()异步解析请求体 JSON,所以await。id: +Date.now():用时间戳当 id,+号显式当数字处理。真实项目 id 由数据库自增。let todos是内存数组 ,不是数据库------服务一重启,POST 进去的数据就没了。这个 demo 把「数据存哪」简化了,真实项目要换数据库。
对比传统 Java 后端的简化
readme 里 Java 全栈:server:3000 → /todos → controller → service → mysql。这一整条链路,在 Next.js 里被压缩成一个 route.ts 文件的两个函数 。全栈框架的爽点就在这:接口逻辑和页面代码住在同一项目,甚至共用同一份类型。
九、前后端共享类型:types.ts
上面 route.ts 和前端 page.tsx 都 import 了 Todo:
ini
export type Todo = {
id: number;
content: string;
completed: boolean;
}
这是 TypeScript 在全栈项目里最大的价值------前端和后端用同一份类型,数据契约不会「对不上」。
csharp
todos/types.ts (唯一的类型源)
▲ ▲
│ │
前端 page.tsx 后端 route.ts
useState<Todo[]>() let todos: Todo[]
如果前后端各维护一份 Todo,某天后端把 completed 改叫 done,前端不知道,运行时才崩。共享同一份 types.ts,任何一边类型不一致,编译阶段 就被 TypeScript 揪出来。这呼应了「创建项目」那节的 typescript 默认选项------用类型把全栈项目串成一个整体。
十、全栈数据流:串起来
把前后端拼起来看「全栈」怎么运转:
scss
浏览器(前端) 服务器(后端)
todos/page.tsx 'use client' api/todos/route.ts 无 'use client'
│ │
│ useEffect → fetch('/api/todos') │
│ ────────────────────────────────────► │
│ │ GET() 被触发
│ │ return Response.json(todos)
│ ◄──────────────────────────────────── │
│ res.json() → Todo[] │
│ setTodos(data) │
▼ │
todos.map(item => <li>...) 渲染列表
前端(客户端组件)用 useEffect 调 /api/todos;后端(服务器组件)GET() 返回 JSON;数据流到前端被 setTodos 写进 state,再 map 成列表。前端用 'use client' 是因为它用了 useEffect/useState;后端 route.ts 不需要标注,因为它天生跑在服务器。
十一、SEO 的三层做法
回到主线------Next.js 为 SEO 而生,那 SEO 怎么做?三层递进:
第一层:告诉搜索引擎「你是谁」
在 <head> 写清三件事------title(你是谁)、description(做什么)、keywords(提供什么价值) :
ini
<title></title>
<meta name="description" content="这是一个描述">
<meta name="keywords" content="这是一个关键词">
Next.js 里不用手写标签,导出 metadata 对象即可(还记得根布局的 export const metadata 吗),自动生成这些 <head> 元素。
第二层:做内容
用户为什么来?因为内容 。title、description 只是「门面」,真正留住用户、让搜索引擎持续给权重的,是扎实的内容本身。
第三层:SSR 服务端渲染
最关键的一层。内容站可能有千万篇文章,如 /post/:id 动态路由。若内容 CSR 渲染,爬虫一篇都抓不到;用 SSR ,每篇 /post/:id 返回的都是编译好的完整 html,整站内容都能被收录,这些收录给站点持续加权。
三层合起来:用 metadata 报家门 → 用内容留住用户 → 用 SSR 让内容被收录。
十二、GEO:下一代搜索引擎优化
readme 埋了个新概念------GEO(Generative Engine Optimization,生成式引擎优化) 。
背景:用户入口正从传统搜索引擎,转向豆包、ChatGPT 这类 AI 生成式引擎。用户不再翻链接,而是直接向 AI 提问。
GEO 核心思路:当 AI 生成答案时,带上我们的内容(甚至购买链接) 。优化目标从「让搜索引擎收录你」变成「让 AI 在回答里提到你、引用你」。
底层逻辑和 SEO 一脉相承:你的内容必须可被机器读取、可被引用------这又回到 SSR。只有服务端渲染、结构良好的内容,才更容易被 AI 抓取引用。
总结
串成一条完整的故事线:
- SPA 的痛 :内容靠 JS 在浏览器渲染,爬虫看到空
#root,SEO 差。 - Next.js 的药方:把 React 组件搬到服务器编译成 html(SSR),爬虫能直接抓到完整内容。
- 约定大于一切 :文件即路由,
page.tsx是页面,layout.tsx是共用布局壳,嵌套文件夹 = 嵌套路由。 - 强交互页面 :
'use client'切客户端组件,但仍是「服务器渲染 + 浏览器水合」两段式,组件执行两次。 - 写后端 :
route.ts用「函数名 = HTTP 方法」的约定处理接口,types.ts统一前后端类型契约。 - SEO 三层:metadata 报家门 → 内容留用户 → SSR 让内容被收录。
- 未来:GEO 把优化目标转向 AI 生成引擎,底层仍依赖结构化、可被机器读取的内容。
Next.js 不再神秘------它就是一个「让 React 组件能在服务器上渲染以解决 SEO,同时还能写接口、统一类型」的开箱即用全栈利器。