从 SPA 到 Next.js 全栈:一个大前端的 SEO 突围笔记

配套两个示例工程:

  • spa-demo/:Vite + React 写的经典 SPA
  • next-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 的产品

三句话记住这套体系:

  1. 🧭 SEO 的根本是"组件在哪里渲染"------CSR 在浏览器,SSR 在服务端。
  2. 📁 Next.js 的核心 是"约定大于一切"------文件即路由,layout 管壳,page 管内容。
  3. 🥟 客户端组件是"速冻水饺"------服务端先渲染好再送来,客户端注水(hydration)激活交互。

Next.js 就是给 React 开发者的开箱即用利器:写一套代码,同时拿到好体验和好 SEO。

相关推荐
默_笙17 小时前
🚋 从流水线到地铁网:为什么复杂 AI 都要拆成多 Agent(上)——LangGraph 基础入门
前端·javascript
ID346107442017 小时前
【课程设计】基于Spring Boot+Vue的游戏账号租赁系统的设计与实现-计算机毕设 附源码50345
javascript·vue.js·spring boot·python·node.js·php·课程设计
柚yuzumi17 小时前
彻底搞懂 JavaScript 类型转换:显式转换、隐式转换与 ToPrimitive
前端·javascript·node.js
AI情绪识别开源19 小时前
检信ALLEMOTION 认知评估分析器 · WebSocket 推送数据文档
javascript
OpsEye19 小时前
公司内部全员使用 AI,如何保证会话合规、行为可审计?
javascript·ai编程
变与不变80619 小时前
JS事件机制精讲
前端·javascript
沉迷...20 小时前
Git skip-worktree 使用笔记(本地忽略配置文件)
javascript·git
志尊宝1 天前
Vue3 零基础每日笔记(032):Suspense 与异步组件——懒加载与顶层 await 终极方案
前端·javascript·vue.js·笔记·html5