Next.js App Router 实战入门:从 SPA 到 SSR 的全栈思维转变

摘要

从SPA的CSR困境出发,深入Next.js约定式路由、Server/Client组件边界、嵌套布局、route.ts全栈API及hydration水合机制,用Todo示例打通前后端一体化开发。


React 开发者从 SPA 转向 Next.js,最需要跨越的障碍不是语法,而是思维模型------从"一切都发生在浏览器里"切换到"代码在服务端和客户端各执行一次"。Next.js 将 React 从纯前端框架升级为全栈框架,既能写页面,也能写 API,背后是 Vercel 对 SSR 和 SEO 的深度优化。大量 AI 产品选择 Next.js 做官网,正是因为它在搜索引擎可见性上的天然优势。

SPA 的 SEO 困境与 SSR 的解法

传统 React SPA 的渲染流程是:浏览器请求 index.html → 解析 <div id="root"></div> → 加载 main.js → React 在客户端挂载组件树 → 页面可见。这个过程称为 CSR(Client Side Rendering)。

搜索引擎爬虫访问 SPA 页面时,看到的是一个空的 #root 节点和一堆 <script> 标签。虽然 Google 宣称能执行 JavaScript,但实际效果远不如直接抓取 HTML 内容。对于内容型网站(掘金、CSDN 等),流量高度依赖 SEO,CSR 是致命短板。

Next.js 的解法是 SSR(Server Side Rendering):请求到达服务端后,React 组件在 Node.js 环境中执行,JSX 被编译为完整的 HTML 字符串返回给浏览器。爬虫拿到的就是一个包含所有内容的 HTML 文档,和传统多页应用一样友好。

从 CSR 到 SSR 的转变,本质上是组件渲染位置的变化------之前是浏览器,现在是服务端

App Router:约定大于配置的文件路由

Next.js 13 引入的 App Router 用文件夹和文件替代了 React Router 的 <Routes> + <Route> 配置。一个路径对应一个文件夹,文件夹内的 page.tsx 自动成为该路径的页面组件:

bash 复制代码
app/
├── layout.tsx          # 根布局(全局导航、字体、metadata)
├── page.tsx            # / 首页
├── about/
│   └── page.tsx        # /about
├── dashboard/
│   ├── layout.tsx      # /dashboard 的嵌套布局
│   ├── page.tsx        # /dashboard
│   └── settings/
│       └── page.tsx    # /dashboard/settings
├── todos/
│   ├── page.tsx        # /todos
│   └── types.ts        # 类型定义
└── api/
    └── todos/
        └── route.ts    # /api/todos(API 接口)

不需要 react-router-dom,不需要手动配置路由表。文件结构本身就是路由表。layout.tsx 定义共享布局------根布局中的 <nav> 和字体配置对所有页面生效,dashboard/layout.tsx 中的导航只对 /dashboard/* 子路由生效。

根布局:SEO 元数据与全局框架

根布局不仅是 UI 的骨架,也是 SEO 的入口。metadata 导出定义了标题、描述等搜索引擎抓取的关键信息:

tsx 复制代码
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }: LayoutProps<"/">) {
  return (
    <html lang="en">
      <body>
        <nav>
          <Link href="/">首页</Link>
          <Link href="/about">关于</Link>
          <Link href="/dashboard">后台</Link>
        </nav>
        {children}
      </body>
    </html>
  );
}

metadata 对象在服务端渲染时被编译为 <title><meta> 标签,直接输出到 HTML 中。这就是 SEO 的第一层------告诉搜索引擎你是谁、做什么、提供什么价值。<Link> 组件来自 next/link,在 App Router 中替代了 React Router 的 <Link>,同样支持客户端导航,不会触发整页刷新。

Server Component:默认在服务端渲染

App Router 中所有组件默认都是服务端组件。它们不标记 'use client',在服务端执行,JSX 被编译为 HTML 后直接返回:

tsx 复制代码
// page.tsx ------ 服务端组件,默认行为
export default function Home() {
  return <h1>Hello World</h1>;
}

这个组件没有 useState、没有 useEffect、没有事件监听。它就是一个纯函数------接收数据,返回 JSX,在服务端编译为 <h1>Hello World</h1> 字符串。服务端组件不能使用浏览器 API(windowdocument),不能包含交互逻辑,但可以直接访问数据库、文件系统等 Node.js 能力。

Client Component:需要交互时声明 'use client'

当页面需要状态管理、事件监听或浏览器 API 时,在文件顶部添加 'use client' 指令:

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

  useEffect(() => {
    fetchTodos();
  }, []);

  const handleAdd = async () => {
    if (!text.trim()) return;
    await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content: text, completed: false }),
    });
    fetchTodos();
  };

  return (
    <div>
      <h1>待办事项</h1>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={handleAdd}>添加</button>
      <ul>
        {todos.map((item) => (
          <li key={item.id}>
            <span style={{ textDecoration: item.completed ? 'line-through' : 'none' }}>
              {item.content}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

'use client' 不意味着组件只在浏览器运行。Next.js 仍然会先在服务端预渲染这个组件的初始 HTML(静态内容部分),然后发送到客户端,客户端 JavaScript 接管后绑定事件、激活状态------这个过程称为 hydration(水合)。

把 hydration 比作速冻水饺:服务端把"馅"(数据)包进"皮"(HTML 结构),冻结后送到客户端;客户端用 JavaScript 的"火"把水饺煮熟,激活所有交互。组件实际上执行了两次------一次在服务端生成静态 HTML,一次在客户端"打补丁"绑定事件。

API Route:在同一个项目中写后端

Next.js 的全栈能力体现在 route.ts 文件上。app/api/todos/route.ts 自动映射为 /api/todos 接口,导出按 HTTP 方法命名的函数:

ts 复制代码
import { type Todo } from '../../todos/types';

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

export async function GET() {
  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() 处理 GET 请求,export async function POST() 处理 POST 请求。Response.json() 是 Next.js 封装的便捷方法,等价于 new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })

前端组件通过 fetch('/api/todos') 调用这个接口。注意这里用的是相对路径------因为 Next.js 的前后端运行在同一个服务上,不存在跨域问题。以前用 React + Vite 开发时,前端在 localhost:5173,后端在 localhost:3000,需要配置代理或 CORS;在 Next.js 中,前端和后端在同一个项目、同一个端口下协作,这就是全栈框架的核心优势。

嵌套布局:分区域复用 UI 结构

/dashboard 目录下有自己的 layout.tsx,为子路由页面提供独立的导航区域:

tsx 复制代码
import Link from "next/link";

export default function DashboardLayout({ children }: LayoutProps<"/dashboard">) {
  return (
    <div>
      <nav>
        <Link href="/dashboard/settings">Settings</Link>
      </nav>
      {children}
    </div>
  );
}

当访问 /dashboard 时,渲染顺序是:RootLayoutDashboardLayoutDashboardPage。当访问 /dashboard/settings 时,DashboardLayout 保持不变,只有 {children} 部分替换为 SettingsPage。这种嵌套布局机制让页面共享 UI 变得极其自然------不需要像 React Router 那样手动嵌套 <Outlet>

CSR vs SSR vs 全栈:渲染位置的全景对比

维度 CSR(SPA) SSR(Next.js) Next.js API Route
组件渲染位置 浏览器 服务端 不涉及
首屏 HTML 空的 #root 完整的 JSX→HTML JSON 响应
SEO 不直接面向用户
数据获取 useEffect + fetch 组件内直接 await req.json()
需要 use client 默认就是客户端 按需声明 不需要
文件约定 page.tsx route.ts

总结

Next.js App Router 用"约定大于配置"的设计,将 React 从纯前端框架扩展为全栈框架。服务端组件默认在服务端渲染 JSX→HTML 解决 SEO,客户端组件通过 'use client' 和 hydration 保留交互能力,route.ts 让前后端在同一项目中协作,嵌套 layout.tsx 让 UI 复用自然发生。

对于 React 开发者而言,最大的转变是从"所有代码都在浏览器跑"切换到"先想清楚这段代码应该在哪里跑"。服务端组件负责静态内容和数据获取,客户端组件负责交互和状态------这个边界的清晰划分,是 Next.js 全栈开发的核心思维。

相关推荐
濮水大叔14 小时前
CabloyJS 强大之处不仅仅是 IoC,而是全栈资源的寻址体系
typescript·node.js·全栈
东方小月15 小时前
从零开发一个 Coding Agent(十一):实现 CLI 的 print 模式
node.js·全栈
用户938515635072 天前
Next.js 笔记系统(二):Redis 数据服务与侧边栏组件拆分实战
javascript·全栈
用户938515635072 天前
从 0 拆解一个 Next.js 笔记系统:npx、App Router、RSC 与组件规划全记录
前端·后端·全栈
GitLqr2 天前
玩转 Flutter 中的 Stack 与 Positioned:解决 UI 重叠问题的实战指南
flutter·面试·全栈
To_OC2 天前
后端接口还没交付,前端如何独立把整套业务跑通
前端·react.js·全栈
用户852495071843 天前
Next.js 入门实战:从 SPA 到全栈,一次搞懂
全栈
烬羽3 天前
彻底搞懂 'use client':Next.js 组件根本不是"只在浏览器渲染"
全栈·next.js·前端工程化
用户938515635073 天前
深入理解 Next.js:从 SPA 的 SEO 问题到约定式路由与 SSR 原理
前端·后端·全栈