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。

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

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 一起返回;客户端组件通常在浏览器 useEffect 里 fetch 数据,爬虫看不到这些异步加载的内容。

打个比方 :

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

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

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%。

相关推荐
Highcharts.js11 小时前
React 中原生支持使用 JSX 组件配置自定义表格 Highcharts Grid
前端·javascript·react.js·前端框架·jsx·highcharts·自定义表格
JudithHuang13 小时前
React 嵌套路由与布局
前端·react.js·arcgis·react·react router
梯度下降者13 小时前
3天时间,我使用 Cursor 开发了一个提示词生成器
ai编程·next.js·cursor·产品开发·提示词生成器
前端小D16 小时前
React Native项目开发(iOS侧为例)
javascript·react native·react.js
锋行天下1 天前
Langgraph-cli 本地构建失败,改用服务器docker构建的流程
人工智能·前端框架·全栈
狂潮lister1 天前
🚀 《掘金・挖矿小精灵》完整产品与系统架构设计 & V1.3‑LD‑M 落地工程设计文档(**封档版**)
前端·架构·前端框架
王亚博_Abel2 天前
AI 编程时代怎么选前端框架?同一个应用让 Agent 用 12 种框架各写 5 遍,我整理出这套选型方法
前端·人工智能·前端框架
志尊宝2 天前
AI原生前端开发(042):AI辅助的路由设计与优化
javascript·react.js·ai-native
znsoft2 天前
MaClaw(码卡龙)技术拆解:一个 Go + React 的可自进化智能体平台
开发语言·react.js·golang
濮水大叔3 天前
React/Next.js 生态里,有没有 CabloyJS 这样的全栈框架?
react.js·全栈·next.js