Next.js 全栈实战:从 SPA 到 SSR,SEO 友好到底怎么落地?

前端同学写官网、内容站、AI 产品落地页,最怕什么?

------页面做得再漂亮,百度 Google 搜不到,流量为 0。

SPA 体验确实好,但爬虫看到的是空 #root;Next.js 背靠 Vercel,一个框架同时搞定页面和 API,把 React 组件的渲染从浏览器搬到了服务端。这篇文章带你从零搞懂 Next.js 全栈与 SEO 的底层逻辑,并用一个待办事项 Demo 跑通 App Router、Server Component、API Routes。

读完你就能上手:文件即路由、服务端组件、客户端组件水合、API 接口,一套全栈流程直接落地。


一、SPA 的体验与 SEO 的代价

先聊聊我们熟悉的 SPA(Single Page Application,单页应用)。

SPA 的好处很明显:

  • 交互流畅,组件在前端挂载,用 useEffect 异步请求数据,不需要刷新页面;
  • 前端路由切换快,体验接近原生 App;
  • 一套 HTML + JS 跑在 WebView 里,就能覆盖 iOS / Android 两端。

你会发现,现在很多移动端 App 里 80% 的页面都是 SPA 做的。为什么?因为 App 不需要 SEO,用户是从应用商店下载的,不是从百度搜进来的。

但换成官网、博客、内容站、AI 产品介绍页,情况就完全不同了------这些页面要靠搜索引擎吃饭

SPA 的短板非常致命:

爬虫通过 URL 抓取页面时,拿到的是这样的 HTML:

xml 复制代码
<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script src="/main.js"></script>
  </body>
</html>

#root 是空的,真正的内容要等 JS 下载、执行、异步请求数据后才渲染出来。搜索引擎爬虫可没耐心等你异步加载完,所以你的页面在搜索结果里就是一片空白。

PC 时代 SEO 是命,移动端时代 App 是超级入口,但内容型产品依然靠搜索引擎吃饭。

所以我们需要一种方案:既能保留 React 的开发体验,又能在服务端把 HTML 渲染好再返回给爬虫。

这就是 Next.js 的核心价值。


二、CSR 与 SSR 的本质:组件到底在哪里渲染?

先厘清两个概念:

  • CSR(Client Side Rendering) :组件在浏览器里挂载、渲染。用户看到页面前,浏览器要下载 JS、执行 React、请求数据,然后才渲染到 #root
  • SSR(Server Side Rendering) :组件在服务端用 Node 跑一遍,把 JSX + 数据编译成 HTML 字符串,直接返回完整页面。爬虫拿到就能解析。

前后端分离的项目里,/todos 这个 URL 返回的是 JSON 数据:

json 复制代码
[{ "id": 1, "content": "学习 App Router", "completed": true }]

爬虫不认 JSON,它要的是 HTML。

而 Next.js 的全栈项目里,/todos 返回的是 React 组件编译后的 HTML:

css 复制代码
<ul>
  <li>学习 App Router</li>
  <li>Next.js 个人官网开发</li>
</ul>

SSR(Server-Side Rendering,服务端渲染)

渲染发生在服务器上。

  • 服务器收到请求后,执行组件代码,把 JSX + 数据编译成完整的 HTML 字符串,返回给浏览器。
  • 浏览器直接显示 HTML,无需等待 JS 下载执行。
  • 优点:首屏快,SEO 友好(爬虫直接拿到内容)。
  • 缺点:服务器压力大,交互需要额外水合。

比喻:餐厅把煮好的水饺端给你,你直接吃。

典型场景:官网、博客、内容站、需要被搜索引擎收录的页面。

CSR(Client-Side Rendering,客户端渲染)

渲染发生在浏览器里。

  • 服务器只返回一个空壳 HTML(如 <div id="root"></div>)和 JavaScript 文件。
  • 浏览器下载 JS,执行 React,在客户端挂载组件、请求数据、更新 DOM。
  • 优点:交互流畅,切换页面不刷新,体验接近原生 App。
  • 缺点:首屏慢(等 JS),SEO 差(爬虫看到空壳)。

比喻:餐厅给你面团和馅,你自己包、自己煮。

典型场景:后台管理系统、移动端 App 内嵌页面、不需要 SEO 的强交互应用。

说白了,组件到底在哪里渲染,决定了 SEO 的根本。

在服务端渲染,SEO 就好;在客户端渲染,SEO 就差。


三、Next.js 项目结构与约定式路由

3.1 创建项目

lua 复制代码
npx create-next-app@latest

选择默认配置即可,你会得到 TypeScript、Tailwind CSS、ESLint 一套全家桶。

3.2 文件即路由

Next.js 的 App Router 有一个核心思想:约定大于配置

不需要像 React Router 那样写一堆 <Route>,你只需要在 app/ 目录下建文件夹和文件,路由就自动生成了。

  • app/page.tsx → 首页 /
  • app/about/page.tsx/about
  • app/dashboard/page.tsx/dashboard
  • app/dashboard/settings/page.tsx/dashboard/settings

嵌套路由通过文件夹实现,布局文件 layout.tsx 负责公共部分。

我们看一个全局布局:

javascript 复制代码
// app/layout.tsx
import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";

export const metadata: Metadata = {
  title: "我的全栈应用",
  description: "一个 Next.js 全栈项目,SEO 友好",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <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>
  );
}

这里的 metadata 就是 SEO 的第一层:告诉搜索引擎你是谁、做什么、提供什么价值。

然后是首页和关于页:

javascript 复制代码
// app/page.tsx
export default function Home() {
  return <h1>Hello, World!</h1>;
}
javascript 复制代码
// app/about/page.tsx
function About() {
  return <h1>About Us</h1>;
}

export default About;

注意:这两个组件都没有 'use client' 声明,它们就是服务端组件。React 可以在 Node 环境里运行,把 JSX 编译成 HTML 字符串返回给浏览器。

3.3 嵌套布局

后台管理系统通常有自己的侧边栏或导航,可以建一个子布局:

javascript 复制代码
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <nav>后台导航栏</nav>
      {children}
    </div>
  );
}

这样 /dashboard/dashboard/settings 都会先经过全局布局,再经过后台布局,最后渲染页面内容。

文件系统就是路由器,文件夹层级就是路由层级。少写配置,多写业务。


四、服务端组件与客户端组件:水饺与水合

Next.js 把 React Server Component(RSC)带到了生产环境,这是它区别于传统 CSR 的关键。

4.1 默认都是服务端组件

在 App Router 下,没有 'use client' 的组件都是服务端组件。它们只在服务端运行,可以访问数据库、文件系统等后端资源,最终输出 HTML。

服务端组件不能使用 useStateuseEffect、事件监听等浏览器特性,因为这些是给客户端准备的。

4.2 什么时候由服务端渲染,什么时候由浏览器渲染?

很多刚接触 Next.js 的同学会困惑: "我这个组件到底是在服务器上跑,还是在浏览器里跑?"

其实规则很简单,记住下面这张表就够了:

组件类型 渲染位置 能否使用 useState / useEffect / 事件 典型场景
服务端组件 (没有 'use client' 只在服务端渲染,浏览器只负责显示静态 HTML ❌ 不能 展示内容、SEO 关键部分、直接查数据库、读取文件
客户端组件 (文件顶部有 'use client' 先在服务端预渲染成 HTML,再在浏览器水合(hydrate)后接管交互 ✅ 可以 表单输入、点击事件、轮播图、需要状态更新的交互模块
API 路由app/api/**/route.ts 只在服务端执行,返回 JSON 数据,不涉及 UI 渲染 ❌ 不涉及 数据接口、第三方服务对接
布局 / 页面layout.tsx / page.tsx 取决于内部组件:若无 'use client' 则整体服务端渲染;若有则混合渲染 视情况 页面骨架、路由容器

几个关键结论:

  • 服务端组件是"一次性"的:它们在服务端执行完毕,生成 HTML 字符串发给浏览器,之后就不再参与任何交互。就像印刷好的报纸,内容固定,不能点击。
  • 客户端组件 是"两步走":先在服务端把能渲染的部分渲染成 HTML(保证 SEO 和首屏速度),然后在浏览器里执行 JS、绑定事件、处理状态更新。所以你会发现,'use client' 组件里的代码会执行两次:一次在服务端生成静态外壳,一次在客户端水合激活。
  • 动态数据获取的位置 也有讲究:服务端组件可以在服务端直接 fetch 数据,数据随 HTML 一起返回;客户端组件通常在浏览器 useEffectfetch 数据,爬虫看不到这些异步加载的内容。

打个比方

服务端先把水饺包好、冻上,送到你家;你自己烧水煮一下,就能吃了。

这个"煮"的过程,就是水合。

4.3 什么时候需要客户端组件?

当页面需要强交互时,比如表单输入、点击事件、异步请求后更新 UI,你需要在文件顶部加上:

arduino 复制代码
'use client';

一个常见的误区是: "客户端组件只在浏览器渲染"

实际上,客户端组件也会在服务端先渲染成静态 HTML,浏览器拿到后执行水合(hydration)------挂载客户端 JS、绑定事件、激活交互。

我们对比一下:

javascript 复制代码
// app/about/page.tsx ------ 服务端组件
function About() {
  return <h1>About Us</h1>;
}
scss 复制代码
// app/todos/page.tsx ------ 客户端组件
'use client';

import { useState, useEffect } from 'react';

export default function TodosPage() {
  const [todos, setTodos] = useState([]);
  const [text, setText] = useState('');

  useEffect(() => {
    fetch('/api/todos')
      .then(res => res.json())
      .then(setTodos);
  }, []);

  // ...
}

服务端组件负责"能被看见",客户端组件负责"能被操作"。

SEO 的关键内容,尽量放在服务端组件里;交互逻辑,用客户端组件增强。

如果不加"use client"


五、API 路由:全栈的另一半

Next.js 不仅能返回 HTML,还能返回 JSON。

app/api/ 目录下创建 route.ts,导出一个或多个 HTTP 方法函数即可。

我们实现一个待办事项的 API:

ini 复制代码
// app/todos/types.ts
export type Todo = {
  id: number;
  content: string;
  completed: boolean;
};
javascript 复制代码
// app/api/todos/route.ts
import { type Todo } from '../../todos/types';

let todos: Todo[] = [
  { id: 1, content: '学习 App Router', completed: true },
  { id: 2, content: 'Next.js 个人官网开发', completed: false },
];

// GET /api/todos
export async function GET() {
  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);
}

文件约定同样适用:route.ts 就是接口地址。

同一个框架,既能返回 HTML,也能返回 JSON。 这就是全栈。


六、实战:待办事项全栈 Demo

把上面的碎片拼起来,我们做一个完整的待办事项页面:

  • 服务端渲染页面骨架,SEO 能看到标题;
  • 客户端组件负责交互:拉取列表、添加事项;
  • API 路由提供数据接口。

目录结构:

markdown 复制代码
app/
├── layout.tsx
├── page.tsx
├── about/
│   └── page.tsx
├── dashboard/
│   ├── layout.tsx
│   ├── page.tsx
│   └── settings/
│       └── page.tsx
├── todos/
│   ├── types.ts
│   └── page.tsx
└── api/
    └── todos/
        └── route.ts

app/todos/page.tsx 完整代码:

javascript 复制代码
'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);
  };

  const handleAdd = async () => {
    if (!text.trim()) return;

    await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content: text }),
    });

    // 添加成功后清空输入框,并重新拉取列表
    setText('');
    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: '12px' }}>
        添加
      </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>
  );
}

运行 npm run dev,访问 /todos

页面先由服务端渲染出标题和输入框,浏览器水合后,useEffect 请求 /api/todos 拉取数据并渲染列表。点"添加"会 POST 到 API,然后重新拉取列表。

这个渲染流程可以拆解成三步:

  1. 服务端渲染阶段TodosPage 组件在服务器上执行,生成包含标题、输入框、按钮的静态 HTML(此时 todos 还是空数组,列表区域为空)。
  2. 浏览器水合阶段 :客户端 JS 加载完成,React 接管页面,绑定输入框的 onChange、按钮的 onClick 等事件。
  3. 客户端数据请求阶段useEffect 在浏览器里发起 fetch('/api/todos'),拿到数据后调用 setTodos 更新状态,React 在浏览器里重新渲染列表。

你会发现,列表数据是浏览器请求后渲染的,爬虫抓取时看不到待办事项的具体内容 。如果希望列表内容也能被搜索引擎收录,就需要在服务端组件里直接获取数据(比如用 async 服务端组件 + fetch),而不是依赖客户端 useEffect

你可以把这段代码直接复制进项目跑起来,这就是一个最小可用的 Next.js 全栈闭环。


七、SEO 三板斧:Next.js 怎么帮你被收录?

做 SEO 其实不复杂,核心就三件事:

第一层:metadata

layout.tsx 或每个页面里导出 metadata

arduino 复制代码
export const metadata: Metadata = {
  title: '我的全栈应用',
  description: '一个 Next.js 全栈项目,SEO 友好',
  keywords: 'Next.js, SEO, 全栈',
};

这对应 HTML 里的:

ini 复制代码
<title>我的全栈应用</title>
<meta name="description" content="一个 Next.js 全栈项目,SEO 友好" />
<meta name="keywords" content="Next.js, SEO, 全栈" />

第二层:内容

搜索引擎喜欢高质量、原创、结构清晰的内容。你的页面要回答用户的问题,提供价值。

第三层:SSR

服务端渲染保证爬虫能拿到完整 HTML。Next.js 默认就是 SSR,所以你的内容站天然具备 SEO 基础。

对于 /post/:id 这种动态路由,Next.js 可以在服务端渲染千万篇文章,整站被收录的内容会给你的域名加权。

SEO 不是玄学,是把该给爬虫看的内容提前端上桌。

现在很多 AI 产品的官网用 Next.js 做,就是这个原因------搜索流量依然是性价比最高的获客渠道。

另外,生成式引擎优化(GEO)也在兴起,用户可能直接在豆包、Kimi 里问问题,如果你的内容出现在生成结果里,就能带来精准流量。SSR 友好的内容同样有利于被 AI 抓取和理解。


总结

Next.js 是一个 React 全栈框架,它解决了 SPA 在 SEO 上的短板:

  • 文件即路由 :约定大于配置,page.tsx 就是页面,layout.tsx 就是布局。
  • 服务端组件:默认 SSR,组件在 Node 里渲染成 HTML,爬虫可读。
  • 客户端组件'use client' 声明交互组件,服务端预渲染 + 客户端水合。
  • API 路由route.ts 返回 JSON,一个项目搞定前后端。

记住这张图:

less 复制代码
#root (SPA)  →  爬虫看到空壳
JSX + 数据   →  HTML (SSR)  →  爬虫看到内容

下次再有人问你"Next.js 和 React 有什么区别?",你可以告诉他:

React 是 UI 库,Next.js 是让 React 在全栈和 SEO 上开箱即用的框架。

动手试试吧,跑一遍这个待办事项 Demo,你就掌握了 Next.js 最核心的 80%。

相关推荐
小林ixn1 小时前
深入理解 React 水合(Hydration):从静态 HTML 到可交互页面
react.js·next.js
烬羽1 小时前
彻底搞懂 App Router 约定式路由:文件放对位置,路由就自动生成了
react.js·next.js·前端工程化
满栀5852 小时前
vue3动态路由详细效果
前端·javascript·vue.js·typescript·前端框架
meilindehuzi_a2 小时前
从 Vite 到 Axios 与 Mock:React Todos 全栈项目架构及请求链路详解
前端·react.js·架构
@卓越俊逸_角立杰出@2 小时前
java实现Agent+ReAct demo(Spring Boot + Spring AI Alibaba ReAct Agent)
java·spring·react.js
用户69190268133917 小时前
用浏览器 WebGPU跑DPSK大模型(1) - 模型的下载和前端下载进度的显示
javascript·react.js·架构
光影少年18 小时前
react navite本地存储:AsyncStorage、MMKV、文件存储
前端·react native·react.js
JieE21221 小时前
前端不等人:用 Mock 接口工程让 React 应用独立起飞
前端·react.js·面试
东方小月21 小时前
从零开发一个 Coding Agent(九):实现 Agent 的工具调用闭环
人工智能·前端框架·node.js