Next.js 入门实战:从 SPA 到全栈,一次搞懂
React 写 SPA 很高效,但当页面需要搜索收录、首屏内容、服务端数据和轻量接口时,单独组合路由、数据请求和服务端能力会越来越重。
Next.js 不是"替代 React 的新框架",而是给 React 加上了路由、服务端渲染、数据获取和后端入口的一套完整约定。你仍然写 React 组件,只是有了更多运行位置可以选择。
本文以 App Router 为主线,讲清楚文件路由、Layout、Server/Client Component、Route Handler 和一个 Todo 数据流。
一、SPA 的问题不只是 SEO
传统 SPA 常见的首屏流程是:浏览器先拿到基础 HTML,再下载 JavaScript,最后在浏览器请求数据并渲染页面。
text
HTML -> 下载 JS -> 执行 JS -> 请求数据 -> 渲染内容
这会带来三个挑战:
- 首屏内容依赖 JavaScript、网络和客户端渲染
- 搜索引擎与社交分享抓取对 JavaScript 的执行能力不完全一致
- 页面和接口通常需要分别部署、分别维护
但"SPA 的 HTML 一定只有空 div,SEO 一定很差"并不准确。现代搜索引擎可以执行部分 JavaScript,SPA 也能做好 SEO;只是对于内容型页面,服务端先提供完整内容通常更稳定、首屏更快。
Next.js 的核心价值是提供多种渲染和数据获取方式,而不是让所有页面都强制 SSR。
二、Next.js 是什么?
Next.js 是 React 的全栈框架。它提供文件路由、服务端组件、渲染策略、Route Handler 和生产构建能力。
| 能力 | 在项目中解决什么 |
|---|---|
| App Router | 用文件和文件夹定义页面路由 |
| Server Component | 在服务端读取数据并生成 UI 结果 |
| Client Component | 承载状态、事件和浏览器 API |
| Layout | 多级页面共享导航、侧栏和结构 |
| Route Handler | 在同一仓库提供 HTTP 接口 |
| Metadata | 声明标题、描述等页面元数据 |
创建项目:
bash
npx create-next-app@latest my-next-app
cd my-next-app
npm run dev
创建器会提供 TypeScript、ESLint、App Router 等选项。Tailwind CSS 是常见选择,不是 Next.js 运行的必需条件。
三、App Router:文件即路由
App Router 的约定很直接:文件夹定义 URL 片段,page.tsx 定义页面,layout.tsx 定义共享外壳。
text
app/
├── layout.tsx # 根布局
├── page.tsx # /
├── about/
│ └── page.tsx # /about
├── dashboard/
│ ├── layout.tsx # /dashboard 下的共享布局
│ ├── page.tsx # /dashboard
│ └── settings/page.tsx # /dashboard/settings
├── todos/page.tsx # /todos
└── api/todos/route.ts # /api/todos
这不需要手写 <Routes>。当用户访问 /dashboard/settings,Next.js 会匹配目录中的 page.tsx,并自动嵌套沿途的 Layout。

四、Layout:共享结构,不是每页复制导航
根布局是必须存在的页面骨架,通常放全局样式、字体和主导航:
tsx
// app/layout.tsx
import Link from "next/link";
import type { ReactNode } from "react";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="zh-CN">
<body>
<nav><Link href="/">首页</Link> <Link href="/todos">Todos</Link></nav>
{children}
</body>
</html>
);
}
children 是当前匹配页面的插槽。访问 /dashboard/settings 时,渲染层级可以理解为:
text
RootLayout
└── DashboardLayout
└── SettingsPage
布局会在路由切换中被复用,但不要把它简单理解成"外层永远不重新渲染"。实际行为还会受到导航方式、缓存、数据依赖和客户端状态影响;它的工程价值是把共享 UI 和页面内容按路由层级组织起来。
五、Server Component 和 Client Component
App Router 中,组件默认是 Server Component。它们可以在服务端读取数据库、调用内部服务、使用密钥环境变量,但不能使用 useState、事件处理函数或浏览器 API。
tsx
// app/about/page.tsx
export default function AboutPage() {
return <h1>关于我们</h1>;
}
需要交互时,在文件顶部写 "use client":
tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((value) => value + 1)}>{count}</button>;
}
关键理解:"use client" 表示该模块及其依赖进入客户端 JavaScript 边界,不等于它只会在浏览器生成 HTML。Next.js 仍可能先为初始页面生成 HTML,再由浏览器下载 JavaScript 并水合,使事件能够工作。
选型很简单:
| 需求 | 优先方案 |
|---|---|
| 读取服务端数据、展示内容 | Server Component |
| 点击、输入、状态、浏览器 API | Client Component |
| 既要首屏数据又要交互 | Server Page 获取数据,传给小型 Client Component |
不要把整页都标成 "use client"。把交互边界缩小到真正需要状态和事件的组件,通常能减少浏览器需要下载的 JavaScript。
六、Route Handler:同仓库提供 HTTP 接口
在 app/api 下创建 route.ts,导出的 HTTP 方法就是接口入口:
ts
// app/api/todos/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const todos = await todoRepository.list();
return NextResponse.json(todos);
}
GET、POST、PUT、PATCH、DELETE 等导出函数分别对应 HTTP 方法。Route Handler 适合 BFF、Webhook、表单提交或给客户端组件提供接口。
不过它不是"免部署的完整后端"。数据库、认证、权限、限流、日志和持久化仍然要按后端标准设计。
特别注意:下面这种内存数组只适合教程演示:
ts
const todos = [];
在开发热更新、无服务器实例扩缩容和多进程环境中,它不会可靠持久化。生产数据应存进数据库或外部持久化服务。
七、Todo 实战:服务端拿初始数据,客户端做交互
页面组件可以直接在服务端获取初始列表:
tsx
// app/todos/page.tsx
import { TodoList } from "./todo-list";
export default async function TodosPage() {
const todos = await todoRepository.list();
return <TodoList initialTodos={todos} />;
}
列表组件只处理用户交互:
tsx
// app/todos/todo-list.tsx
"use client";
import { useState } from "react";
export function TodoList({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
async function addTodo(content) {
const response = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify({ content }),
});
const todo = await response.json();
setTodos((items) => [...items, todo]);
}
}
这个结构有两个好处:首次列表数据不必等浏览器 useEffect 再请求一次;浏览器只下载真正需要交互的 TodoList 代码。
正式项目还需要处理请求失败、加载状态、输入校验和接口认证。客户端提交成功后,也可以使用 router.refresh() 重新获取服务端页面数据;选择乐观更新还是刷新数据,取决于交互复杂度和一致性要求。
八、SEO:先把内容、元数据和可访问性做好
服务端渲染是 SEO 的有利条件,但不是 SEO 的全部。一个更可靠的优先级是:
- 有价值、可索引的页面内容
- 正确的标题、描述和分享元数据
- 可访问的语义化 HTML、稳定 URL 和内部链接
- 合适的数据获取与渲染策略
页面元数据可以通过 metadata 导出声明:
tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "我的 Todo 应用",
description: "使用 Next.js 构建的待办事项应用",
};
不要把"Client Component"直接等同于 SEO 差。真正要问的是:用户和爬虫在首个可访问响应中能拿到什么内容?页面是否有正确元数据、链接与内容结构?
九、常见误区
1. Next.js 等于 SSR
不完全正确。Next.js 可以按页面和数据选择不同策略,包括静态生成、动态服务端渲染和客户端交互。它提供的是选择权,而不是单一渲染模式。
2. use client 后,组件只在浏览器运行
不正确。"use client" 定义客户端边界,组件需要客户端 JavaScript 才能交互;初始 HTML 仍可能由服务端预渲染。
3. 有了 Route Handler 就不需要后端工程
不正确。Route Handler 解决的是接口入口位置,不会自动解决数据库、鉴权、缓存、可观测性和安全问题。
十、总结
| 概念 | 一句话理解 |
|---|---|
| App Router | 用目录和约定组织页面、布局和接口 |
page.tsx |
当前路由的页面内容 |
layout.tsx |
当前路由及后代的共享结构 |
| Server Component | 默认服务端组件,适合数据读取和内容展示 |
| Client Component | "use client",适合交互和浏览器 API |
| Route Handler | route.ts 中导出 HTTP 方法处理接口 |
| Metadata | 声明页面标题、描述等 SEO 基础信息 |
Next.js 的核心不是"把 React 搬到服务器",而是把路由、服务端数据、客户端交互和接口边界组织在同一套框架约定中。理解 Server/Client Component 的边界后,SPA 与全栈之间不再是二选一,而是根据页面目标选择合适的组合。