文章

Next.js核心原理深度解析

Next.js核心原理深度解析

一句话概括

Next.js 是 Vercel 团队维护的基于 React 的全栈 Web 框架,它通过文件系统路由多种渲染模式(SSR/SSG/ISR)React Server Components 三大核心设计,将前端开发者从繁琐的构建配置、路由管理和渲染策略选择中解放出来。其最突出的创新在于:以文件路径作为路由声明、以导出函数控制渲染行为、以 App Router 实现服务端组件和客户端组件的无缝混用。Next.js 不仅是 SSR 框架的事实标准,更代表了”全栈 React”的架构方向,在 2025-2026 年的前端生态中占据了统治地位。

背景与意义

Next.js 的诞生与演进

回顾前端框架的发展史,2016 年是一个关键节点。彼时 React 已经证明了组件化开发的优越性,但有两个痛点始终没有得到很好的解决:

  1. 路由配置繁琐:需要手动安装和配置 react-router,路由分散在多个文件中。
  2. SSR 配置复杂:自己搭建 SSR 服务器需要处理 webpack 配置、Node.js 中间件、hydration 等问题,工程量大且容易出错。

Next.js 正是在这个背景下诞生的。创始人 Guillermo Rauch(同时也是 Socket.IO 和 MongoDB 的作者)的目标很简单:让 React 应用的路由和 SSR 像创建文件一样简单。

经过 8 年的发展,Next.js 已经从最初的 Pages Router 演变到了 App Router(基于 React Server Components),累计获得了超过 130 万 GitHub Star,成为 Vercel 生态的核心产品。

为什么理解 Next.js 核心原理很重要?

在面试场景中,Next.js 相关问题的出现频率在 2024-2026 年间持续攀升:

  • 初级前端:考察基本的路由定义和数据获取方式。
  • 中级前端:考察 SSG/ISR/SSR 的适用场景选择和配置。
  • 高级前端:考察 App Router 的架构设计和 React Server Components 的原理。

在实际项目中,Next.js 已经承包了从个人博客到 Shopify、TikTok、Twitch 等大型商业应用的构建需求。不理解其核心原理,就无法做出正确的架构决策——比如什么时候用 SSG 代替 SSR、ISR 的 revalidate 间隔应该如何设置、Server Components 和 Client Components 的分界在哪里。

概念与定义

什么是 Next.js?

Next.js 是一个生产级的 React 全栈框架,提供以下核心能力:

  • 文件系统路由:在 pages/app/ 目录中创建文件即声明路由。
  • 多种渲染策略:静态生成(SSG)、服务端渲染(SSR)、增量静态再生(ISR)。
  • API 路由:在同一个项目中编写服务端 API 接口。
  • App Router:基于 React Server Components 的新一代路由系统。
  • 图像优化、字体优化、中间件等功能

渲染模式速览

模式生成时机数据新鲜度适合场景
SSG (Static Generation)构建时构建时确定博客、文档、营销页
SSR (Server-Side Rendering)请求时每次请求最新个性化内容、实时数据
ISR (Incremental Static Regeneration)构建时 + 按需更新可配置 TTL电商商品页、新闻列表
SSG (Static + Client Fetch)构建时 + 客户端更新混合需要 SEO + 动态部分

Pages Router vs App Router

Next.js 13 引入的 App Router 是一次架构革命:

  • Pages Router:所有组件都是客户端组件,通过 getServerSideProps / getStaticProps 控制渲染模式。
  • App Router:组件默认是服务端组件(Server Components),需要交互时在文件顶部声明 'use client'

核心知识点拆解

1. 文件系统路由原理

Next.js 最令人印象深刻的设计是文件即路由。理解其内部实现,有助于解决路由相关的各种问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// pages/index.tsx - 根路由 /
export default function HomePage() {
  return <h1>Home Page</h1>;
}

// pages/blog/[slug].tsx - 动态路由 /blog/:slug
import { useRouter } from 'next/router';

export default function BlogPost() {
  const router = useRouter();
  const { slug } = router.query;

  return <article>Blog post: {slug}</article>;
}

// pages/blog/[...catchAll].tsx - 捕获所有子路径
// 匹配 /blog/a, /blog/a/b, /blog/a/b/c ...
export default function CatchAll() {
  const router = useRouter();
  const { catchAll } = router.query; // ['a'], ['a', 'b'], ['a', 'b', 'c']
  return <div>Path segments: {catchAll?.join(' / ')}</div>;
}

// pages/[[...optionalCatchAll]].tsx - 可选捕获所有(包含根节点)
// 同时匹配 /blog 和 /blog/a/b
export default function OptionalCatchAll() {
  const router = useRouter();
  const { optionalCatchAll } = router.query;
  return <div>Optional catch all: {JSON.stringify(optionalCatchAll)}</div>;
}

文件路由的核心逻辑——Next.js 在构建时会执行一个路由发现阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// 简化的路由发现机制
import fs from 'fs';
import path from 'path';

interface RouteConfig {
  path: string;       // 路由路径,如 /blog/[slug]
  file: string;       // 对应文件路径
  isDynamic: boolean;  // 是否动态路由
  params: string[];    // 路由参数名,如 ['slug']
}

function discoverRoutes(pagesDir: string): RouteConfig[] {
  const routes: RouteConfig[] = [];

  function walkDir(dir: string, basePath: string = '') {
    const entries = fs.readdirSync(dir, { withFileTypes: true });

    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      const routePath = path.join(basePath, entry.name);

      if (entry.isDirectory()) {
        walkDir(fullPath, routePath);
      } else if (entry.isFile() && /\.(tsx|jsx|ts|js)$/.test(entry.name)) {
        // 去除扩展名
        let normalizedPath = routePath.replace(/\.(tsx|jsx|ts|js)$/, '');

        // 处理 index 路由:/blog/index → /blog
        if (normalizedPath.endsWith('/index')) {
          normalizedPath = normalizedPath.replace('/index', '');
        }

        // 处理动态路由:[slug] → :slug
        const params: string[] = [];
        const pathPattern = normalizedPath.replace(/\[\[\.\.\.(\w+)\]\]|\[\.\.\.(\w+)\]|\[(\w+)\]/g, (match, optionalCatchAll, catchAll, param) => {
          if (optionalCatchAll) {
            params.push(optionalCatchAll);
            return `:${optionalCatchAll}*`; // 可选通配符
          }
          if (catchAll) {
            params.push(catchAll);
            return `:${catchAll}+`; // 通配符
          }
          if (param) {
            params.push(param);
            return `:${param}`; // 参数
          }
          return match;
        });

        routes.push({
          path: normalizedPath || '/',
          file: fullPath,
          isDynamic: params.length > 0,
          params
        });
      }
    }
  }

  walkDir(pagesDir);
  return routes;
}

// 使用示例
// pages/ 目录结构:
//   pages/
//     index.tsx           →  /
//     about.tsx           →  /about
//     blog/
//       index.tsx         →  /blog
//       [slug].tsx        →  /blog/:slug
//     api/
//       hello.ts          →  /api/hello
const routes = discoverRoutes('./pages');
console.log(routes);
// [
//   { path: '/', file: '.../index.tsx', isDynamic: false, params: [] },
//   { path: '/about', file: '.../about.tsx', isDynamic: false, params: [] },
//   { path: '/blog', file: '.../blog/index.tsx', isDynamic: false, params: [] },
//   { path: '/blog/:slug', file: '.../blog/[slug].tsx', isDynamic: true, params: ['slug'] }
// ]

2. SSG(静态生成)的工作原理

SSG 是 Next.js 中最常用的渲染模式。当页面可以预先生成时,SSG 提供了最好的性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// pages/blog/[slug].tsx - SSG 模式

import { GetStaticProps, GetStaticPaths } from 'next';

// 定义页面 props 的类型
interface BlogPostProps {
  post: {
    slug: string;
    title: string;
    content: string;
    createdAt: string;
  };
  buildTime: string;
}

// 页面组件
export default function BlogPost({ post, buildTime }: BlogPostProps) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div>构建时间: {buildTime}</div>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

// 1. getStaticPaths: 告诉 Next.js 需要预渲染哪些动态路径
// 构建时执行一次
export async function getStaticPaths(): Promise<GetStaticPaths> {
  // 从 CMS 获取所有文章的 slug
  const res = await fetch('https://cms.example.com/api/posts');
  const posts: Array<{ slug: string }> = await res.json();

  // 生成所有需要预渲染的路径
  const paths = posts.map((post) => ({
    params: { slug: post.slug }
  }));

  // fallback 控制未预渲染路径的行为:
  // false → 返回 404
  // true  → 服务端动态生成并缓存
  // 'blocking' → 像 SSR 一样等待生成
  return { paths, fallback: 'blocking' };
}

// 2. getStaticProps: 为每个路径获取数据
// 构建时执行一次,或者 revalidate 到期时重新执行
export async function getStaticProps({ params }: { params: { slug: string } }): Promise<GetStaticProps<BlogPostProps>> {
  const res = await fetch(`https://cms.example.com/api/posts/${params.slug}`);
  const post = await res.json();

  return {
    props: {
      post,
      buildTime: new Date().toISOString()
    },
    // ISR 配置:每 60 秒重新生成一次
    revalidate: 60
  };
}

SSG 构建时的执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Graph: SSG 构建过程
┌──────────────────────────────────────────────────────┐
│  next build                                           │
├──────────────────────────────────────────────────────┤
│  1. 编译所有页面组件                                   │
│                                                       │
│  2. 扫描 pages/ 目录,发现所有路由                      │
│     └─ blog/[slug].tsx 有 getStaticPaths              │
│                                                       │
│  3. 调用 getStaticPaths()                              │
│     └─ 返回 [{ params: { slug: 'hello-world' } },     │
│               { params: { slug: 'react-tutorial' } }] │
│                                                       │
│  4. 对每个路径执行:                                   │
│     a. 调用 getStaticProps({ params: { slug: ... } })  │
│     b. 获取数据 → 传递给组件                            │
│     c. renderToString 生成 HTML                        │
│     d. 写入磁盘: .next/server/pages/blog/[slug].html  │
│                                                       │
│  5. 生成路由映射文件,用于服务端运行时                    │
└──────────────────────────────────────────────────────┘

3. ISR(增量静态再生)的运行时机制

ISR 是 SSG 的增强版,它让静态页面可以在不重新构建整个项目的前提下,定期或在触发时更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// pages/products/[id].tsx - ISR 模式

// 方式一:基于时间的 ISR
export async function getStaticProps({ params }: { params: { id: string } }) {
  const res = await fetch(`https://api.example.com/products/${params.id}`);
  const product = await res.json();

  return {
    props: { product, timestamp: Date.now() },
    // ✅ 关键配置:每隔 60 秒重新生成
    revalidate: 60
  };
}

// 方式二:按需 ISR(On-Demand ISR)- Next.js 12.1+
// 通过 API 路由手动触发页面重新生成
// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // 验证请求来源(安全校验)
  if (req.query.secret !== process.env.REVALIDATION_SECRET) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  try {
    const { path } = req.body;
    // 重新生成指定路径的页面
    await res.revalidate(path);
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).send('Error revalidating');
  }
}

// 在产品 CMS 中,当编辑保存后触发重新生成
// 假设 CMS 的 Webhook 配置指向 /api/revalidate

ISR 的运行时流程——理解 ISR 的缓存机制是正确配置的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// ISR 运行时简化的逻辑
interface ISRCacheEntry {
  html: string;
  props: any;
  createdAt: number;     // 页面生成时间戳
  revalidate: number;    // 过期时间间隔(秒)
}

class ISRCache {
  // 内存缓存(实际生产环境使用 LRU Cache)
  private cache = new Map<string, ISRCacheEntry>();

  // 获取页面
  async getPage(path: string): Promise<{ html: string; isStale: boolean }> {
    const entry = this.cache.get(path);

    if (!entry) {
      // 首次访问或缓存被清除:动态生成
      return { html: await this.generatePage(path), isStale: false };
    }

    const age = (Date.now() - entry.createdAt) / 1000;

    if (age < entry.revalidate) {
      // 缓存有效:直接返回(最快)
      return { html: entry.html, isStale: false };
    }

    // 缓存过期:返回旧页面的同时启动后台重新生成
    // 这是 ISR 的精髓——用户永远不会等待重新生成
    this.staleWhileRevalidate(path);
    return { html: entry.html, isStale: true };
  }

  // 后台重新生成
  private async staleWhileRevalidate(path: string) {
    const newHtml = await this.generatePage(path);
    // 生成完成后更新缓存
    this.cache.set(path, {
      html: newHtml,
      props: {},
      createdAt: Date.now(),
      revalidate: 60
    });
  }

  // 客户端触发按需重新生成
  async revalidateOnDemand(path: string) {
    const newHtml = await this.generatePage(path);
    this.cache.set(path, {
      html: newHtml,
      props: {},
      createdAt: Date.now(),
      revalidate: 60
    });
  }

  private async generatePage(path: string): Promise<string> {
    // 实际逻辑:调用 getStaticProps 获取 props
    // 然后调用 renderToString 生成 HTML
    // 这里返回模拟结果
    return `<html>...</html>`;
  }
}

4. App Router 与 React Server Components

App Router 是 Next.js 13.4+ 引入的新一代路由系统,其核心是 React Server Components(RSC)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// app/layout.tsx - 根布局(所有页面共享)
// App Router 中,布局是嵌套的,不会重新渲染
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-CN">
      <body>
        <header>
          <nav>
            <a href="/">首页</a>
            <a href="/blog">博客</a>
            <a href="/about">关于</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>© 2026 Next.js Demo</footer>
      </body>
    </html>
  );
}

// app/blog/page.tsx - 博客列表页
// 默认是 Server Component,可以直接使用 async/await
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    // App Router 中,fetch 默认缓存
    // next: { revalidate: 3600 } 控制 ISR 缓存
    next: { revalidate: 3600 }
  });
  return res.json();
}

export default async function BlogPage() {
  // 直接在组件中 await(Server Component 专属能力)
  const posts = await getPosts();

  return (
    <div className="blog-grid">
      {posts.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

// app/blog/[slug]/page.tsx - 文章详情页
// 动态路由在 App Router 中的写法
interface PageProps {
  params: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
}

export default async function BlogPost({ params }: PageProps) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then(res => res.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <ContentRenderer content={post.content} />
      {/* Server Component 可以安全地获取 token 等敏感信息 */}
      <LikeButton postId={post.id} initialLikes={post.likes} />
    </article>
  );
}

// app/components/LikeButton.tsx - 客户端组件
// 需要交互的组件使用 'use client' 声明
'use client';

import { useState } from 'react';

export default function LikeButton({
  postId,
  initialLikes
}: {
  postId: string;
  initialLikes: number;
}) {
  const [likes, setLikes] = useState(initialLikes);
  const [loading, setLoading] = useState(false);

  async function handleLike() {
    setLoading(true);
    const res = await fetch('/api/like', {
      method: 'POST',
      body: JSON.stringify({ postId })
    });
    const data = await res.json();
    setLikes(data.totalLikes);
    setLoading(false);
  }

  return (
    <button
      onClick={handleLike}
      disabled={loading}
      className="like-button"
    >
      {loading ? '...' : '❤️'} {likes}
    </button>
  );
}

Server Component vs Client Component 的对比

特性Server ComponentClient Component
渲染位置服务端浏览器
可交互性
可访问服务端资源是(数据库、文件系统、token)
可包含客户端状态是(useState, useEffect)
Bundle 大小不增加客户端 JS全部打包到客户端
数据获取直接 async/awaituseEffect 或 SWR/React Query
声明方式默认'use client'

实战案例

构建一个完整的 Next.js 电商产品页

让我们构建一个使用 App Router 的电商产品详情页,同时展示 SSR、ISR 和 Server Components 的协作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// app/products/[id]/page.tsx - 产品详情页
import { notFound } from 'next/navigation';
import { Metadata } from 'next';
import ProductGallery from './ProductGallery';
import AddToCartButton from './AddToCartButton';
import RelatedProducts from './RelatedProducts';
import PriceHistory from './PriceHistory';
import ReviewSection from './ReviewSection';

// 数据接口定义
interface Product {
  id: string;
  name: string;
  price: number;
  originalPrice: number;
  description: string;
  images: string[];
  specs: Record<string, string>;
  inventory: number;
  category: string;
  rating: number;
  reviewCount: number;
}

// SEO 元数据生成(Server Component 专属能力)
export async function generateMetadata(
  { params }: { params: { id: string } }
): Promise<Metadata> {
  const product = await fetchProduct(params.id);

  return {
    title: `${product.name} - 在线商城`,
    description: product.description.substring(0, 160),
    openGraph: {
      images: [product.images[0]],
      description: product.description.substring(0, 200)
    }
  };
}

// 数据获取函数
async function fetchProduct(id: string): Promise<Product> {
  const res = await fetch(
    `https://api.example.com/products/${id}`,
    {
      // ISR: 每 30 秒重新验证一次
      next: { revalidate: 30 },
      // 读超时设置
      signal: AbortSignal.timeout(5000)
    }
  );

  if (!res.ok) {
    if (res.status === 404) {
      notFound(); // 触发 404
    }
    throw new Error('Failed to fetch product');
  }

  return res.json();
}

// 主页面组件(Server Component)
export default async function ProductPage(
  { params }: { params: { id: string } }
) {
  const product = await fetchProduct(params.id);

  // 判断促销标签
  const discount = Math.round(
    (1 - product.price / product.originalPrice) * 100
  );

  return (
    <div className="product-page">
      {/* 左侧:产品图片画廊 */}
      <div className="product-gallery">
        <ProductGallery images={product.images} productName={product.name} />
      </div>

      {/* 右侧:产品信息和操作 */}
      <div className="product-info">
        <h1 className="product-name">{product.name}</h1>

        {/* 价格区域 */}
        <div className="price-section">
          <span className="current-price">¥{product.price.toFixed(2)}</span>
          {discount > 0 && (
            <>
              <span className="original-price">
                ¥{product.originalPrice.toFixed(2)}
              </span>
              <span className="discount-badge">-{discount}%</span>
            </>
          )}
        </div>

        {/* 评分 */}
        <div className="rating">
          {''.repeat(Math.round(product.rating))}
          {''.repeat(5 - Math.round(product.rating))}
          <span className="review-count">
            ({product.reviewCount} 条评价)
          </span>
        </div>

        {/* 规格参数 */}
        <div className="specs">
          <h3>规格参数</h3>
          <table>
            <tbody>
              {Object.entries(product.specs).map(([key, value]) => (
                <tr key={key}>
                  <td className="spec-key">{key}</td>
                  <td className="spec-value">{value}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>

        {/* 库存状态 */}
        <div className={`inventory ${product.inventory > 0 ? 'in-stock' : 'out-of-stock'}`}>
          {product.inventory > 0
            ? `有货 (库存 ${product.inventory} 件)`
            : '暂时缺货'}
        </div>

        {/* 加入购物车(Client Component - 需要交互) */}
        <AddToCartButton
          productId={product.id}
          disabled={product.inventory === 0}
        />

        {/* 描述 */}
        <div
          className="product-description"
          dangerouslySetInnerHTML={{ __html: product.description }}
        />
      </div>

      {/* 商品详情区域 */}
      <div className="product-details">
        {/* 价格走势(Client Component - 需要客户端图表) */}
        <PriceHistory productId={product.id} />

        {/* 评价区域(Client Component - 需要分页和交互) */}
        <ReviewSection productId={product.id} />

        {/* 相关推荐(嵌套的 Server Component) */}
        <RelatedProducts
          category={product.category}
          excludeId={product.id}
        />
      </div>
    </div>
  );
}

// 生成静态路径(适用于热门商品)
export async function generateStaticParams() {
  const products = await fetch(
    'https://api.example.com/products/popular'
  ).then(res => res.json());

  return products.map((product: Product) => ({
    id: product.id
  }));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// app/products/[id]/AddToCartButton.tsx - 客户端交互组件
'use client';

import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';

export default function AddToCartButton({
  productId,
  disabled
}: {
  productId: string;
  disabled: boolean;
}) {
  const [isPending, startTransition] = useTransition();
  const [added, setAdded] = useState(false);
  const router = useRouter();

  async function handleAddToCart() {
    // 调用购物车 API
    const res = await fetch('/api/cart/add', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ productId, quantity: 1 })
    });

    if (res.ok) {
      setAdded(true);
      // 触发服务端组件重新验证
      startTransition(() => {
        router.refresh(); // 刷新 Server Component 数据
      });

      // 3 秒后重置按钮状态
      setTimeout(() => setAdded(false), 3000);
    }
  }

  return (
    <button
      onClick={handleAddToCart}
      disabled={disabled || isPending}
      className={`add-to-cart-btn ${added ? 'added' : ''}`}
    >
      {disabled
        ? '暂时缺货'
        : added
          ? '✓ 已加入购物车'
          : isPending
            ? '添加中...'
            : '加入购物车'}
    </button>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// app/api/cart/add/route.ts - API Route(App Router 方式)
import { NextRequest, NextResponse } from 'next/server';

// App Router 中,API Route 使用 Web 标准的 Request/Response
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { productId, quantity } = body;

    // 验证输入
    if (!productId || typeof quantity !== 'number' || quantity < 1) {
      return NextResponse.json(
        { error: 'Invalid input' },
        { status: 400 }
      );
    }

    // 从 cookie 获取用户会话(实际项目使用 JWT/session)
    const sessionId = request.cookies.get('session_id')?.value;

    // 模拟数据库操作
    console.log(`[Cart] User ${sessionId}: add ${productId} x${quantity}`);

    // revalidate 相关页面
    // 这里可以触发 on-demand ISR

    return NextResponse.json({
      success: true,
      message: '已添加到购物车'
    });
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

底层原理

getStaticProps / getServerSideProps 的实现机制

Next.js 的核心抽象就是用函数控制渲染模式。下面我们揭开这些函数的底层实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// 简化的 Next.js 渲染引擎核心逻辑

type DataFetcher = (context: any) => Promise<{ props: any; revalidate?: number }>;
type PageModule = {
  default: React.ComponentType<any>;
  getStaticProps?: DataFetcher;
  getStaticPaths?: () => Promise<{ paths: any[]; fallback: boolean | 'blocking' }>;
  getServerSideProps?: DataFetcher;
};

class NextRenderer {
  // 判断渲染模式
  private getRenderMode(pageModule: PageModule): 'ssg' | 'ssr' | 'isr' {
    if (pageModule.getStaticProps && pageModule.getStaticPaths) {
      return 'ssg';
    }
    if (pageModule.getStaticProps) {
      // 有 revalidate → ISR
      return 'isr';
    }
    if (pageModule.getServerSideProps) {
      return 'ssr';
    }
    // 默认:页面没有数据依赖,视为纯静态
    return 'ssg';
  }

  // 核心渲染方法
  async renderPage(
    pagePath: string,
    pageModule: PageModule,
    params: Record<string, string>,
    query: Record<string, string | string[]>
  ): Promise<{ html: string; headers: Record<string, string> }> {
    const mode = this.getRenderMode(pageModule);
    let props = {};
    let revalidate: number | undefined;

    switch (mode) {
      case 'ssg':
      case 'isr': {
        // 检查缓存
        const cacheKey = `${pagePath}:${JSON.stringify(params)}`;
        const cached = this.cacheManager.get(cacheKey);

        if (cached && !this.isExpired(cached, revalidate)) {
          // 缓存命中且未过期
          return { html: cached.html, headers: { 'x-cache': 'HIT' } };
        }

        // 需要生成(首次或过期)
        const dataResult = await pageModule.getStaticProps!({ params });
        props = dataResult.props;
        revalidate = dataResult.revalidate;

        // 生成 HTML
        const html = this.renderToString(
          React.createElement(pageModule.default, props)
        );

        // 缓存
        if (revalidate) {
          this.cacheManager.set(cacheKey, {
            html,
            props,
            createdAt: Date.now(),
            revalidate
          });
        }

        return {
          html,
          headers: {
            'x-cache': 'MISS',
            ...(revalidate ? { 'x-next-revalidate': String(revalidate) } : {})
          }
        };
      }

      case 'ssr': {
        // 每次请求都重新获取数据
        const dataResult = await pageModule.getServerSideProps!({
          params,
          query,
          req: this.currentRequest,
          res: this.currentResponse
        });

        props = dataResult.props;

        const html = this.renderToString(
          React.createElement(pageModule.default, props)
        );

        return {
          html,
          headers: {
            'x-cache': 'DYNAMIC',
            'cache-control': 'no-cache, no-store, must-revalidate'
          }
        };
      }

      default:
        throw new Error(`Unknown render mode for ${pagePath}`);
    }
  }

  private renderToString(element: React.ReactElement): string {
    // 实际调用 react-dom/server 的 renderToString
    // 包装了错误处理、流式渲染等逻辑
    return '';
  }

  private cacheManager = {
    store: new Map<string, { html: string; props: any; createdAt: number; revalidate: number }>(),
    get(key: string) {
      return this.store.get(key);
    },
    set(key: string, value: any) {
      this.store.set(key, value);
    },
    invalidate(key: string) {
      this.store.delete(key);
    }
  };

  private isExpired(
    entry: { createdAt: number },
    revalidate?: number
  ): boolean {
    if (!revalidate) return false;
    const age = (Date.now() - entry.createdAt) / 1000;
    return age >= revalidate;
  }
}

App Router 的核心:React Server Components 协议

App Router 的底层是 React Server Components 的实现。Server Components 的核心是序列化协议:服务端组件渲染的结果不是 HTML,而是一种特殊的 JSON 格式,称为 RSC Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// RSC Payload 的结构示意
// 这不是 HTML,而是一种描述组件树的特殊格式

// Server Component 渲染后的输出示例
const rscPayload = {
  // 根节点
  node: {
    type: 'layout',
    props: {
      children: {
        type: 'page',
        props: {
          children: [
            {
              // 服务端渲染的静态内容(直接是 HTML 片段)
              type: '$html',
              content: '<h1>商品列表</h1><div class="grid">...</div>'
            },
            {
              // 客户端组件的占位
              type: '$client',
              module: 'app/components/AddToCartButton.js',
              props: { productId: '123' },
              // 此部分等待客户端 hydrate
            }
          ]
        }
      }
    }
  }
};

// 简化版 RSC 解析器
class RSCParser {
  // 将 RSC Payload 转换为 HTML + JS 引用
  parse(
    payload: typeof rscPayload,
    clientModules: Map<string, string>
  ): { html: string; clientRefs: Set<string> } {
    const htmlParts: string[] = [];
    const clientRefs = new Set<string>();

    function traverse(node: any) {
      if (node.type === '$html') {
        // 纯 HTML 片段,直接插入
        htmlParts.push(node.content);
      } else if (node.type === '$client') {
        // 客户端组件:插入一个特殊标记的占位节点
        // 并记录需要加载的客户端模块
        clientRefs.add(node.module);
        htmlParts.push(
          `<div id="__rsc_${clientRefs.size}__" data-module="${node.module}"></div>`
        );
      } else if (node.type === 'layout' || node.type === 'page') {
        // 容器节点:继续处理 children
        if (Array.isArray(node.props.children)) {
          node.props.children.forEach(traverse);
        } else if (node.props.children) {
          traverse(node.props.children);
        }
      }
    }

    traverse(payload.node);
    return { html: htmlParts.join(''), clientRefs };
  }
}

Server Components 的三大核心机制

  1. 零客户端体积:服务器组件中的依赖不会被包含在客户端的 bundle 中。例如,一个 Markdown 渲染库只在服务端使用,客户端永远不需要下载它。

  2. 直接访问后端:服务器组件可以直接访问数据库、文件系统、API 密钥等后端资源,无需通过 API Route 中转。

  3. 自动代码分割:App Router 在服务端就确定了哪些组件需要客户端 JavaScript,并在响应中嵌入模块引用,客户端按需加载。

App Router 的嵌套布局机制

App Router 的一个重要创新是布局的持久性和嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 布局持久化的实现原理
class LayoutManager {
  // 路由 → 布局链的映射
  // /blog/hello-world → [RootLayout, BlogLayout, BlogPostLayout]
  private layoutChain: Map<string, React.ComponentType<any>[]>;

  constructor() {
    this.layoutChain = new Map();
  }

  // 构建布局链
  buildLayoutChain(path: string, projectDir: string) {
    const segments = path.split('/').filter(Boolean);
    const layouts: React.ComponentType<any>[] = [];

    // 从根布局开始
    const rootLayoutPath = `${projectDir}/app/layout.tsx`;
    if (fs.existsSync(rootLayoutPath)) {
      layouts.push(require(rootLayoutPath).default);
    }

    // 逐层查找布局
    let currentPath = '';
    for (const segment of segments) {
      currentPath += `/${segment}`;
      const layoutPath = `${projectDir}/app${currentPath}/layout.tsx`;
      if (fs.existsSync(layoutPath)) {
        layouts.push(require(layoutPath).default);
      }
    }

    this.layoutChain.set(path, layouts);
    return layouts;
  }

  // 布局包装渲染
  renderWithLayouts(
    page: React.ReactElement,
    path: string
  ): React.ReactElement {
    const layouts = this.layoutChain.get(path);
    if (!layouts || layouts.length === 0) return page;

    // 从最内层布局开始向外包装
    let wrapped = page;
    for (let i = layouts.length - 1; i >= 0; i--) {
      const Layout = layouts[i];
      wrapped = React.createElement(Layout, {
        children: wrapped
      });
    }

    return wrapped;
  }

  // 客户端导航时的布局优化
  // 只重新渲染变化的布局层
  navigate(fromPath: string, toPath: string): Set<number> {
    const fromLayouts = this.layoutChain.get(fromPath) || [];
    const toLayouts = this.layoutChain.get(toPath) || [];

    const changedIndexes = new Set<number>();
    const minLength = Math.min(fromLayouts.length, toLayouts.length);

    // 找到第一个不同的布局层级
    let firstDiff = 0;
    for (let i = 0; i < minLength; i++) {
      if (fromLayouts[i] !== toLayouts[i]) {
        firstDiff = i;
        break;
      }
    }

    // 所有不同的层级都需要重新渲染
    for (let i = firstDiff; i < toLayouts.length; i++) {
      changedIndexes.add(i);
    }

    return changedIndexes;
  }
}

高频面试题解析

面试题 1:Next.js 中 getStaticProps、getServerSideProps 和 getStaticPaths 的区别是什么?分别在什么场景下使用?

答案要点

getStaticProps

  • 执行时机:构建时(Build Time),或在 ISR 模式下按配置时间间隔重新执行。
  • 返回值:{ props: {}, revalidate?: number }
  • 适用场景:数据变化不频繁的页面,如博客文章、产品详情页、文档页面。
  • 优势:页面被编译为静态 HTML,可通过 CDN 缓存,加载速度极快。

getServerSideProps

  • 执行时机:每次请求时(Request Time),在服务端执行。
  • 参数:context 包含 reqresparamsquery 等请求相关信息。
  • 适用场景:数据实时性要求高的页面,如用户仪表盘、实时价格、个性化推荐。
  • 注意:每次请求都重新渲染,无法 CDN 缓存,性能开销大。

getStaticPaths

  • 执行时机:构建时,与 getStaticProps 配合使用。
  • 返回值:{ paths: [], fallback: boolean | 'blocking' }
  • 适用场景:动态路由的 SSG 页面,需要告诉 Next.js 哪些路径需要预渲染。
  • fallback 选项至关重要:false 立即 404;true 客户端显示 fallback UI;'blocking' 用户等待服务端生成。

选择指南:能静态化就静态化(SSG),需要定时更新用 ISR,实时性要求极高才用 SSR。

面试题 2:App Router 与 Pages Router 的核心区别是什么?为什么 Next.js 要引入 App Router?

答案要点

核心区别

  1. 组件默认渲染方式不同
    • Pages Router:所有组件默认是客户端组件。
    • App Router:所有组件默认是服务端组件(Server Components),需要用户交互的组件手动声明 'use client'
  2. 数据获取方式不同
    • Pages Router:使用 getStaticProps / getServerSideProps 在组件外部获取数据。
    • App Router:使用 async component 直接在组件内部 await 获取数据。
  3. 布局系统不同
    • Pages Router:布局需要手动实现或使用第三方库。
    • App Router:通过文件系统嵌套实现布局持久化(layout.tsx),导航时布局不会重新渲染。
  4. 路由组织不同
    • Pages Router:扁平的 pages/ 目录。
    • App Router:支持路由组 (group)、平行路由 @slot、拦截路由 (.)path 等高级模式。

引入 App Router 的原因

  • 拥抱 React Server Components 架构,减少客户端 JS 体积。
  • 提供更灵活的路由组织能力,解决复杂应用的布局和数据加载需求。
  • 统一的加载状态管理(loading.tsx)、错误边界(error.tsx)和 404 页面(not-found.tsx)。

面试题 3:ISR(增量静态再生)是如何工作的?它的局限性是什么?

答案要点

ISR 的工作原理(以 revalidate: 60 为例)

  1. 用户 A 首次访问 /products/123,Next.js 在服务端生成页面并缓存。
  2. 在接下来的 60 秒内,所有用户访问都直接返回缓存的静态页面。
  3. 60 秒后,用户 B 访问同一个页面时,Next.js 返回缓存的旧页面,同时在后台重新生成。
  4. 后台生成完成后,缓存更新。用户 C 访问时将得到新页面。

这个过程用户无感知——用户 B 虽然拿到了旧页面,但不会像 SSR 那样等待服务端渲染完成。

局限性

  1. 数据一致性无法保证:用户可能看到过期数据(最多过期一个 revalidate 周期)。
  2. 回退缓存(stale cache)膨胀:如果用户访问了大量动态路径,LRU 缓存可能被填满,老页面被淘汰后需要重新生成。
  3. On-Demand ISR 的逻辑复杂度:需要在 CMS 中配置 Webhook,处理并发重新生成请求。
  4. 不适用于所有场景:用户完全个性化页面(如”为你推荐”)不适合 ISR。

面试题 4:如何优化 Next.js 应用的性能?请列出至少 5 种策略。

答案要点

  1. 渲染策略选择
    • 优先使用 SSG(静态生成),而非 SSR。
    • 需要新鲜度时使用 ISR 代替 SSR。
    • 对于个性化内容多的页面,使用 Client Side Fetch + 骨架屏。
  2. 图片优化
    • 使用 Next.js 内置的 next/image 组件,自动处理响应式图片、WebP 格式和懒加载。
    • 配置 remotePatterns 允许外部图片源。
  3. 代码拆分与懒加载
    1
    2
    3
    4
    5
    6
    
    import dynamic from 'next/dynamic';
    // 大型组件按需加载
    const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
      loading: () => <p>Loading...</p>,
      ssr: false // 不需要 SEO 的组件禁用 SSR
    });
    
  4. 缓存策略
    • 配置 CDN 缓存规则。
    • ISR 的 revalidate 时间设置为合理值(根据数据变化频率调整)。
    • 使用 stale-while-revalidate 缓存策略。
  5. Server Components 最佳实践
    • 尽可能使用 Server Components 减少客户端 JS 体积。
    • 将用户交互部分提取为 Client Components 并用 'use client' 声明。
    • 利用 Streaming SSR 处理慢速数据请求。

面试题 5:Next.js 中的 Middleware(中间件)是什么?它解决了什么问题?

答案要点

定义:Next.js Middleware 允许你在请求完成之前执行代码。它基于 Edge Runtime,运行在 CDN Edge 上,延迟极低。

解决的问题

  1. A/B 测试:在边缘根据 cookie 分流用户到不同页面。
  2. 国际化/本地化:根据请求头或 cookie 重定向到对应语言的页面。
  3. 认证和权限控制:访问受保护页面时检查 token,未认证时重定向到登录页。
  4. 请求重写和重定向:URL 规范化、旧 URL 301 重定向。
  5. Bot 检测:拦截爬虫并返回简化版内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// middleware.ts - 位于项目根目录
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 从 cookie 获取用户区域
  const country = request.cookies.get('country')?.value || request.geo?.country || 'US';
  const currency = CURRENCY_MAP[country] || 'USD';

  // 重写 URL,将区域信息注入请求
  const url = request.nextUrl.clone();
  url.searchParams.set('currency', currency);

  return NextResponse.rewrite(url);
}

// 配置中间件只匹配特定路径
export const config = {
  matcher: [
    '/products/:path*',
    '/((?!api|_next|static|favicon.ico).*)'
  ],
};

总结与扩展

知识体系

Next.js 的知识图谱可以分为以下层次:

  • 基础层:文件路由规则、页面布局、Link 导航、静态资源管理。
  • 数据层:getStaticProps、getServerSideProps、getStaticPaths、ISR 配置。
  • 编译层:Understanding bundling、code splitting、tree shaking。
  • 运行时层:App Router 架构、RSC 协议、Middleware、Edge Runtime。
  • 运维层:自建服务器部署、Vercel 部署、Docker 容器化、CDN 配置。
  • 进阶层:Parallel Routes、Intercepting Routes、Server Actions、Streaming。

延伸阅读

Next.js 的演进不会停止。随着 React 19 的稳定和 Server Actions 的成熟,Next.js 正在将”全栈 React”推向新的高度。理解其核心原理,不仅能帮你在面试中脱颖而出,更能让你在日常开发中做出更好的架构决策。

本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量