第 1 章

Next.js 是什么:全栈框架的设计哲学

第1章:Next.js 是什么:全栈框架的设计哲学

React 只是一个 UI 库,它不知道路由、不知道数据获取、不知道如何生成 HTML 给搜索引擎。Next.js 在 React 之上构建了完整的全栈平台,让 React 应用真正"生产就绪"。

本章核心问题:Next.js 在 React 之上增加了什么,为什么需要这些东西?

读完本章你将理解


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/ 目录下每个文件对应一个路由,通过 getServerSidePropsgetStaticProps 等函数在服务器上获取数据。

// 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.tsxloading.tsxerror.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 的迭代更新。

本章评分
4.7  / 5  (106 评分)

💬 留言讨论