Next.js 是什么:全栈框架的设计哲学
第1章:Next.js 是什么:全栈框架的设计哲学
React 只是一个 UI 库,它不知道路由、不知道数据获取、不知道如何生成 HTML 给搜索引擎。Next.js 在 React 之上构建了完整的全栈平台,让 React 应用真正"生产就绪"。
本章核心问题:Next.js 在 React 之上增加了什么,为什么需要这些东西?
读完本章你将理解:
- 纯客户端 React 应用的三个系统性问题(SEO、加载瀑布、数据瀑布)
- Next.js 的四层架构(路由、渲染、打包、缓存)各自解决什么问题
- App Router 与 Pages Router 的根本区别,以及 React Server Components 为什么重要
Level 1 · 你需要知道的(1-3年经验)
从客户端渲染的困境说起
2013 年 React 诞生时,它解决的是一个特定问题:如何用声明式 UI 描述复杂的交互界面。它做到了,而且做得很好。但 React 本身只是一个 UI 库,它不知道路由、不知道数据获取、不知道如何生成 HTML 给搜索引擎。
当开发者用纯 React 构建应用时,他们通常选择 Create React App(CRA)。CRA 的产物是一个几乎空白的 HTML 文件:
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div id="root"></div>
<script src="/static/js/main.chunk.js"></script>
</body>
</html>
浏览器下载这个文件,看到的是空白页面。然后它下载 JavaScript bundle,执行 React,React 才开始渲染 DOM。用户在此之前看到的是白屏。
这带来三个系统性问题:
SEO 死角。搜索引擎爬虫(即便 Googlebot 已能执行 JavaScript)仍然偏好直接可读的 HTML。纯 SPA 应用的内容对爬虫来说经常是不可见的,或者需要等待 JavaScript 执行完成——这会降低抓取效率和排名。对博客、文档、电商产品页这类内容型页面来说,这是致命缺陷。
初始加载瀑布。用户打开页面的时序是:下载 HTML → 下载 JS bundle → 执行 React → 发起数据请求 → 渲染内容。这个链条上每一步都需要等待上一步完成。在慢速网络(4G、移动端)上,用户可能需要等待 3-5 秒才能看到实质内容。
数据获取瀑布(Waterfall)。在 React 中,数据获取通常发生在 useEffect 里:
// 客户端数据获取 — 典型的瀑布问题
function BlogPost({ slug }: { slug: string }) {
const [post, setPost] = useState<Post | null>(null);
const [author, setAuthor] = useState<Author | null>(null);
useEffect(() => {
// 第一个请求:获取文章
fetch(`/api/posts/${slug}`)
.then(res => res.json())
.then(data => {
setPost(data);
// 第二个请求:必须等文章加载完才知道 authorId
return fetch(`/api/authors/${data.authorId}`);
})
.then(res => res.json())
.then(setAuthor);
}, [slug]);
if (!post || !author) return <div>Loading...</div>;
return <Article post={post} author={author} />;
}
这段代码存在串行请求:必须先得到文章数据,才能知道 authorId,才能请求作者信息。两个请求是顺序发出的,而不是并行的。更糟糕的是,所有这些都发生在客户端——服务器没有参与任何数据预处理。
Next.js 加了什么
Next.js 不是另一个 UI 框架,它是 React 的运行时平台。它在 React 之上增加了四个关键层:
路由层。基于文件系统的路由,文件结构即 URL 结构,无需手动配置 React Router。
渲染层。支持服务端渲染(SSR)、静态生成(SSG)、流式渲染(Streaming)。在服务器上生成 HTML,直接发送给浏览器和爬虫。
打包层。内置 webpack(现在是 Turbopack),自动处理代码分割、Tree Shaking、图片优化、字体内联等。
缓存层。多层缓存策略——路由缓存、数据缓存、完整路由缓存——让应用在不牺牲动态能力的情况下获得接近静态站点的性能。
这四层构成了一个完整的、有主见的(opinionated)全栈平台。
React Server Components:App Router 的基石
理解 App Router,必须先理解 React Server Components 是什么,以及它为什么重要。
RSC 引入了一个新的组件分类:服务器组件默认在服务器上运行,永远不会发送到客户端;客户端组件(用 'use client' 标记)才是传统意义上的 React 组件,在浏览器中运行。
服务器组件可以直接访问数据库、文件系统、内部 API,无需经过 HTTP 层。它们的代码永远不会出现在客户端 bundle 中——即使你在服务器组件里 import 了一个 1MB 的 JSON 文件,客户端也不会下载这个文件,只会收到渲染后的 HTML。
这让"组件级数据获取"成为可能:
// app/blog/[slug]/page.tsx — 服务器组件(默认)
// 这个组件只在服务器上运行,可以直接查询数据库
import { db } from '@/lib/db';
import { notFound } from 'next/navigation';
interface Props {
params: Promise<{ slug: string }>;
}
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params;
// 直接查询数据库,无需 API 层
const post = await db.posts.findOne({ slug });
if (!post) {
notFound(); // 自动渲染 not-found.tsx
}
// 并行获取作者信息——无需等待 post 的 authorId
// 因为我们在服务器上,可以直接查
const [author, relatedPosts] = await Promise.all([
db.authors.findOne({ id: post.authorId }),
db.posts.findRelated({ tags: post.tags, limit: 3 }),
]);
return (
<article>
<h1>{post.title}</h1>
<AuthorCard author={author} />
<PostContent content={post.content} />
<RelatedPosts posts={relatedPosts} />
</article>
);
}
对比之前的客户端 useEffect 版本:
| 维度 | useEffect 客户端获取 | 服务器组件直接获取 |
|---|---|---|
| 请求时机 | JS 执行后才发起 | 服务器渲染时已完成 |
| 请求位置 | 客户端 → API → 数据库 | 服务器 → 数据库(内网直连) |
| 白屏时间 | 存在(等待 JS + 数据) | 无(HTML 含数据) |
| Bundle 大小 | 数据获取逻辑进 bundle | 不进 bundle |
| SEO | 内容不可见或延迟可见 | 内容直接在 HTML 中 |
Level 2 · 它是怎么运行的(3-5年经验)
Pages Router 的历史与 App Router 的革命
Next.js 从 2016 年发布,最初以 Pages Router 为核心。Pages Router 的模型很直观:pages/ 目录下每个文件对应一个路由,通过 getServerSideProps、getStaticProps 等函数在服务器上获取数据。
// Pages Router 时代 — pages/blog/[slug].tsx
export async function getServerSideProps({ params }) {
const post = await db.posts.findOne({ slug: params.slug });
return { props: { post } };
}
export default function BlogPost({ post }) {
return <Article post={post} />;
}
这个模型工作得不错,但存在根本局限:数据获取与组件是分离的。getServerSideProps 只能在页面级别运行,不能在深层子组件中运行。这意味着即使只有页面一小部分需要服务端数据,也必须在顶层一次性获取所有数据,然后通过 props 层层传递。
2023 年,Next.js 13 引入 App Router,建立在 React Server Components(RSC)之上。这是一次根本性的架构革命,不是渐进改良。
"零 API"全栈模型
App Router 带来的最深刻变革是:对于数据读取操作,你不再需要 API 路由。
传统思维认为前后端必须通过 API 通信——前端 fetch /api/posts,后端处理请求返回 JSON,前端再渲染。这个模式在 Pages Router 时代仍然是主流。
在 App Router 中,服务器组件可以直接调用数据库、ORM、或任何服务端函数。这不是绕过架构约束,而是从根本上重新定义了"前端"和"后端"的边界:
// lib/data.ts — 服务端数据访问层(不是 API)
import { db } from './db';
export async function getPostBySlug(slug: string) {
return db.posts.findOne({
where: { slug },
include: { author: true, tags: true },
});
}
// app/blog/[slug]/page.tsx — 直接调用,无需 fetch
import { getPostBySlug } from '@/lib/data';
export default async function Page({ params }: Props) {
const { slug } = await params;
const post = await getPostBySlug(slug);
// ...
}
当然,API 路由(route.ts)仍然有其用途:为移动端、第三方客户端提供接口,或处理 Webhook、OAuth 回调等场景。但对于同构的 Web 应用,很多情况下你根本不需要写 API。
这个"零 API"模型减少了一整个抽象层,消除了序列化/反序列化开销,消除了额外的 HTTP 往返,也消除了大量的 TypeScript 类型对齐工作(不再需要为 API 响应单独定义类型,因为函数返回类型就是组件接收的类型)。
Level 3 · 规范怎么定义的(资深)
设计哲学的核心
Next.js 15 的设计哲学可以归结为三个原则:
在正确的地方运行正确的代码。数据库查询在服务器,交互逻辑在客户端,渲染尽量在服务器。不是所有东西都需要进客户端 bundle。
约定优于配置。文件系统路由、特殊文件名(layout.tsx、loading.tsx、error.tsx)、默认行为(服务器组件优先)——这些约定减少了决策成本,让团队聚焦在业务逻辑上。
性能是默认值,不是优化项。自动代码分割、图片优化、字体子集、流式渲染——这些在 Next.js 中是开箱即用的,不需要额外配置。
理解了这三个原则,你就理解了为什么 App Router 的很多设计决策看起来"奇怪"但其实是合理的。后续章节的每一个技术细节,都可以追溯到这里。
这些原则的形成有其历史背景。Vercel(Next.js 的创建公司)在 2020 年的 RFC 中明确提出了"零配置框架"的愿景,认为开发者不应该花时间在打包配置、路由配置和缓存策略上。React 团队在 React 18/19 中引入的 Server Components 和 Suspense 架构为这一愿景提供了技术基础,Next.js App Router 是这两方合作的产物。
Level 4 · 边界与陷阱(所有人)
陷阱一:误以为 App Router 只是 Pages Router 的升级
App Router 不是 Pages Router 的"v2",它是一个完全不同的心智模型。Pages Router 的 getServerSideProps 是页面级的数据获取钩子;App Router 的 Server Components 是组件级的,每个组件都可以是 async 的,每个组件都可以直接获取数据。如果你用 Pages Router 的思维写 App Router 代码,会写出很多不必要的 API 路由和客户端 fetch。
陷阱二:对 RSC 的序列化约束认识不足
Server Components 传给 Client Components 的 props 必须是可序列化的——不能传函数、不能传类实例、不能传 Date 对象(需要先转为字符串)。很多初学者在这里踩坑,运行时才发现某个 prop 无法序列化。
陷阱三:过度使用 'use client'
新接触 App Router 的开发者常常在遇到 hooks 报错时把整个页面标记为 'use client',这会失去 Server Components 的所有优势(零 bundle、服务端数据获取、SEO)。正确做法是将交互逻辑拆分到最小的客户端组件中,保持页面主体为服务器组件。
小结
Next.js 解决的不是"如何让 React 更好用",而是"如何让 React 应用生产就绪"。它在 React 之上构建了路由、渲染、打包、缓存四个层,将服务器端能力直接引入组件树,用文件系统约定替代繁琐配置。App Router 基于 React Server Components,让数据获取回归到组件层,消除了客户端渲染的白屏问题、SEO 问题和数据瀑布问题。这是框架层面的一次范式转移,而不只是 API 的迭代更新。