文章

Next.js 全栈开发框架:核心特性、功能用法与最佳实践全解

Next.js 全栈开发框架:核心特性、功能用法与最佳实践全解

Next.js 是由 Vercel 公司基于 React 生态研发的生产级全栈 Web 开发框架,是当前 React 技术栈中最主流的企业级开发方案。它解决了传统 React 客户端渲染(CSR)应用首屏性能差、SEO 不友好、工程化配置复杂、全栈能力缺失等核心痛点,从最初的静态站点生成工具,演进为如今基于 React Server Component(RSC)的一站式全栈开发体系,覆盖前端渲染、后端接口、数据处理、部署运维全流程。

截至 2026 年,Next.js 已发布至 16.x 稳定版本,v13.4 起 App Router 成为官方默认路由方案,完全兼容旧版 Pages Router,同时提供了更强大的渲染能力、更灵活的代码组织方式和更极致的性能优化。本文将从基础入门到高级进阶,全面拆解 Next.js 的核心特性、功能用法、实现原理与最佳实践,覆盖企业级开发的全场景需求。


一、Next.js 基础架构与项目初始化

1.1 环境要求与项目创建

Next.js 对运行环境有明确的版本要求,推荐使用 Node.js 18.17 及以上稳定版本,支持 macOS、Windows、Linux 全平台开发。

官方提供了一键项目初始化工具 create-next-app,通过以下命令即可创建标准化的 Next.js 项目:

1
2
3
npx create-next-app@latest my-next-project
# 或指定核心配置快速创建
npx create-next-app@latest my-next-project --typescript --eslint --tailwind --app --src-dir --import-alias "@/*"

初始化命令的核心参数说明:

参数作用最佳实践
--typescript启用 TypeScript 支持,生成类型配置文件企业级项目必选,保障类型安全
--eslint内置 ESLint 代码规范配置,提供 Next.js 专属规则必选,统一团队代码规范
--tailwind一键集成 Tailwind CSS,生成默认配置官方推荐样式方案,提升开发效率
--app启用 App Router 路由体系,而非旧版 Pages Router新项目必选,官方默认推荐方案
--src-dir将源码放在 src/ 目录下,与配置文件隔离大型项目推荐,优化目录结构
--import-alias配置路径别名,默认 @/* 映射 src/ 目录简化导入路径,避免层级嵌套过深

1.2 标准项目目录结构

基于 App Router 模式的 Next.js 项目,核心目录与文件的职责如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
my-next-project/
├── public/                # 静态资源目录,存放图片、字体、PDF等,直接通过根路径访问
├── src/                   # 源码根目录(可选,推荐大型项目使用)
│   └── app/               # App Router 核心目录,所有路由、布局、页面均在此定义
│       ├── layout.tsx     # 根布局文件,必选,全局共享UI,必须包含<html>和<body>标签
│       ├── page.tsx       # 根页面文件,对应网站首页 / 路由
│       ├── not-found.tsx  # 全局404页面,路由不匹配时渲染
│       ├── global-error.tsx # 全局错误捕获页面,捕获根布局的渲染错误
│       ├── about/         # 嵌套路由目录,对应 /about 路径
│       │   └── page.tsx   # 子页面文件,生成可访问路由
│       └── api/           # 接口路由目录,存放路由处理程序
│           └── posts/
│               └── route.ts # 路由处理程序,对应 /api/posts 接口
├── next.config.ts         # Next.js 框架配置文件,自定义构建、路由、性能等配置
├── tsconfig.json          # TypeScript 配置文件,内置类型定义
├── package.json           # 项目依赖与脚本配置
├── .env.local             # 本地环境变量,不会被git提交
└── .eslintrc.json         # ESLint 代码规范配置

1.3 两大路由体系对比

Next.js 同时兼容两套路由方案,官方明确推荐新项目使用 App Router,旧项目可增量迁移:

对比维度App Router(新版,默认)Pages Router(旧版,兼容)
设计理念基于 React Server Component 构建,以文件夹为路由核心,通过特殊文件定义路由行为基于传统 React 客户端渲染,以文件为路由核心,js/ts 文件直接映射为路由
渲染模型支持服务端组件、客户端组件双组件模型,默认服务端渲染,原生支持流式渲染支持 CSR/SSR/SSG/ISR 四种渲染模式,无服务端组件概念
数据获取服务端组件原生支持 async/await 直接获取数据,fetch 内置缓存与去重,Server Actions 处理数据提交必须通过 getServerSideProps/getStaticProps 等约定函数获取数据,API 路由处理提交
代码组织支持路由组、平行路由、拦截路由,可按业务逻辑灵活分组,同一路由可分离布局、加载、错误逻辑所有路由文件必须放在 pages 目录下,无法在路由目录内存放非路由文件,代码组织灵活性差
性能表现服务端组件不打包进客户端 bundle,大幅减少 JS 体积,流式渲染优化首屏性能,局部渲染减少重渲染所有组件代码均需打包进客户端,水合成本高,首屏性能优化成本高
官方支持持续迭代新特性,官方文档优先更新,未来长期维护仅做 bug 修复,无新特性更新,长期将逐步废弃

二、App Router 路由系统(核心特性)

Next.js App Router 的核心是基于文件系统的路由约定,核心逻辑为:文件夹对应 URL 路径段,嵌套文件夹对应嵌套路由路径;特殊文件定义路由的渲染、布局、加载、错误等行为,仅 page.tsx 文件会生成可访问的路由地址

2.1 基础路由定义

基础路由通过「文件夹 + page.tsx」的组合实现,文件夹名称直接映射为 URL 路径段,嵌套文件夹对应多级路由:

文件路径对应访问路由说明
src/app/page.tsx/网站首页,根路由
src/app/about/page.tsx/about关于页面,一级路由
src/app/dashboard/settings/page.tsx/dashboard/settings二级嵌套路由

核心规则:只有包含 page.tsx 的文件夹才会生成可访问的路由,无 page.tsx 的文件夹仅用于存放组件、工具函数、样式等资源,不会影响路由路径,也不会被构建为路由页面。

page.tsx 基础示例:

1
2
3
4
5
6
7
8
9
10
// src/app/about/page.tsx
// Next.js 中组件默认是服务端组件,无需额外声明
export default function AboutPage() {
  return (
    <main className="container mx-auto py-10">
      <h1>关于我们</h1>
      <p>这是基于 Next.js App Router 生成的关于页面</p>
    </main>
  );
}

2.2 动态路由

动态路由用于处理路径段无法提前确定的场景(如文章详情、用户主页、商品详情页),通过方括号修饰文件夹名实现,路由参数会通过 params 属性传递给页面、布局、路由处理程序等组件。

Next.js 提供三种动态路由形式,覆盖全场景需求:

2.2.1 单级动态路由 [folderName]

匹配单个动态路径段,是最常用的动态路由形式。

  • 示例路径:src/app/posts/[slug]/page.tsx
  • 匹配路由:/posts/hello-world/posts/123
  • 参数获取:params.slug 对应路径中的动态值

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/app/posts/[slug]/page.tsx
// Next.js 15+ 中 params 为异步 Promise,必须通过 await 获取
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  // 服务端组件直接通过 slug 获取文章数据
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(res => res.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

2.2.2 全量捕获动态路由 […folderName]

匹配当前路径段及后续所有嵌套路径段,适用于多级分类、文档站点等场景。

  • 示例路径:src/app/docs/[...slug]/page.tsx
  • 匹配路由:/docs/guide/docs/guide/start/docs/api/route/params
  • 参数获取:params.slug 为路径段组成的数组,如 /docs/guide/start 对应 ['guide', 'start']

2.2.3 可选全量捕获动态路由 [[…folderName]]

在全量捕获的基础上,兼容无参数的根路径,是灵活性最高的动态路由形式。

  • 示例路径:src/app/shop/[[...slug]]/page.tsx
  • 匹配路由:/shop/shop/clothes/shop/clothes/t-shirt
  • 参数获取:访问 /shopparams 为空对象,访问带参数路径时与全量捕获规则一致。

2.2.4 预渲染动态路由:generateStaticParams

对于可提前确定动态参数的场景(如博客文章、固定商品分类),可通过 generateStaticParams 函数在构建时预渲染所有动态路由页面,实现静态生成(SSG),大幅提升访问性能。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/app/posts/[slug]/page.tsx
// 构建时预渲染所有文章页面
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json());
  // 返回所有预渲染的参数组合
  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  // 页面逻辑不变
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(res => res.json());
  return <article>{post.title}</article>;
}

2.3 路由组(Route Groups)

路由组通过圆括号包裹文件夹名实现,格式为 (folderName),核心特性是:文件夹名不会被计入 URL 路由路径,仅用于代码逻辑组织,不改变路由的最终访问地址

2.3.1 核心用途

  1. 按业务逻辑分组代码 将不同业务模块的路由、组件、工具函数按路由组隔离,避免 app 目录下文件夹过多导致的结构混乱。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    src/app/
    ├── (marketing)/           # 营销相关路由组,不影响URL
    │   ├── about/page.tsx     # 对应 /about 路由
    │   ├── contact/page.tsx   # 对应 /contact 路由
    │   └── layout.tsx         # 仅营销页面共享的布局
    ├── (dashboard)/           # 后台管理路由组
    │   ├── dashboard/page.tsx # 对应 /dashboard 路由
    │   ├── settings/page.tsx  # 对应 /settings 路由
    │   └── layout.tsx         # 仅后台页面共享的布局
    └── layout.tsx             # 全局根布局
    
  2. 同层级路由使用不同布局 无需嵌套路由,即可为同一层级的不同路由设置独立的布局,避免布局嵌套冗余。
  3. 创建多个根布局 删除全局根布局 app/layout.tsx 后,可在每个路由组内创建独立的根布局(必须包含 <html><body> 标签),实现完全隔离的页面体系,典型场景为前台商城与后台管理系统分离。

2.3.2 注意事项

  • 不同路由组不能解析为相同的 URL 路径,否则会导致构建冲突报错;
  • 多根布局场景下,根路由 / 对应的 page.tsx 必须定义在某一个路由组内;
  • 跨不同根布局的路由导航会触发页面完全重新加载,无法实现客户端无刷新导航;
  • 多根布局场景下,全局 not-found.tsx 存在兼容问题,需在各路由组内单独定义。

2.4 路由特殊文件约定(核心)

App Router 通过一系列约定式的特殊文件,定义路由的全生命周期行为,所有特殊文件均默认导出 React 组件,支持嵌套使用,子路由的特殊文件会继承父路由的配置,同时可覆盖自定义。

文件名核心功能关键特性与使用规则
layout.tsx定义路由段的共享 UI 布局,包裹同层级及子层级的所有页面1. 必须接收 children 属性,作为子内容的插槽;
2. 导航切换时组件实例不会销毁,状态完全保留,不重新渲染
3. 根布局 app/layout.tsx 为必选文件,必须包含 <html><body> 标签;
4. 支持无限嵌套,子布局会继承父布局的所有内容。
template.tsx类似 layout,定义路由段的公共包裹结构1. 必须接收 children 属性;
2. 路由切换时会销毁并重建组件实例,状态完全重置
3. 同一层级下,layout 包裹 template,template 再包裹 page;
4. 适用于页面访问统计、入场动画、需重新执行的副作用等场景。
page.tsx定义路由的页面 UI 内容,唯一会生成可访问路由的文件1. 每个路由段最多一个 page.tsx;
2. 支持 async 异步函数,直接在服务端获取数据;
3. 会被同层级的 layout、template、loading、error 等文件包裹。
loading.tsx定义路由加载时的兜底 UI,基于 React Suspense 实现1. 自动包裹同层级及子层级的 page.tsx,页面加载时自动渲染;
2. 触发条件:page 为 async 异步函数、组件内使用 React use 函数加载异步数据;
3. 支持嵌套定义,子路由可单独配置专属加载界面,覆盖父级配置。
error.tsx定义路由渲染错误时的兜底 UI,基于 React Error Boundary 实现1. 必须是客户端组件,顶部添加 "use client" 声明
2. 接收 error(错误信息)和 reset(错误重试函数)两个属性;
3. 仅捕获同层级及子层级组件的渲染错误,无法捕获同级 layout/template 的错误;
4. 根布局的错误需通过 global-error.tsx 捕获。
not-found.tsx定义 404 页面,处理路由不存在或资源未找到的场景1. 根目录的 not-found.tsx 可由「路由地址不匹配」或「手动调用 notFound() 函数」触发;
2. 子路由的 not-found.tsx 仅能通过手动调用 notFound() 函数触发;
3. 适用于文章不存在、用户不存在等数据兜底场景。
route.ts定义路由处理程序,即 API 接口,处理 HTTP 请求1. 与同层级的 page.tsx 互斥,不能共存;
2. 通过导出与 HTTP 方法同名的 async 函数处理请求;
3. 支持 Edge Runtime 和 Node.js Runtime,可部署在边缘节点。
default.tsx定义平行路由的兜底内容,硬导航时 URL 与插槽不匹配时渲染1. 仅用于平行路由的 @ 插槽目录下;
2. 作为插槽的默认内容,避免硬导航时触发 404 错误。
global-error.tsx全局错误捕获文件,处理根布局/根模板的渲染错误1. 触发时会完全替换根布局内容,因此必须自行定义 <html><body> 标签;
2. 仅生产环境生效,开发环境会直接显示错误堆栈。

核心文件代码示例

  1. 嵌套布局 layout.tsx
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    // src/app/dashboard/layout.tsx
    // 后台管理系统的嵌套布局,仅对 /dashboard 下的所有路由生效
    export default function DashboardLayout({ children }: { children: React.ReactNode }) {
      return (
     <div className="flex h-screen">
       <aside className="w-64 bg-gray-100 p-4">
         <nav>
           <ul>
             <li><a href="/dashboard">控制台</a></li>
             <li><a href="/dashboard/settings">设置</a></li>
           </ul>
         </nav>
       </aside>
       <main className="flex-1 p-6 overflow-auto">
         {children} {/* 子页面内容会被渲染在这里 */}
       </main>
     </div>
      );
    }
    
  2. 加载界面 loading.tsx
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // src/app/dashboard/loading.tsx
    // 访问 /dashboard 下的所有路由时,数据加载期间会渲染此组件
    export default function DashboardLoading() {
      return (
     <div className="flex items-center justify-center h-full">
       <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
       <p className="ml-3">正在加载后台数据...</p>
     </div>
      );
    }
    
  3. 错误边界 error.tsx
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
'use client'; // 必须声明为客户端组件
import { useEffect } from 'react';

// 捕获 /dashboard 下的所有渲染错误
export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // 错误上报到监控系统
    console.error('后台页面渲染错误:', error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center h-full text-center">
      <h2 className="text-xl font-bold text-red-600 mb-2">页面加载出错了</h2>
      <p className="text-gray-600 mb-4">{error.message}</p>
      <button
        onClick={reset}
        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      >
        重试加载
      </button>
    </div>
  );
}
  1. 404 页面 not-found.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Link from 'next/link';

// 全局404页面,路由不匹配时渲染
export default function NotFound() {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen text-center">
      <h2 className="text-4xl font-bold mb-4">404</h2>
      <p className="text-xl text-gray-600 mb-6">您访问的页面不存在</p>
      <Link href="/" className="px-4 py-2 bg-blue-500 text-white rounded">
        返回首页
      </Link>
    </div>
  );
}

2.5 链接与导航

Next.js 提供了4种客户端导航方式,均基于浏览器 History API 实现,相比原生页面跳转,仅更新页面必要组件,不会重新加载整个页面,大幅提升导航体验,同时支持预获取,提前加载目标路由的资源。

<Link> 是 Next.js 封装的导航组件,拓展了原生 HTML <a> 标签,支持路由预获取、客户端无刷新导航、滚动行为控制等特性,是最常用的导航方式。

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Link from 'next/link';

export default function Navbar() {
  return (
    <nav>
      {/* 基础导航 */}
      <Link href="/">首页</Link>
      {/* 动态路由导航 */}
      <Link href="/posts/hello-world">文章详情</Link>
      {/* 替换当前历史记录,无后退功能 */}
      <Link href="/login" replace>登录</Link>
      {/* 禁用滚动到顶部,保持当前滚动位置 */}
      <Link href="/about" scroll={false}>关于我们</Link>
      {/* 禁用预获取,默认视口内的 Link 会自动预获取 */}
      <Link href="/dashboard" prefetch={false}>后台</Link>
    </nav>
  );
}

导航激活态实现: 配合 usePathname() Hook 获取当前路由路径,实现导航高亮:

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
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';

export default function Navbar() {
  const pathname = usePathname();
  
  const isActive = (path: string) => pathname === path;
  
  return (
    <nav>
      <Link 
        href="/" 
        className={isActive('/') ? 'text-blue-600 font-bold' : 'text-gray-600'}
      >
        首页
      </Link>
      <Link 
        href="/about" 
        className={isActive('/about') ? 'text-blue-600 font-bold' : 'text-gray-600'}
      >
        关于我们
      </Link>
    </nav>
  );
}

2.5.2 useRouter() Hook(客户端组件专用)

useRouter() 用于在客户端组件中实现触发式导航,比如按钮点击、表单提交完成后的跳转,仅能在添加了 'use client' 声明的客户端组件中使用。

基础用法

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
'use client';
import { useRouter } from 'next/navigation';

export default function LoginButton() {
  const router = useRouter();
  
  const handleLogin = () => {
    // 登录逻辑...
    // 跳转到后台首页
    router.push('/dashboard');
    // 替换当前历史记录,无法后退
    // router.replace('/dashboard');
    // 后退一页
    // router.back();
    // 前进一页
    // router.forward();
    // 刷新当前页面
    // router.refresh();
  };
  
  return (
    <button onClick={handleLogin} className="px-4 py-2 bg-blue-500 text-white rounded">
      登录并进入后台
    </button>
  );
}

2.5.3 redirect 函数(服务端专用)

redirect 函数用于在服务端组件、路由处理程序、Server Actions 中实现重定向,适用于数据请求失败、鉴权不通过等场景的路由跳转。

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
import { redirect } from 'next/navigation';
import { checkUserAuth } from '@/lib/auth';

// 服务端组件中实现鉴权重定向
export default async function DashboardPage() {
  const isAuth = await checkUserAuth();
  // 未登录则重定向到登录页
  if (!isAuth) {
    redirect('/login');
  }
  
  return <div>后台管理首页</div>;
}

2.5.4 浏览器原生 History API

通过 window.history.pushStatewindow.history.replaceState 可以直接更新浏览器历史记录,实现无刷新路由更新,常配合 usePathname()useSearchParams() 使用,适用于列表排序、筛选条件更新等无需刷新页面的场景。

2.6 路由处理程序(Route Handlers)

路由处理程序是 Next.js 中用于自定义 API 接口的实现方案,替代传统前后端分离架构中的独立后端服务,基于 Web 标准的 RequestResponse API 开发,无需额外搭建 Node.js 服务即可实现全栈开发。

2.6.1 核心约定与规则

  • 文件约定:必须命名为 route.ts,放在 app 目录下,支持嵌套;
  • 路径规则:文件夹路径对应接口地址,如 app/api/posts/route.ts 对应 /api/posts 接口;
  • 互斥规则:与同层级的 page.tsx 互斥,不能共存(前者处理 API 请求,后者渲染 UI 页面);
  • 方法定义:通过导出与 HTTP 方法同名的 async 函数处理请求,支持 GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS
  • 响应处理:推荐使用 next/server 包的 NextRequestNextResponse,对 TypeScript 友好,封装了 Cookie、Headers 处理等便捷功能,也可使用原生 Web Request/Response

2.6.2 基础实现示例

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
// src/app/api/posts/route.ts
import { NextResponse } from 'next/server';

// GET 请求:查询文章列表
export async function GET() {
  try {
    // 服务端直接查询数据库或调用第三方接口
    const posts = await fetch('https://jsonplaceholder.typicode.com/posts').then(res => res.json());
    // 返回 JSON 响应,默认状态码 200
    return NextResponse.json({ code: 0, data: posts, message: 'success' });
  } catch (error) {
    // 错误响应,自定义状态码
    return NextResponse.json(
      { code: -1, message: '获取文章列表失败' },
      { status: 500 }
    );
  }
}

// POST 请求:新增文章
export async function POST(request: NextRequest) {
  // 解析 JSON 请求体
  const body = await request.json();
  // 业务逻辑:写入数据库
  const newPost = { id: Date.now(), ...body };
  // 返回 201 状态码(创建成功)
  return NextResponse.json({ code: 0, data: newPost }, { status: 201 });
}

2.6.3 入参与参数处理

请求处理函数可接收两个可选入参,用于获取请求信息和动态路由参数:

  1. requestNextRequest 对象,原生 Web Request 的扩展,用于获取查询参数、请求体、Cookie、请求头等信息;
  2. context:仅包含 params 属性,用于获取动态路由的路径参数。

完整参数处理示例

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
// src/app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

// 动态路由接口:处理 /api/posts/123 请求
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  // 1. 获取动态路由参数
  const { id } = await params;
  
  // 2. 获取查询参数:如 /api/posts/123?field=title&userId=1
  const searchParams = request.nextUrl.searchParams;
  const field = searchParams.get('field');
  const userId = searchParams.get('userId');
  
  // 3. 获取 Cookie
  const token = request.cookies.get('token')?.value;
  
  // 4. 获取请求头
  const userAgent = request.headers.get('user-agent');
  
  // 业务逻辑
  const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`).then(res => res.json());
  
  return NextResponse.json({
    data: field ? { [field]: post[field] } : post,
    meta: { id, userId, userAgent, hasToken: !!token }
  });
}

// PUT 请求:更新文章
export async function PUT(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  // 解析 JSON 请求体
  const updateData = await request.json();
  // 解析 FormData 格式请求体:const formData = await request.formData();
  return NextResponse.json({ id, data: updateData });
}

2.6.4 缓存机制(GET 请求专属)

Next.js 对路由处理程序的 GET 请求有完善的缓存策略,生产环境下默认开启静态缓存,大幅提升接口响应速度。

  1. 默认缓存行为
    • 开发模式(npm run dev):无缓存,每次请求实时处理;
    • 生产模式(npm run build && start):默认静态缓存,构建时预渲染接口结果,后续请求直接返回缓存内容。
  2. 退出缓存(转为动态渲染) 满足以下任一条件,GET 请求将取消缓存,每次请求实时处理:
    • 函数中使用了 request 参数(哪怕未实际使用);
    • 同一 route.ts 中导出了非 GET 方法(如 POST/PUT);
    • 调用了 cookies()/headers() 等动态函数;
    • 手动声明路由段配置 export const dynamic = 'force-dynamic'
  3. 缓存重新验证(定时更新) 无需退出缓存,仅设置缓存时效,到期后触发后台更新,平衡性能与数据实时性,两种实现方式:
    • 路由段全局配置:export const revalidate = 60;(60秒后重新验证);
    • 单个 fetch 请求配置:fetch(url, { next: { revalidate: 30 } })(仅对该请求生效)。

2.6.5 高频操作指南

操作场景实现方法
设置 Cookie通过响应头 Set-Cookie 配置:
return new Response('ok', { headers: { 'Set-Cookie': 'token=123; Path=/; HttpOnly' } })
设置响应头返回新的 Response 实例,自定义 headers:
return NextResponse.json(data, { headers: { 'X-Custom-Header': 'value' } })
跨域 CORS 配置在响应头中添加跨域相关配置,指定允许的源、方法、请求头
重定向使用 redirect 函数:redirect('https://example.com')
流式响应通过 ReadableStream 实现流式传输,适用于打字机效果、大文件传输
直接返回非 HTML 内容可直接返回 XML、文本、图片等格式的 Response,内置支持 sitemap.xml/robots.txt 生成

2.6.6 版本差异与避坑指南

  • Next.js 15+ 中,params/cookies()/headers() 均变为异步 API,必须通过 await 获取后再使用;
  • Next.js 15+ 中,路由处理程序默认无缓存行为,如需缓存需手动配置 revalidate
  • 生产环境与开发环境的缓存行为不一致,缓存相关逻辑必须在生产构建后测试;
  • 路由处理程序默认运行在 Edge Runtime,禁止使用 Node.js 专属 API,如需使用需手动配置 runtime: 'nodejs'

2.7 中间件(Middleware)

中间件是 Next.js 中用于拦截并控制应用所有请求与响应的核心能力,运行在服务端,在请求到达路由页面或接口之前执行,支持重写、重定向、修改请求/响应头、鉴权、限流、A/B 测试等场景,是全栈应用的流量入口控制中心。

2.7.1 核心约定与规则

  • 文件约定:必须命名为 middleware.ts,放在项目根目录(与 app 同级),若使用 src 目录则放在 src 下;
  • 执行时机:在所有请求到达服务器时最先执行,早于路由匹配、页面渲染、接口处理;
  • 运行时:默认仅支持 Edge Runtime,Next.js 15.5+ 正式支持 Node.js Runtime;
  • 导出规则:必须默认导出一个 middleware 函数,可同步或异步执行;支持导出 config 对象配置匹配路径。

2.7.2 基础实现示例

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
// src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// 中间件核心函数,每个匹配的请求都会执行
export function middleware(request: NextRequest) {
  // 1. 获取请求信息
  const { pathname } = request.nextUrl;
  const token = request.cookies.get('token')?.value;
  const isAuthPage = pathname.startsWith('/login');
  const isProtectedRoute = pathname.startsWith('/dashboard');

  // 2. 鉴权逻辑:未登录访问后台路由,重定向到登录页
  if (isProtectedRoute && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // 3. 已登录用户访问登录页,重定向到后台
  if (isAuthPage && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  // 4. 修改请求头,传递给下游路由/接口
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-request-path', pathname);

  // 5. 继续执行后续逻辑,传递修改后的请求头
  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });

  // 6. 修改响应头
  response.headers.set('x-middleware-executed', 'true');

  return response;
}

// 配置中间件的匹配路径,仅匹配指定路由
export const config = {
  matcher: [
    '/dashboard/:path*', // 匹配 /dashboard 下所有路由
    '/login', // 匹配登录页
    '/api/:path*', // 匹配所有 API 接口
  ],
};

2.7.3 请求路径匹配

中间件提供两种路径匹配方式,推荐使用 matcher 配置项,性能更优。

  1. matcher 配置项 基于 path-to-regexp 库解析,支持字符串、数组、正则表达式,可精准匹配复杂场景:
1
2
3
4
5
6
7
8
9
10
11
export const config = {
  // 匹配所有路径,排除静态资源、API、图片等
  matcher: [
    /*
     * 匹配所有请求路径,除了:
     * 1. 以 /api, /_next/static, /_next/image, /favicon.ico 开头的路径
     * 2. 静态资源文件(后缀为 .svg, .png, .jpg 等)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)',
  ],
};

高级匹配支持通过 has/missing 判断请求头、查询参数、Cookie,实现精准匹配:

1
2
3
4
5
6
7
8
9
export const config = {
  matcher: [
    {
      source: '/about',
      // 仅当请求头包含 x-preview 时匹配
      has: [{ type: 'header', key: 'x-preview' }],
    },
  ],
};
  1. 条件语句匹配middleware 函数内通过 request.nextUrl.pathname 做路径判断,适用于正则难以实现的复杂逻辑,灵活性更高。

2.7.4 核心操作能力

中间件支持以下核心操作,覆盖全场景流量控制需求:

  • 重定向NextResponse.redirect(),将请求重定向到指定 URL;
  • 重写NextResponse.rewrite(),透明重写请求路径,用户无感知,常用于反向代理、A/B 测试;
  • 修改请求头:通过 NextResponse.next({ request: { headers } }) 传递修改后的请求头给下游;
  • 修改响应头:直接修改 response.headers,设置自定义响应头、CORS 配置等;
  • 直接返回响应:可直接返回 NextResponse 实例,终止请求,适用于限流、IP 封禁等场景。

2.7.5 执行优先级

Next.js 明确了中间件与其他配置的执行顺序,从先到后为:

  1. next.config.js 中的 headers 配置
  2. next.config.js 中的 redirects 配置
  3. 中间件(Middleware)
  4. next.config.js 中的 rewrites(beforeFiles) 配置
  5. 文件系统路由匹配(页面/接口)
  6. next.config.js 中的 rewrites(afterFiles) 配置
  7. 动态路由匹配
  8. next.config.js 中的 rewrites(fallback) 配置

2.7.6 代码维护技巧

对于复杂项目,中间件可能包含鉴权、限流、日志、国际化、A/B 测试等多个逻辑,推荐通过高阶函数+洋葱模型实现模块化拆分,便于维护和扩展:

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
// src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest, NextFetchEvent } from 'next/server';

// 定义中间件类型
type Middleware = (
  request: NextRequest,
  event: NextFetchEvent,
  next: () => Promise<NextResponse>
) => Promise<NextResponse>;

// 洋葱模型链式调用工具函数
function chain(middlewares: Middleware[], index = 0): Middleware {
  return async (req, event, next) => {
    if (index >= middlewares.length) return next();
    const current = middlewares[index];
    return current(req, event, chain(middlewares, index + 1));
  };
}

// 1. 日志中间件
const loggerMiddleware: Middleware = async (req, _, next) => {
  const start = Date.now();
  const response = await next();
  console.log(`[${req.method}] ${req.nextUrl.pathname} - ${Date.now() - start}ms`);
  return response;
};

// 2. 鉴权中间件
const authMiddleware: Middleware = async (req, _, next) => {
  const token = req.cookies.get('token')?.value;
  const isProtected = req.nextUrl.pathname.startsWith('/dashboard');
  
  if (isProtected && !token) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
  return next();
};

// 3. CORS 中间件
const corsMiddleware: Middleware = async (req, _, next) => {
  const response = await next();
  response.headers.set('Access-Control-Allow-Origin', '*');
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  return response;
};

// 主中间件函数
export default async function middleware(request: NextRequest, event: NextFetchEvent) {
  const middlewareChain = chain([loggerMiddleware, authMiddleware, corsMiddleware]);
  return middlewareChain(request, event, async () => NextResponse.next());
}

export const config = {
  matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
};

2.8 高级路由特性

2.8.1 平行路由(Parallel Routes)

平行路由允许在同一布局中同时渲染多个独立的页面,每个页面拥有独立的路由、加载状态、错误处理,通过@开头命名的文件夹创建「插槽」,类似 Vue 的插槽功能,插槽会作为 props 传递给父布局。

核心适用场景

  • 复杂后台看板,同时渲染多个独立模块;
  • 条件渲染,如根据登录状态切换展示登录页/后台页面;
  • 模态框弹窗,结合拦截路由实现软导航弹窗、硬导航独立页面。

基础实现示例

1
2
3
4
5
6
7
src/app/dashboard/
├── @analytics/          # 数据统计插槽
│   └── page.tsx         # 插槽页面内容
├── @notifications/      # 通知插槽
│   └── page.tsx         # 插槽页面内容
├── layout.tsx           # 父布局,接收插槽props
└── page.tsx             # 主页面内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/app/dashboard/layout.tsx
// 父布局接收平行路由插槽,插槽名与 @ 文件夹名一致
export default function DashboardLayout({
  children, // 对应 page.tsx 的内容
  analytics, // 对应 @analytics/page.tsx 的内容
  notifications, // 对应 @notifications/page.tsx 的内容
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  notifications: React.ReactNode;
}) {
  return (
    <div className="container mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">后台控制台</h1>
      <div className="grid grid-cols-2 gap-6 mb-6">
        {/* 渲染两个平行插槽 */}
        <div className="border rounded-lg p-4">{analytics}</div>
        <div className="border rounded-lg p-4">{notifications}</div>
      </div>
      {/* 渲染主页面内容 */}
      <main>{children}</main>
    </div>
  );
}

核心注意事项

  • 每个插槽可独立定义 loading.tsx/error.tsx,单个插槽的加载/错误不会影响其他部分;
  • 软导航(<Link> 点击)时,URL 与插槽不匹配会保留原有状态,不会渲染 404;
  • 硬导航(浏览器刷新)时,URL 与插槽不匹配会触发 404,必须为每个插槽创建 default.tsx 作为兜底内容。

2.8.2 拦截路由(Intercepting Routes)

拦截路由允许在当前路由中拦截其他路由地址,在当前页面内展示目标路由的内容,实现同一URL在不同访问方式下展示不同内容:软导航(客户端点击)时在当前页面展示弹窗/抽屉,硬导航(浏览器刷新/直接访问)时展示完整独立页面,兼顾用户体验与链接分享需求。

典型适用场景:图片预览、商品详情弹窗、任务编辑弹窗、登录模态框。

层级匹配规则:通过特殊前缀修饰文件夹名,匹配路由层级(路由组、平行路由不计算层级):

前缀匹配规则
(.)匹配同一层级的路由
(..)匹配上一层级的路由
(..)(..)匹配上上层级的路由
(...)匹配根目录的路由

结合平行路由实现弹窗示例

1
2
3
4
5
6
7
8
9
10
11
src/app/
├── photos/
│   ├── [id]/
│   │   └── page.tsx       # 图片详情独立页面,硬导航时渲染
│   └── page.tsx           # 图片列表页
├── @modal/                # 平行路由插槽,用于渲染弹窗
│   ├── (.)photos/[id]/
│   │   └── page.tsx       # 拦截路由,匹配同层级 /photos/[id] 路由
│   └── default.tsx        # 兜底内容,返回 null
├── layout.tsx             # 根布局,接收 modal 插槽
└── page.tsx
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
// src/app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="zh-CN">
      <body>
        {children}
        {/* 渲染弹窗插槽 */}
        {modal}
      </body>
    </html>
  );
}

// src/app/@modal/default.tsx
export default function ModalDefault() {
  return null; // 兜底内容,无弹窗时不渲染任何内容
}

// src/app/photos/page.tsx
import Link from 'next/link';

export default function PhotosPage() {
  const photos = [
    { id: '1', title: '风景照', url: 'https://picsum.photos/800/600?random=1' },
    { id: '2', title: '人像照', url: 'https://picsum.photos/800/600?random=2' },
  ];

  return (
    <div className="grid grid-cols-3 gap-4 p-6">
      {photos.map(photo => (
        <Link key={photo.id} href={`/photos/${photo.id}`} className="block">
          <img src={photo.url} alt={photo.title} className="w-full h-auto rounded-lg" />
          <p className="mt-2 text-center">{photo.title}</p>
        </Link>
      ))}
    </div>
  );
}

// src/app/@modal/(.)photos/[id]/page.tsx
'use client';
import { useRouter } from 'next/navigation';

// 拦截路由组件,软导航访问 /photos/[id] 时渲染弹窗
export default function PhotoModal({ params }: { params: Promise<{ id: string }> }) {
  const { id } = params;
  const router = useRouter();

  // 关闭弹窗,返回上一页
  const handleClose = () => {
    router.back();
  };

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={handleClose}>
      <div className="bg-white rounded-lg p-4 max-w-3xl w-full" onClick={e => e.stopPropagation()}>
        <button onClick={handleClose} className="mb-4 text-gray-500 hover:text-black">关闭</button>
        <img src={`https://picsum.photos/800/600?random=${id}`} alt="图片详情" className="w-full h-auto rounded-lg" />
      </div>
    </div>
  );
}

// src/app/photos/[id]/page.tsx
// 独立页面,硬导航访问 /photos/[id] 时渲染
export default async function PhotoDetailPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return (
    <div className="container mx-auto p-6">
      <h1 className="text-2xl font-bold mb-4">图片详情 {id}</h1>
      <img src={`https://picsum.photos/1200/800?random=${id}`} alt="图片详情" className="w-full h-auto rounded-lg" />
    </div>
  );
}

三、Next.js 渲染体系(核心能力)

Next.js 的核心优势之一是提供了完善的渲染方案,从传统的 CSR/SSR/SSG/ISR,到基于 React Server Component(RSC)的服务端/客户端双组件模型,再到流式渲染、部分预渲染等高级特性,覆盖了从静态站点到动态全栈应用的全场景渲染需求,兼顾性能、SEO 与用户体验。

3.1 传统渲染模式基础

3.1.1 CSR(客户端渲染)

客户端渲染是传统 React 单页应用(SPA)的默认渲染模式,渲染工作完全在浏览器端执行:

  1. 浏览器先下载一个几乎空白的 HTML 文件和完整的 JS bundle;
  2. JS 加载完成后,在客户端执行 React 代码,发起数据请求;
  3. 数据返回后,React 渲染生成完整的 DOM 树,更新页面。

核心优缺点

  • 优点:开发体验好,前后端分离彻底,页面切换无刷新,交互体验好;
  • 缺点:首屏加载速度慢,白屏时间长,SEO 不友好(搜索引擎难以抓取 JS 渲染的内容),bundle 体积大,低端设备体验差。

Next.js 实现方式

  • 使用 React useEffect 钩子在客户端发起数据请求;
  • 使用 SWR、TanStack Query 等客户端数据获取库。

3.1.2 SSR(服务端渲染)

服务端渲染将渲染工作放在服务端执行,解决了 CSR 的首屏和 SEO 问题:

  1. 浏览器发起请求,服务端接收请求后,获取页面所需的所有数据;
  2. 服务端将 React 组件渲染为完整的 HTML 字符串,返回给浏览器;
  3. 浏览器直接渲染 HTML,快速展示非交互页面,同时下载 JS bundle;
  4. JS 加载完成后,执行水合(Hydration)过程,为 HTML 绑定事件,赋予页面交互能力。

核心优缺点

  • 优点:首屏加载速度快,SEO 友好,内容可被搜索引擎直接抓取,首屏内容绘制时间短;
  • 缺点:服务端计算压力大,首字节时间(TTFB)长,需等待所有数据获取完成才能返回 HTML,全量水合完成后页面才能交互,串行执行效率低。

Next.js Pages Router 实现方式: 页面组件中导出 getServerSideProps 异步函数,该函数每次请求都会被调用,获取的数据通过 props 传递给页面组件。

3.1.3 SSG(静态站点生成)

静态站点生成是 Next.js 推荐的默认渲染模式,渲染工作在构建阶段执行:

  1. 项目构建时,Next.js 提前获取所有静态页面所需的数据;
  2. 将每个页面预渲染为完整的静态 HTML 文件,保存在构建产物中;
  3. 用户访问时,直接返回预先生成的 HTML 文件,可搭配 CDN 实现全球加速。

核心优缺点

  • 优点:性能极致,访问速度最快,服务端无计算压力,稳定性高,SEO 友好;
  • 缺点:内容实时性差,内容更新需要重新构建项目,不适合频繁更新的动态内容。

Next.js Pages Router 实现方式

  • 无数据请求的页面,默认会被预渲染为静态 HTML;
  • 需数据的页面,导出 getStaticProps 函数,构建时执行获取数据;
  • 动态路由页面,导出 getStaticPaths 函数定义预渲染的路径列表。

3.1.4 ISR(增量静态再生)

增量静态再生是 SSG 的升级方案,解决了静态内容实时性差的问题,兼顾静态性能与动态内容实时性:

  1. 构建时预渲染指定的静态页面;
  2. 用户访问时,先返回预渲染的静态页面;
  3. 若访问时间超过设置的重新验证间隔,Next.js 会在后台重新渲染页面,更新静态缓存;
  4. 下一次用户访问时,返回更新后的静态页面。

核心优缺点

  • 优点:无需全量重新构建即可更新内容,兼顾静态性能与实时性,支持百万级页面的增量生成;
  • 缺点:首次访问超时时长的用户,仍会看到旧内容,后台更新后才会展示新内容。

Next.js Pages Router 实现方式: 在 getStaticProps 中添加 revalidate 配置项,设置重新验证的时间间隔(单位:秒)。

3.2 React Server Component(RSC)核心原理

React Server Component 是 React 官方在 2020 年推出的全新组件模型,与 Next.js 团队联合开发,在 Next.js 13 中正式落地,是 App Router 体系的底层核心。

3.2.1 诞生背景

传统 React 渲染模型存在三个无法兼顾的核心矛盾:好的用户体验、易于维护的代码结构、极致的性能,具体表现为:

  1. 客户端组件分组件请求数据,会产生多次 HTTP 往返,串行请求时性能极差;
  2. 所有组件代码都需要打包到客户端,哪怕是仅在服务端执行的逻辑,导致 bundle 体积过大,加载和水合成本高;
  3. 客户端组件无法直接访问后端数据库、私有 API,必须通过中间层接口转发,增加开发成本;
  4. 敏感逻辑放在客户端存在安全风险,容易被反编译和篡改。

RSC 的出现,彻底解决了这些矛盾,将组件分为服务端组件和客户端组件,各司其职,实现了性能、安全性、开发体验的平衡。

3.2.2 核心运行原理

  1. 组件拆分:React 将组件树分为服务端组件和客户端组件,服务端组件在服务端执行,客户端组件在浏览器端执行;
  2. 服务端渲染:服务端组件在服务端完成数据请求和渲染,可直接访问数据库、私有 API,无需通过接口转发;
  3. RSC Payload 生成:服务端将渲染结果序列化为特殊的 JSON 格式(RSC Payload),包含三部分内容:
    • 服务端组件的渲染结果(DOM 结构和数据);
    • 客户端组件的占位符和引用地址;
    • 组件之间传递的 props 数据;
  4. 客户端重构:浏览器接收到 RSC Payload 后,React 重构完整的组件树,加载客户端组件的 JS 代码,填充占位符;
  5. 水合与交互:仅对客户端组件执行水合过程,绑定事件,赋予页面交互能力。

3.2.3 RSC 与 SSR 的核心区别

很多开发者会混淆 RSC 与 SSR,二者虽均在服务端执行代码,但本质完全不同,是互补而非替代关系,核心差异如下:

对比维度React Server Component(RSC)传统 SSR
核心侧重点聚焦组件级的代码拆分、数据获取与渲染,是组件模型的革新聚焦页面级的 HTML 渲染,是页面加载方式的优化
数据获取每个服务端组件可独立获取数据,并行执行,无瀑布流请求必须在页面顶层统一获取所有数据,数据获取完成后才能开始渲染
打包规则服务端组件代码完全不打包进客户端 bundle,大幅减少 JS 体积所有组件代码均需打包进客户端,哪怕是仅用于渲染的静态内容
渲染产物特殊格式的 RSC Payload,包含组件结构、数据、客户端组件引用完整的 HTML 字符串,可直接被浏览器渲染
水合逻辑仅客户端组件需要水合,水合成本极低,页面可交互时间大幅缩短所有组件都需要全量水合,水合成本高,必须全量水合完成后才能交互
更新机制支持局部更新,仅重新渲染变化的服务端组件,不丢失客户端状态每次更新都需要全量重新渲染页面,状态完全重置
适用场景全场景适用,尤其是动态全栈应用、数据密集型应用仅适用于页面首屏加载优化,对后续交互无帮助

3.3 服务端组件 vs 客户端组件

Next.js App Router 中,组件分为服务端组件(Server Components)客户端组件(Client Components) 两种类型,默认所有组件都是服务端组件,无需额外声明。

3.3.1 核心定义与特性

特性服务端组件(默认)客户端组件
声明方式无需声明,默认即为服务端组件必须在文件顶部添加 'use client' 声明
运行环境构建时 + 服务端,仅在服务端执行渲染构建时 + 服务端(生成初始HTML) + 客户端(水合与交互)
数据获取原生支持 async/await,直接在组件内异步获取数据需通过 useEffect、SWR 等方式在客户端获取数据
状态与生命周期不支持 useState、useEffect、useReducer 等状态/生命周期 API完全支持所有 React 状态、生命周期、副作用 API
浏览器 API不支持 window、document、localStorage 等浏览器专属 API完全支持所有浏览器 API
交互事件不支持 onClick、onChange 等用户交互事件完全支持所有用户交互事件
打包规则代码完全不打包进客户端 bundle,不参与水合代码打包进客户端 bundle,需要执行水合
核心优势数据获取更快、敏感逻辑更安全、减少客户端 JS 体积、支持流式渲染实现页面交互、响应用户操作、管理客户端状态
适用场景数据获取、静态内容渲染、敏感逻辑执行、访问后端资源用户交互、状态管理、依赖浏览器 API 的功能

3.3.2 组合使用规则

Next.js 对两种组件的组合使用有严格的规则,核心是单向导入规则

  1. 服务端组件可直接导入客户端组件,可在服务端组件中嵌套使用客户端组件;
  2. 客户端组件不能直接导入服务端组件,否则会报错,因为客户端环境无法执行服务端组件的代码;
  3. 解决方案:若需在客户端组件中使用服务端组件,可将服务端组件以 children 或其他 props 的形式传递给客户端组件,实现二者解耦,服务端组件仍在服务端执行,客户端组件仅接收渲染后的结果。

正确用法示例

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
// src/app/page.tsx(服务端组件)
import { ClientCard } from '@/components/ClientCard';
import { ServerContent } from '@/components/ServerContent';

// 服务端组件直接导入客户端组件,同时将服务端组件以children传递给客户端组件
export default function HomePage() {
  return (
    <div className="container mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">首页</h1>
      {/* 直接使用客户端组件 */}
      <ClientCard title="客户端卡片">
        {/* 将服务端组件作为children传递给客户端组件,完全合法 */}
        <ServerContent />
      </ClientCard>
    </div>
  );
}

// src/components/ClientCard.tsx(客户端组件)
'use client';
import { useState } from 'react';

// 客户端组件接收children props,不关心children是服务端还是客户端组件
export function ClientCard({ title, children }: { title: string; children: React.ReactNode }) {
  const [isExpanded, setIsExpanded] = useState(true);

  return (
    <div className="border rounded-lg p-4">
      <div className="flex items-center justify-between mb-4">
        <h3 className="text-lg font-bold">{title}</h3>
        <button onClick={() => setIsExpanded(!isExpanded)}>
          {isExpanded ? '收起' : '展开'}
        </button>
      </div>
      {isExpanded && <div>{children}</div>}
    </div>
  );
}

// src/components/ServerContent.tsx(服务端组件)
// 直接在服务端获取数据,无需客户端请求
export async function ServerContent() {
  const data = await fetch('https://jsonplaceholder.typicode.com/posts/1').then(res => res.json());
  return (
    <div>
      <h4 className="font-bold mb-2">{data.title}</h4>
      <p className="text-gray-600">{data.body}</p>
    </div>
  );
}

3.3.3 开发最佳实践

  1. 服务端组件优先原则:尽可能使用服务端组件,仅在需要用户交互、状态管理、浏览器 API 时才使用客户端组件,最小化客户端 JS 体积。
  2. 客户端组件尽可能下移:将客户端组件放在组件树的最底层,仅让需要交互的部分为客户端组件,外层布局、静态内容保持为服务端组件。
  3. 服务端代码防泄露:使用 server-only 包,确保包含敏感逻辑的服务端模块不会被客户端组件引用,构建时会直接报错。
    1
    
    npm install server-only
    
    1
    2
    3
    4
    
    // src/lib/db.ts
    import 'server-only';
    // 包含数据库连接、API密钥等敏感信息的模块
    export const db = connectDatabase(process.env.DATABASE_URL!);
    
  4. 三方包适配:若第三方包使用了客户端特性但未添加 'use client' 声明,需自行封装一层客户端组件包裹该三方包,再在服务端组件中使用。
  5. React Context 使用:服务端组件不支持 React Context,需在客户端组件中创建 Context.Provider,再在根布局(服务端组件)中导入使用。
  6. props 序列化:服务端组件传递给客户端组件的 props 必须是可序列化的,不能传递函数、React 组件、类实例等不可序列化数据。

3.4 Suspense 与 Streaming 流式渲染

Streaming 流式渲染是 React 18 推出的核心特性,Next.js 基于 <Suspense> 组件原生支持流式渲染,彻底解决了传统 SSR 的串行阻塞问题。

3.4.1 传统 SSR 的核心弊端

传统 SSR 的执行流程是完全串行的,存在三个无法解决的阻塞问题:

  1. 数据获取阻塞渲染:必须等待页面所有数据获取完成,才能开始渲染 HTML;
  2. 渲染阻塞发送:必须等待整个页面渲染完成,才能将 HTML 发送给浏览器;
  3. 水合阻塞交互:必须等待所有组件的 JS 加载完成,才能开始水合,全量水合完成后页面才能交互。

任何一个环节的慢请求,都会导致整个页面的加载和交互延迟,用户体验极差。

3.4.2 Suspense 与 Streaming 的核心原理

Streaming 流式渲染基于 HTTP 的分块传输编码Transfer-Encoding: chunked)实现,将页面 HTML 拆分为多个小块,逐步从服务端发送到客户端,无需等待整个页面渲染完成。

<Suspense> 组件是实现流式渲染的核心,它允许开发者推迟指定组件的渲染,直至数据加载完成,同时为组件设置 fallback 兜底 UI,慢请求的组件不会阻塞整个页面的渲染。

完整执行流程

  1. 服务端接收到请求,先渲染页面中无需等待数据的部分,发送初始 HTML 给浏览器;
  2. 浏览器接收到初始 HTML,立即渲染页面的静态内容和 fallback 兜底 UI,用户可快速看到页面内容;
  3. 服务端异步加载慢组件的数据,数据加载完成后,渲染组件的 HTML,通过同一个 HTTP 连接分块发送给浏览器;
  4. 浏览器接收到新的 HTML 块,替换对应的 fallback UI,更新页面内容;
  5. 所有组件发送完成后,HTTP 连接关闭,同时客户端逐步加载组件的 JS 代码,优先水合用户交互的组件。

3.4.3 Next.js 中的两种实现方式

  1. 组件级 Suspense 实现 直接使用 React <Suspense> 组件包装需要延迟渲染的异步组件,实现细粒度的流式渲染:
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
// src/app/page.tsx
import { Suspense } from 'react';
import { PostFeed } from '@/components/PostFeed';
import { WeatherWidget } from '@/components/WeatherWidget';
import { RecommendSidebar } from '@/components/RecommendSidebar';
import { LoadingSkeleton } from '@/components/LoadingSkeleton';

export default function HomePage() {
  return (
    <div className="container mx-auto p-6 grid grid-cols-3 gap-6">
      <main className="col-span-2">
        <h1 className="text-2xl font-bold mb-6">最新文章</h1>
        {/* 包装异步组件,加载期间展示兜底骨架屏 */}
        <Suspense fallback={<LoadingSkeleton count={5} />}>
          <PostFeed />
        </Suspense>
      </main>
      <aside className="space-y-6">
        <Suspense fallback={<LoadingSkeleton count={1} />}>
          <WeatherWidget />
        </Suspense>
        <Suspense fallback={<LoadingSkeleton count={3} />}>
          <RecommendSidebar />
        </Suspense>
      </aside>
    </div>
  );
}

// src/components/PostFeed.tsx(异步服务端组件)
export async function PostFeed() {
  // 模拟慢请求,延迟2秒
  await new Promise(resolve => setTimeout(resolve, 2000));
  const posts = await fetch('https://jsonplaceholder.typicode.com/posts').then(res => res.json());
  
  return (
    <div className="space-y-4">
      {posts.slice(0, 5).map(post => (
        <article key={post.id} className="border rounded-lg p-4">
          <h3 className="font-bold">{post.title}</h3>
          <p className="text-gray-600 mt-2">{post.body}</p>
        </article>
      ))}
    </div>
  );
}
  1. 页面级 loading.tsx 实现 Next.js 约定式的 loading.tsx 文件,本质是自动用 <Suspense> 包裹同层级的 page.tsx,实现页面级的流式渲染,无需手动编写 Suspense 代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // src/app/dashboard/loading.tsx
    // 访问 /dashboard 下的所有路由,页面加载期间自动渲染此组件
    export default function DashboardLoading() {
      return (
     <div className="flex items-center justify-center h-screen">
       <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
     </div>
      );
    }
    

3.4.4 核心优势与局限性

核心优势

  • 大幅降低首字节时间(TTFB)和首次内容绘制(FCP),用户可更快看到页面内容;
  • 缩短页面可交互时间(TTI),优先渲染和水合用户关注的内容;
  • 慢请求不会阻塞整个页面,实现按需渲染,提升用户体验;
  • 完全不影响 SEO,搜索引擎可抓取到完整的流式渲染内容。

局限性: 仅优化了渲染和水合的执行方式,未解决客户端 JS 体积过大的问题,非交互组件仍需在客户端进行水合,而 RSC 彻底解决了这个问题——服务端组件无需打包进客户端,也无需水合。

3.5 Next.js 三种服务端渲染策略

Next.js 会根据开发者使用的功能和 API,自动为每个路由段选择最佳的渲染策略,无需手动配置,核心包含三种渲染策略:静态渲染、动态渲染、Streaming。

3.5.1 静态渲染(Static Rendering)

静态渲染是 Next.js 的默认渲染策略,路由在构建时重新验证后的后台渲染,渲染结果会被缓存并推送到 CDN,用户访问时直接返回缓存内容,性能极致。

核心特性

  • 适用场景:非用户个性化、数据可提前获取的内容,如静态博客、产品介绍页、营销页面;
  • 触发条件:路由中未使用任何动态函数、未使用未缓存的数据请求;
  • 缓存更新:通过路由段配置项 revalidate = 数字 设置缓存重新验证时效,超时时长后首次访问会触发后台更新缓存,后续访问返回新内容;
  • 即使配置了 revalidate,该路由仍会被标记为静态渲染。

3.5.2 动态渲染(Dynamic Rendering)

动态渲染的路由在每次用户请求时重新渲染,结果不缓存,适用于用户个性化内容、依赖请求信息的动态场景。

核心特性

  • 适用场景:用户后台、实时数据、个性化推荐、依赖 Cookie/请求头的内容;
  • 触发条件:路由中使用了动态函数未缓存的数据请求,二者满足其一即自动切换为动态渲染;
  • 动态函数:获取请求时专属信息的函数,包括 cookies()headers()、页面组件的 searchParams props;
  • 未缓存的数据请求:fetch 请求添加 cache: 'no-store'revalidate: 0,或在动态函数后使用的 fetch 请求。

关键提醒:动态渲染和数据缓存是两个独立的概念,动态渲染不代表数据请求一定不缓存,可在动态渲染的路由中使用缓存的 fetch 请求,平衡性能与实时性。

3.5.3 Streaming 流式渲染

无需手动配置,使用 loading.tsx 或 React <Suspense> 组件即可自动开启流式渲染,将页面拆分为多个块,逐步从服务端发送到客户端,优化慢请求场景的用户体验。

3.6 渲染体系最佳实践

  1. 静态优先,动态兜底:尽可能使用静态渲染,仅在必要时使用动态渲染,最大化页面性能;
  2. 最小化客户端组件:仅在需要交互的地方使用客户端组件,其余内容均使用服务端组件,减少客户端 JS 体积;
  3. 合理拆分 Suspense:将慢请求的组件单独用 Suspense 包裹,避免阻塞整个页面的渲染,优先展示用户关注的内容;
  4. 平衡实时性与性能:对于半动态内容,使用 revalidate 设置合理的缓存时效,而非直接使用无缓存的动态渲染;
  5. 避免水合不匹配:客户端组件中不要在渲染期间修改依赖浏览器 API 的内容,如 window.innerWidth,否则会导致水合错误,可通过 useEffectuseState 延迟到客户端执行。

四、Next.js 数据获取与缓存体系

数据获取与缓存是 Next.js 全栈能力的核心,也是其性能优势的关键所在。Next.js 基于 React 扩展了原生 fetch API,实现了请求去重、缓存、按需重新验证等能力,配合四大缓存层,构建了一套完整的、自动化的全栈数据获取与缓存体系,彻底解决了传统前后端分离架构中的数据瀑布流、重复请求、缓存管理复杂等问题。

4.1 数据获取方案的演进

4.1.1 Pages Router 数据获取

旧版 Pages Router 基于约定式函数实现数据获取,与组件渲染逻辑分离,灵活性差:

  • getServerSideProps:每次请求时在服务端执行,获取数据,实现 SSR;
  • getStaticProps:构建时在服务端执行,获取数据,实现 SSG;
  • getStaticPaths:配合动态路由,定义构建时预渲染的路径列表;
  • API 路由:处理数据提交、修改等写操作,需手动处理请求和响应。

4.1.2 App Router 数据获取

App Router 基于 RSC 重构了数据获取方案,是当前官方推荐的标准方式,核心特性:

  1. 服务端组件原生支持异步数据获取:直接在组件中使用 async/await 获取数据,无需约定式函数,代码更简洁;
  2. 原生 fetch 扩展:React 扩展了原生 fetch API,支持自动请求去重、缓存、重新验证,无需第三方数据获取库;
  3. 并行数据获取:多个异步组件的 fetch 请求并行执行,无瀑布流问题,大幅提升数据获取效率;
  4. Server Actions 处理数据提交:无需单独编写 API 路由,直接在服务端函数中处理数据提交、修改、删除等写操作,类型安全,开发效率更高;
  5. 四大缓存层协同:从请求级到路由级,再到客户端级,实现全链路缓存自动化,无需手动管理缓存。

4.2 App Router 基础数据获取

4.2.1 服务端组件数据获取(推荐首选)

Next.js 服务端组件原生支持 async/await,可直接在组件中异步获取数据,请求在服务端执行,不会暴露给客户端,安全性更高,性能更好。

基础示例

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
// src/app/posts/[slug]/page.tsx
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  
  // 直接在服务端组件中获取数据,无需 useEffect、无需 API 路由
  // Next.js 会自动缓存该 fetch 请求的结果
  const post = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 60 }, // 缓存60秒
  }).then(res => {
    if (!res.ok) throw new Error('文章不存在');
    return res.json();
  });

  // 同时获取作者信息,两个请求并行执行,无瀑布流
  const author = await fetch(`https://api.example.com/users/${post.authorId}`).then(res => res.json());

  return (
    <article className="container mx-auto py-10">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <div className="flex items-center mb-6">
        <img src={author.avatar} alt={author.name} className="w-10 h-10 rounded-full mr-3" />
        <span>{author.name}</span>
      </div>
      <div className="prose max-w-none">{post.content}</div>
    </article>
  );
}

核心优势

  • 数据请求在服务端执行,更接近数据源,延迟更低,速度更快;
  • 敏感的 API 密钥、数据库凭证不会暴露给客户端,安全性更高;
  • 多个 fetch 请求并行执行,无需等待前一个请求完成,减少总加载时间;
  • 数据获取与组件渲染逻辑放在一起,代码更内聚,易于维护;
  • 自动缓存请求结果,避免重复请求。

4.2.2 客户端组件数据获取

客户端组件无法直接使用 async/await 获取数据,需通过 React 副作用或第三方数据获取库实现,适用于需要客户端交互、实时更新的数据场景。

推荐方案:使用 SWR(Next.js 官方团队开发)或 TanStack Query,支持自动重新验证、缓存、乐观更新、轮询等高级特性。

SWR 示例

1
npm install swr
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
'use client';
import useSWR from 'swr';

// 数据获取函数
const fetcher = (url: string) => fetch(url).then(res => res.json());

export function ClientPostList() {
  // 客户端获取数据,支持自动重新验证、缓存、错误重试
  const { data, error, isLoading } = useSWR('https://jsonplaceholder.typicode.com/posts', fetcher, {
    revalidateOnFocus: true, // 窗口聚焦时重新验证
    refreshInterval: 30000, // 30秒轮询更新
  });

  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>加载失败</div>;

  return (
    <div className="space-y-4">
      {data.map((post: any) => (
        <article key={post.id} className="border rounded-lg p-4">
          <h3 className="font-bold">{post.title}</h3>
          <p className="text-gray-600 mt-2">{post.body}</p>
        </article>
      ))}
    </div>
  );
}

4.3 Next.js 四大缓存层

Next.js 的缓存体系分为四个层级,从请求级到客户端级,层层协同,自动化管理数据和渲染结果的缓存,是 Next.js 性能优化的核心。

缓存层级作用生命周期失效方式
请求记忆化(Request Memoization)去重同一次渲染中的重复 fetch 请求,避免重复执行相同请求单次请求渲染周期内,组件树渲染完成后失效渲染周期结束自动失效,手动调用 fetch 强制刷新
数据缓存(Data Cache)持久化缓存 fetch 请求的结果,跨请求、跨用户共享,是 Next.js 最核心的缓存层持久化存储,直至手动失效或重新验证1. 时间驱动重新验证(revalidate);2. 按需重新验证(revalidateTag/revalidatePath);3. 手动设置 cache: 'no-store'
全路由缓存(Full Route Cache)缓存路由的渲染结果(静态 HTML/RSC Payload),用户访问时直接返回缓存内容构建时生成,持久化存储,直至路由代码变更或缓存失效1. 数据缓存失效触发路由缓存重新渲染;2. 重新部署自动失效;3. 动态路由每次请求重新渲染
客户端路由缓存(Router Cache)客户端浏览器中缓存的 RSC Payload,优化导航体验,避免重复请求服务端用户会话期间,页面刷新后失效1. 页面刷新自动失效;2. 调用 router.refresh() 手动失效;3. Server Actions 执行后自动失效

4.3.1 请求记忆化(Request Memoization)

React 扩展的原生 fetch 特性,在同一次渲染周期内,多次调用相同 URL 和配置的 fetch 请求,只会执行一次,结果会被记忆化,传递给所有调用的组件。

核心作用:解决组件树中多个组件依赖相同数据的重复请求问题,比如文章详情页和作者信息组件都需要请求用户数据,只会执行一次 fetch 请求。

生命周期:仅在单次请求的渲染周期内有效,渲染完成后自动失效,下一次请求会重新执行。

4.3.2 数据缓存(Data Cache)

Next.js 最核心的缓存层,持久化缓存 fetch 请求的结果,跨请求、跨用户共享,默认所有 fetch 请求都会被缓存,除非手动配置退出缓存。

核心配置:通过 fetch 的 cachenext.revalidate 选项控制缓存行为:

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 默认强制缓存,永久缓存直至手动失效
fetch('https://api.example.com/posts', { cache: 'force-cache' });

// 2. 不缓存,每次请求都重新执行
fetch('https://api.example.com/posts', { cache: 'no-store' });

// 3. 缓存60秒,60秒内返回缓存,超时时长后后台重新验证
fetch('https://api.example.com/posts', { next: { revalidate: 60 } });

// 4. 给请求打标签,用于按需批量失效缓存
fetch('https://api.example.com/posts', { next: { tags: ['posts'] } });
fetch('https://api.example.com/users', { next: { tags: ['users'] } });

4.3.3 全路由缓存(Full Route Cache)

缓存路由的渲染结果,分为静态渲染和动态渲染两种模式:

  • 静态渲染的路由,构建时渲染结果会被缓存,永久有效,直至重新部署或数据缓存失效;
  • 动态渲染的路由,渲染结果不会被缓存,每次请求都会重新渲染。

核心特性:数据缓存的失效会自动触发全路由缓存的重新渲染,无需手动处理,数据与页面渲染自动同步。

4.3.4 客户端路由缓存(Router Cache)

客户端浏览器中缓存的 RSC Payload,当用户通过 <Link> 组件导航时,Next.js 会预获取目标路由的 RSC Payload 并缓存,导航时直接使用缓存内容,无需请求服务端,实现毫秒级页面切换。

生命周期:用户会话期间有效,页面刷新后自动失效,可通过 router.refresh() 手动清空缓存,重新请求服务端。

4.4 缓存重新验证与失效

Next.js 提供了两种缓存重新验证方式,平衡数据实时性与性能,同时支持手动精准失效缓存。

4.4.1 时间驱动重新验证

通过 revalidate 配置设置缓存的有效时长,到期后自动触发重新验证,更新缓存内容,分为两个级别:

  1. fetch 级别:仅对单个 fetch 请求生效
    1
    
    fetch('https://api.example.com/posts', { next: { revalidate: 60 } });
    
  2. 路由段级别:对整个路由段的所有 fetch 请求生效,在 page.tsx/layout.tsx 中导出配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // src/app/posts/page.tsx
    // 整个路由段的缓存有效期为60秒
    export const revalidate = 60;
       
    export default async function PostsPage() {
      // 两个请求都会继承路由段的 revalidate 配置
      const posts = await fetch('https://api.example.com/posts').then(res => res.json());
      const categories = await fetch('https://api.example.com/categories').then(res => res.json());
      return <div>{/* 页面内容 */}</div>;
    }
    

4.4.2 按需重新验证(On-demand Revalidation)

通过 revalidateTagrevalidatePath 函数,手动精准失效缓存,适用于数据提交后立即更新页面内容的场景,无需等待缓存超时。

  1. revalidateTag:失效所有带有指定标签的 fetch 请求,是最灵活的缓存失效方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    'use server';
    import { revalidateTag } from 'next/cache';
       
    export async function createPost(formData: FormData) {
      // 业务逻辑:创建新文章,写入数据库
      await db.post.create({ data: { title: formData.get('title') as string } });
         
      // 失效所有带有 posts 标签的 fetch 请求,更新文章列表
      revalidateTag('posts');
    }
    
  2. revalidatePath:失效指定路径的路由缓存,更新整个页面的内容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    'use server';
    import { revalidatePath } from 'next/cache';
       
    export async function updatePost(formData: FormData) {
      const id = formData.get('id') as string;
      // 业务逻辑:更新文章
      await db.post.update({ where: { id }, data: { title: formData.get('title') as string } });
         
      // 失效文章列表页的缓存
      revalidatePath('/posts');
      // 失效文章详情页的缓存
      revalidatePath(`/posts/${id}`);
      // 失效整个站点的所有缓存
      // revalidatePath('/', 'layout');
    }
    

4.5 服务端动作(Server Actions)

Server Actions 是 Next.js 提供的服务端函数执行方案,允许开发者直接在 React 组件中定义异步服务端函数,处理表单提交、数据修改、文件上传等写操作,无需单独编写 API 路由,是全栈开发的核心能力。

4.5.1 核心特性与启用方式

  • 类型安全:客户端与服务端共享类型定义,编译时校验,避免运行时错误;
  • 内置安全:自动防止 CSRF 攻击,支持加密签名、请求验证;
  • 渐进增强:支持无 JS 环境的表单提交,兼容性好;
  • 自动缓存失效:执行后自动触发相关缓存的重新验证,同步页面数据;
  • 乐观更新:配合 React useOptimistic 实现乐观更新,提升用户体验;
  • 错误处理:配合 useFormState 处理表单提交错误,返回友好提示。

启用方式:Next.js 14+ 默认启用,无需额外配置,直接使用即可。

4.5.2 基础用法

Server Actions 分为两种定义方式:

  1. 组件内定义:在服务端组件中定义,函数顶部添加 'use server' 声明;
  2. 单独文件定义:在单独的文件中定义,文件顶部添加 'use server' 声明,可在客户端组件和服务端组件中复用。

表单提交基础示例

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
// src/app/posts/page.tsx(服务端组件)
import { createPost } from '@/actions/post.actions';

export default function PostsPage() {
  return (
    <div className="container mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">发布文章</h1>
      {/* 直接将 Server Action 传递给 form 的 action 属性 */}
      <form action={createPost} className="space-y-4 max-w-xl">
        <div>
          <label className="block mb-1">文章标题</label>
          <input
            type="text"
            name="title"
            className="w-full border rounded-lg p-2"
            required
          />
        </div>
        <div>
          <label className="block mb-1">文章内容</label>
          <textarea
            name="content
            className="w-full border rounded-lg p-2 h-40"
            required
          />
        </div>
        <button
          type="submit"
          className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
        >
          发布文章
        </button>
      </form>
    </div>
  );
}
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
// src/actions/post.actions.ts
'use server'; // 整个文件均为服务端动作,必须在顶部声明
import { revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod'; // 推荐使用Zod做服务端数据校验
import { db } from '@/lib/db';

// 表单校验规则,服务端必须做二次校验,客户端校验不可信
const PostSchema = z.object({
  title: z.string().min(5, '标题至少5个字符').max(100, '标题最多100个字符'),
  content: z.string().min(20, '内容至少20个字符'),
});

// 定义服务端动作,必须为async函数
export async function createPost(formData: FormData) {
  try {
    // 1. 解析表单数据
    const rawData = {
      title: formData.get('title') as string,
      content: formData.get('content') as string,
    };

    // 2. 服务端数据校验
    const validatedData = PostSchema.safeParse(rawData);
    if (!validatedData.success) {
      return {
        error: validatedData.error.flatten().fieldErrors,
        success: false,
      };
    }

    // 3. 核心业务逻辑:写入数据库
    const newPost = await db.post.create({
      data: validatedData.data,
    });

    // 4. 失效缓存,触发文章列表页面自动更新
    revalidateTag('posts');

    // 5. 重定向到文章详情页
    redirect(`/posts/${newPost.id}`);
  } catch (error) {
    // 注意:redirect会抛出特殊错误,不能捕获,否则会导致重定向失效
    if (error instanceof Error && error.name !== 'NEXT_REDIRECT') {
      console.error('创建文章失败:', error);
      return {
        error: { _form: ['系统异常,创建文章失败,请稍后重试'] },
        success: false,
      };
    }
    throw error;
  }
}

4.5.3 Server Actions 高级用法

1. 客户端组件中使用

客户端组件无法直接定义服务端动作,但可导入单独文件定义的服务端动作,配合React Hooks实现更丰富的交互逻辑。

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
'use client';
import { useFormState } from 'react-dom';
import { createPost } from '@/actions/post.actions';

// 客户端组件中使用服务端动作
export function CreatePostForm() {
  // useFormState:处理表单提交状态、错误信息、返回数据
  // 第一个参数:服务端动作函数
  // 第二个参数:初始状态
  const [state, formAction] = useFormState(createPost, {
    success: false,
    error: {},
  });

  return (
    <form action={formAction} className="space-y-4 max-w-xl">
      <div>
        <label className="block mb-1">文章标题</label>
        <input
          type="text"
          name="title"
          className="w-full border rounded-lg p-2"
        />
        {/* 展示字段错误信息 */}
        {state.error?.title && (
          <p className="text-red-500 text-sm mt-1">{state.error.title[0]}</p>
        )}
      </div>
      <div>
        <label className="block mb-1">文章内容</label>
        <textarea
          name="content"
          className="w-full border rounded-lg p-2 h-40"
        />
        {state.error?.content && (
          <p className="text-red-500 text-sm mt-1">{state.error.content[0]}</p>
        )}
      </div>
      {/* 展示表单全局错误 */}
      {state.error?._form && (
        <p className="text-red-500 text-sm">{state.error._form[0]}</p>
      )}
      <SubmitButton />
    </form>
  );
}

// 拆分提交按钮组件,使用useFormStatus获取表单提交状态
function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button
      type="submit"
      disabled={pending}
      className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed"
    >
      {pending ? '发布中...' : '发布文章'}
    </button>
  );
}
2. 乐观更新

配合React useOptimistic Hook实现乐观更新,在服务端动作执行期间就提前更新UI,无需等待服务端响应,大幅提升用户体验。

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
'use client';
import { useOptimistic, useState } from 'react';
import { likePost } from '@/actions/post.actions';

type Post = { id: string; title: string; likeCount: number };

export function PostLikeButton({ post }: { post: Post }) {
  const [likeCount, setLikeCount] = useState(post.likeCount);
  // 乐观更新:pending状态下提前展示更新后的UI
  const [optimisticCount, addOptimisticLike] = useOptimistic(
    likeCount,
    (state, increment: number) => state + increment
  );

  async function handleLike() {
    // 立即更新UI,无需等待服务端响应
    addOptimisticLike(1);
    try {
      // 执行服务端动作
      const newCount = await likePost(post.id);
      setLikeCount(newCount);
    } catch (error) {
      // 服务端失败,回滚UI
      addOptimisticLike(-1);
      console.error('点赞失败', error);
    }
  }

  return (
    <button onClick={handleLike} className="flex items-center gap-2">
      👍 <span>{optimisticCount}</span>
    </button>
  );
}
3. 嵌套布局与路由中的使用

Server Actions 支持在嵌套布局、客户端组件、服务端组件中任意复用,不受路由层级限制,执行后会自动触发当前路由的缓存失效与重新渲染。

4.5.4 Server Actions 安全最佳实践

  1. 强制服务端校验:永远不要信任客户端传递的数据,所有入参必须在服务端做二次校验,推荐使用Zod、Yup等校验库;
  2. 鉴权与权限控制:服务端动作顶部必须先做用户鉴权和权限校验,防止未授权访问,禁止在客户端控制权限;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    'use server';
    import { auth } from '@/lib/auth';
       
    export async function deletePost(postId: string) {
      // 第一步:鉴权,未登录直接拒绝
      const session = await auth();
      if (!session?.user) {
        throw new Error('未登录,无操作权限');
      }
      // 第二步:权限校验,仅文章作者可删除
      const post = await db.post.findUnique({ where: { id: postId } });
      if (post?.authorId !== session.user.id) {
        throw new Error('无权限删除该文章');
      }
      // 第三步:执行业务逻辑
      await db.post.delete({ where: { id: postId } });
      revalidateTag('posts');
    }
    
  3. 速率限制:对敏感操作(登录、注册、数据修改)添加速率限制,防止暴力攻击和恶意请求;
  4. 禁止暴露敏感信息:服务端动作的错误信息必须脱敏,禁止将数据库错误、系统路径等敏感信息返回给客户端;
  5. 严格的类型定义:使用TypeScript严格定义入参和返回值类型,避免类型漏洞。

4.6 数据获取与缓存最佳实践

  1. 服务端组件优先:尽可能在服务端组件中获取数据,减少客户端请求,提升性能和安全性;
  2. 并行数据获取:将无依赖的异步请求拆分到独立组件中,实现并行执行,避免请求瀑布流;
  3. 合理设置缓存策略
    • 静态不变内容:使用默认永久缓存,提升访问速度;
    • 半动态内容:通过revalidate设置合理的缓存时效,平衡性能与实时性;
    • 强实时内容:使用cache: 'no-store'退出缓存,每次请求获取最新数据;
  4. 标签化缓存管理:给fetch请求添加标签,通过revalidateTag精准失效缓存,避免全量刷新;
  5. 错误边界兜底:给异步组件添加error.tsx错误边界,处理数据请求失败的场景,提升用户体验;
  6. 加载状态优化:通过loading.tsxSuspense拆分加载粒度,优先展示用户关注的内容,减少白屏时间;
  7. 客户端数据请求:需要实时更新、频繁交互的数据,使用SWR或TanStack Query,避免手动管理缓存和请求状态。

五、Next.js 样式方案

Next.js 原生支持多种样式方案,覆盖从原子化CSS、CSS Modules、预处理器到CSS-in-JS的全场景需求,官方优先推荐Tailwind CSS,同时对其他方案提供完善的兼容支持。

5.1 Tailwind CSS(官方推荐)

Tailwind CSS 是一个功能类优先的原子化CSS框架,与Next.js深度集成,是官方create-next-app初始化时的默认选项,开发效率极高,生产环境可通过PurgeCSS极致压缩CSS体积。

基础集成与使用

  1. 初始化时一键集成:
    1
    
    npx create-next-app@latest --tailwind
    
  2. 手动集成:
    1
    2
    
    npm install -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p
    
  3. 配置tailwind.config.ts,指定模板文件路径:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    import type { Config } from 'tailwindcss';
       
    const config: Config = {
      content: [
        './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
        './src/components/**/*.{js,ts,jsx,tsx,mdx}',
        './src/app/**/*.{js,ts,jsx,tsx,mdx}',
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    };
    export default config;
    
  4. 全局CSS文件中引入Tailwind指令:
    1
    2
    3
    4
    
    /* src/app/globals.css */
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
  5. 在根布局中引入全局CSS,即可在所有组件中使用Tailwind类名:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // src/app/layout.tsx
    import './globals.css';
       
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="zh-CN">
          <body>{children}</body>
        </html>
      );
    }
    

5.2 CSS Modules

CSS Modules 是Next.js原生支持的样式方案,通过.module.css后缀的文件实现样式隔离,避免全局样式污染,适合中大型项目的组件级样式管理。

基础使用

  1. 创建CSS Modules文件:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    /* src/components/Button.module.css */
    .primary {
      background-color: #0070f3;
      color: white;
      padding: 0.5rem 1rem;
      border-radius: 0.5rem;
      border: none;
      cursor: pointer;
    }
    .primary:hover {
      background-color: #0051aa;
    }
    
  2. 在组件中导入使用:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // src/components/Button.tsx
    import styles from './Button.module.css';
       
    type ButtonProps = {
      children: React.ReactNode;
    };
       
    export function Button({ children }: ButtonProps) {
      return <button className={styles.primary}>{children}</button>;
    }
    

核心特性

  • 自动生成唯一类名,完全隔离样式,不会出现全局污染;
  • 支持TypeScript类型提示,安装typescript-plugin-css-modules可实现类型校验;
  • 生产环境自动压缩,拆分CSS chunk,按需加载。

5.3 全局样式

Next.js 仅允许在根布局layout.tsx中引入全局CSS文件,其他组件中禁止直接引入全局CSS,避免样式冲突和加载冗余。

1
2
3
4
5
6
7
8
9
10
11
// src/app/layout.tsx
// 仅能在根布局中引入全局CSS
import './globals.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body>{children}</body>
    </html>
  );
}

5.4 Sass/SCSS 预处理器

Next.js 原生支持Sass/SCSS,无需额外配置webpack,只需安装sass依赖即可使用,支持.scss.sass两种格式,同时兼容CSS Modules。

1
npm install -D sass

使用方式与CSS一致,支持变量、嵌套、混合、函数等Sass所有特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/styles/variables.scss
$primary-color: #0070f3;

// src/components/Card.module.scss
@import '../styles/variables.scss';

.card {
  border: 1px solid #eaeaea;
  border-radius: 0.5rem;
  padding: 1rem;
  &:hover {
    border-color: $primary-color;
  }
  .title {
    color: $primary-color;
    font-size: 1.25rem;
    font-weight: bold;
  }
}

5.5 CSS-in-JS 方案

Next.js 支持styled-components、Emotion等主流CSS-in-JS方案,但由于服务端渲染的限制,需要额外配置,且官方不推荐优先使用——CSS-in-JS会在运行时动态生成样式,影响页面性能和水合效率。

styled-components 集成示例

  1. 安装依赖:
    1
    2
    
    npm install styled-components
    npm install -D @types/styled-components babel-plugin-styled-components
    
  2. 配置next.config.ts,开启styled-components支持:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    import type { NextConfig } from 'next';
       
    const nextConfig: NextConfig = {
      compiler: {
        styledComponents: true,
      },
    };
       
    export default nextConfig;
    
  3. 创建客户端组件的样式注册表,处理服务端渲染的样式收集:
    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
    
    // src/lib/StyledComponentsRegistry.tsx
    'use client';
    import React, { useState } from 'react';
    import { useServerInsertedHTML } from 'next/navigation';
    import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
       
    export default function StyledComponentsRegistry({
      children,
    }: {
      children: React.ReactNode;
    }) {
      const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
       
      useServerInsertedHTML(() => {
        const styles = styledComponentsStyleSheet.getStyleElement();
        styledComponentsStyleSheet.instance.clearTag();
        return <>{styles}</>;
      });
       
      if (typeof window !== 'undefined') return <>{children}</>;
       
      return (
        <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
          {children}
        </StyleSheetManager>
      );
    }
    
  4. 在根布局中引入注册表:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    // src/app/layout.tsx
    import StyledComponentsRegistry from '@/lib/StyledComponentsRegistry';
       
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="zh-CN">
          <body>
            <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
          </body>
        </html>
      );
    }
    

六、Metadata 与 SEO 优化

Next.js 提供了完善的Metadata API,支持静态/动态元数据配置、模板化、结构化数据、图片/视频元数据等全场景SEO需求,无需手动操作<head>标签,自动处理服务端渲染的元数据注入,完美适配搜索引擎抓取和社交平台分享。

6.1 静态 Metadata 配置

layout.tsxpage.tsx中导出metadata对象,即可配置当前路由及子路由的元数据,子路由的配置会自动合并并覆盖父路由的配置。

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
// src/app/layout.tsx
import type { Metadata } from 'next';

// 全局静态元数据,所有子路由默认继承
export const metadata: Metadata = {
  title: {
    default: 'Next.js 全栈开发指南',
    template: '%s | Next.js 开发指南', // 子路由标题模板
  },
  description: '基于Next.js App Router的企业级全栈开发教程,从入门到实战',
  keywords: ['Next.js', 'React', '全栈开发', 'App Router', 'SSR'],
  authors: [{ name: '冴羽', url: 'https://github.com/mqyqingfeng' }],
  creator: '冴羽',
  publisher: '掘金小册',
  formatDetection: {
    email: false,
    address: false,
    telephone: false,
  },
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body>{children}</body>
    </html>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
// src/app/about/page.tsx
import type { Metadata } from 'next';

// 子路由元数据,自动继承父路由模板
export const metadata: Metadata = {
  title: '关于我们', // 最终标题:关于我们 | Next.js 开发指南
  description: '关于Next.js开发指南的作者与项目介绍',
};

export default function AboutPage() {
  return <h1>关于我们</h1>;
}

6.2 动态 Metadata 配置

通过generateMetadata函数实现动态元数据,可根据动态路由参数、请求信息、异步数据生成个性化元数据,适用于文章详情、商品详情等动态页面。

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
// src/app/posts/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';

// 动态生成元数据
export async function generateMetadata(
  { params }: { params: Promise<{ slug: string }> },
  parent: ResolvingMetadata // 父路由的元数据,可继承合并
): Promise<Metadata> {
  const { slug } = await params;
  // 异步获取文章数据
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(res => res.json());
  // 继承父路由的关键词
  const previousKeywords = (await parent).keywords || [];

  return {
    title: post.title,
    description: post.summary,
    keywords: [...previousKeywords, ...post.tags],
    openGraph: {
      title: post.title,
      description: post.summary,
      type: 'article',
      publishedTime: post.publishTime,
      authors: [post.author.name],
      images: [post.coverImage],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.summary,
      images: [post.coverImage],
    },
  };
}

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  // 页面逻辑
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(res => res.json());
  return <article>{post.title}</article>;
}

6.3 核心元数据字段说明

字段类型核心用途适用场景
基础SEO字段title、description、keywords、robots搜索引擎基础优化,控制抓取规则
Open GraphopenGraph社交平台(微信、微博、Facebook)分享卡片优化
Twitter CardtwitterTwitter/X平台分享卡片优化
文章元数据article博客、新闻类内容,标注发布时间、作者、标签等
视口配置viewport移动端适配,控制页面缩放、viewport尺寸
主题色themeColor浏览器地址栏主题色适配
图标配置icons、manifest网站favicon、PWA应用图标配置

6.4 特殊文件元数据

Next.js 支持通过约定式的特殊文件,自动生成对应的元数据,无需手动配置,放在app目录下即可全局生效,放在子路由目录下仅对当前路由生效:

文件名用途支持格式
favicon.ico网站默认图标.ico
icon.png/icon.jpg网站图标,支持多尺寸.png/.jpg/.jpeg/.svg
apple-icon.pngApple设备图标.png
opengraph-image.pngOpen Graph分享默认图片.png/.jpg/.jpeg
twitter-image.pngTwitter分享默认图片.png/.jpg/.jpeg
sitemap.ts自动生成网站站点地图.ts/.js
robots.ts自动生成robots.txt文件.ts/.js

站点地图与robots.txt生成示例

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
// src/app/sitemap.ts
import { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  // 获取所有文章
  const posts = await fetch('https://api.example.com/posts').then(res => res.json());
  const postEntries = posts.map((post: { slug: string; updateTime: string }) => ({
    url: `https://your-domain.com/posts/${post.slug}`,
    lastModified: post.updateTime,
    changeFrequency: 'weekly' as const,
    priority: 0.8,
  }));

  // 静态页面
  const staticEntries = [
    {
      url: 'https://your-domain.com',
      lastModified: new Date(),
      changeFrequency: 'daily' as const,
      priority: 1,
    },
    {
      url: 'https://your-domain.com/about',
      lastModified: new Date(),
      changeFrequency: 'monthly' as const,
      priority: 0.5,
    },
  ];

  return [...staticEntries, ...postEntries];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/app/robots.ts
import { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: ['/admin/', '/api/'],
    },
    sitemap: 'https://your-domain.com/sitemap.xml',
  };
}

6.5 SEO 最佳实践

  1. 每个页面必须配置唯一的title和description,避免重复内容导致搜索引擎降权;
  2. 使用模板化标题,通过title.template统一站点标题格式,提升品牌辨识度;
  3. 完善Open Graph和Twitter Card配置,优化社交平台分享效果,提升点击率;
  4. 动态页面必须使用generateMetadata,根据内容生成个性化元数据,避免静态模板化内容;
  5. 配置sitemap.xml和robots.txt,引导搜索引擎高效抓取站点内容;
  6. 使用语义化HTML标签,配合元数据提升搜索引擎对内容的理解;
  7. 移动端适配:配置正确的viewport,保证移动端体验,适配搜索引擎移动优先索引;
  8. 结构化数据:添加JSON-LD结构化数据,提升搜索引擎对内容的解析能力,获得富文本搜索结果。

七、Next.js 工程化与核心配置

Next.js 提供了高度可定制的工程化配置,通过next.config.ts文件可自定义构建、路由、性能、部署等全流程行为,同时原生支持TypeScript、ESLint、环境变量、路径别名等工程化能力,无需额外配置webpack。

7.1 核心配置文件 next.config.ts

next.config.ts位于项目根目录,是Next.js的核心配置文件,支持TypeScript类型提示,完整的配置项可参考官方文档,以下为企业级项目常用核心配置:

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
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  /* 基础配置 */
  reactStrictMode: true, // 开启React严格模式,提前发现问题,推荐开启
  poweredByHeader: false, // 移除响应头中的X-Powered-By,提升安全性
  devIndicators: {
    buildActivity: true, // 开发环境构建指示器
    buildActivityPosition: 'bottom-right',
  },

  /* 构建输出配置 */
  output: 'standalone', // 独立构建模式,生成最小化部署包,Docker部署必选
  // output: 'export', // 静态导出模式,生成纯静态HTML文件,适用于纯静态站点

  /* 环境变量配置 */
  env: {
    CUSTOM_API_URL: process.env.CUSTOM_API_URL, // 暴露给客户端的环境变量
  },

  /* 路径重写与重定向 */
  async redirects() {
    return [
      // 永久重定向,308状态码
      {
        source: '/old-blog/:slug',
        destination: '/posts/:slug',
        permanent: true,
      },
    ];
  },
  async rewrites() {
    return {
      beforeFiles: [
        // API反向代理,解决跨域问题
        {
          source: '/external-api/:path*',
          destination: 'https://external-api.com/:path*',
        },
      ],
    };
  },

  /* 图片优化配置 */
  images: {
    remotePatterns: [
      // 允许加载的远程图片域名,提升安全性
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        pathname: '/**',
      },
    ],
    formats: ['image/avif', 'image/webp'], // 优先使用现代图片格式
    deviceSizes: [640, 750, 828, 1080, 1200, 1920], // 响应式图片尺寸
  },

  /* 构建与编译配置 */
  compiler: {
    styledComponents: true, // 开启styled-components支持
    removeConsole: process.env.NODE_ENV === 'production' ? {
      exclude: ['error', 'warn'], // 生产环境移除console.log,保留error和warn
    } : false,
  },

  /* webpack自定义配置 */
  webpack: (config, { dev, isServer }) => {
    // 自定义webpack配置,如添加loader、plugin等
    if (!dev && !isServer) {
      // 生产环境客户端构建优化
    }
    return config;
  },

  /* 实验性特性 */
  experimental: {
    // 开启实验性特性,如ppr、reactCompiler等
    ppr: true, // 部分预渲染
    reactCompiler: true, // React编译器,自动优化渲染性能
  },
};

export default nextConfig;

7.2 TypeScript 支持

Next.js 原生深度支持TypeScript,create-next-app初始化时可一键开启,自动生成tsconfig.json配置文件,内置完整的类型定义,无需额外配置。

核心类型支持

  • 路由相关类型:NextPageLayoutPropsPagePropsMetadata
  • API相关类型:NextRequestNextResponseRouteHandler
  • 中间件类型:MiddlewareNextFetchEvent
  • 配置类型:NextConfig

最佳实践

  1. 开启严格模式:在tsconfig.json中设置"strict": true,强制类型校验,减少运行时错误;
  2. 禁止使用any类型,所有数据必须定义明确的类型;
  3. 服务端动作、API接口必须严格校验入参类型,配合Zod实现运行时校验;
  4. 使用typescript-plugin-css-modules实现CSS Modules的类型提示。

7.3 环境变量

Next.js 提供了完善的环境变量管理方案,区分服务端环境变量和客户端环境变量,避免敏感信息泄露。

环境变量文件

Next.js 会自动加载以下环境变量文件,优先级从高到低:

  1. .env.local:本地开发环境变量,不会被git提交,优先级最高;
  2. .env.development:开发环境(npm run dev)专属变量;
  3. .env.production:生产环境(npm run build && start)专属变量;
  4. .env:全局默认环境变量,所有环境均生效。

环境变量使用规则

  1. 服务端环境变量:默认所有环境变量仅在服务端可访问,不会暴露给客户端,可安全存储API密钥、数据库凭证等敏感信息;
    1
    2
    
    // 服务端组件、路由处理程序、Server Actions中可直接使用
    const db = connectDatabase(process.env.DATABASE_URL!);
    
  2. 客户端环境变量:必须以NEXT_PUBLIC_为前缀,才会被暴露给客户端,可在客户端组件中直接使用;
    1
    2
    
    # .env.local
    NEXT_PUBLIC_SITE_URL=https://your-domain.com
    
    1
    2
    3
    4
    
    'use client';
    export function SiteUrl() {
      return <p>站点地址:{process.env.NEXT_PUBLIC_SITE_URL}</p>;
    }
    
  3. 注意事项:永远不要将敏感信息放在以NEXT_PUBLIC_为前缀的环境变量中,会完全暴露给客户端。

7.4 ESLint 与代码规范

Next.js 内置了Next.js专属的ESLint配置,初始化时可一键开启,自动生成.eslintrc.json配置文件,提供了针对Next.js最佳实践的代码校验规则,避免常见的性能和安全问题。

1
2
3
{
  "extends": ["next/core-web-vitals", "next/typescript"]
}

常用命令

1
2
3
4
5
# 执行ESLint校验
npm run lint

# 自动修复可修复的问题
npm run lint -- --fix

最佳实践

  1. 配合Prettier实现代码格式化,统一团队代码风格;
  2. 在git提交前通过husky+lint-staged执行ESLint校验,禁止不合规代码提交;
  3. 生产环境构建时自动执行ESLint校验,不通过则构建失败,保证线上代码质量。

八、Next.js 性能优化核心方案

Next.js 内置了大量开箱即用的性能优化能力,无需开发者手动配置,即可实现接近满分的Core Web Vitals指标,同时提供了灵活的自定义优化方案,覆盖图片、字体、脚本、代码分割、渲染等全场景性能优化。

8.1 图片优化

图片是影响页面性能的最大因素之一,Next.js 提供了内置的<Image>组件,自动实现图片压缩、格式转换、响应式尺寸、懒加载、预加载等优化能力,相比原生<img>标签,可减少70%以上的图片体积,大幅提升加载速度。

基础使用

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
import Image from 'next/image';
// 本地图片,直接导入
import avatar from '@/assets/avatar.png';

export function Avatar() {
  return (
    <div>
      {/* 本地图片,自动获取宽高,避免布局偏移 */}
      <Image
        src={avatar}
        alt="用户头像"
        placeholder="blur" // 模糊占位图,避免布局偏移
        priority // 首屏关键图片,开启预加载
      />

      {/* 远程图片,必须指定宽高和域名白名单 */}
      <Image
        src="https://images.unsplash.com/photo-123456"
        alt="风景图"
        width={800}
        height={600}
        quality={80} // 图片质量,1-100,默认75
        loading="lazy" // 非首屏图片,懒加载
      />
    </div>
  );
}

核心优化能力

  1. 自动格式转换:根据浏览器支持情况,自动转换为AVIF、WebP等现代图片格式,体积比JPG/PNG小50%以上;
  2. 响应式图片:自动根据设备尺寸生成不同大小的图片,移动端加载小尺寸图片,避免带宽浪费;
  3. 避免布局偏移(CLS):强制要求指定宽高或使用fill模式配合父容器,从根本上解决图片加载导致的布局偏移,优化Core Web Vitals的CLS指标;
  4. 懒加载:非首屏图片默认开启懒加载,仅当图片进入视口时才加载,减少首屏请求数;
  5. 预加载:首屏关键图片通过priority属性开启预加载,优化LCP(最大内容绘制)指标;
  6. 模糊占位图:本地图片自动生成模糊占位图,远程图片可通过blurDataURL自定义,提升用户体验。

8.2 字体优化

网页字体加载是导致布局偏移、性能下降的常见原因,Next.js 提供了内置的next/font模块,自动实现字体优化,零布局偏移,同时避免字体加载的FOIT/FOUT问题。

谷歌字体优化

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
// src/app/layout.tsx
import { Inter, Noto_Sans_SC } from 'next/font/google';
import './globals.css';

// 加载谷歌字体,自动内联CSS,避免外部请求
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter', // CSS变量,全局使用
  display: 'swap', // 字体加载策略,避免FOIT
});

// 加载中文字体
const notoSansSC = Noto_Sans_SC({
  subsets: ['latin'],
  weight: ['400', '500', '700'],
  variable: '--font-noto-sans-sc',
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN" className={`${inter.variable} ${notoSansSC.variable}`}>
      <body>{children}</body>
    </html>
  );
}
1
2
3
4
/* src/app/globals.css */
body {
  font-family: var(--font-noto-sans-sc), var(--font-inter), sans-serif;
}

本地字体优化

1
2
3
4
5
6
7
8
import localFont from 'next/font/local';

// 加载本地字体文件
const myFont = localFont({
  src: './assets/MyFont.woff2',
  variable: '--font-my-font',
  display: 'swap',
});

核心优化能力

  1. 零布局偏移:字体文件在构建时自动下载,与HTML内联在一起,避免字体加载导致的布局偏移,优化CLS指标;
  2. 预加载优化:自动预加载关键字体,减少首屏字体加载时间;
  3. 子集化处理:自动对字体进行子集化,仅加载页面使用的字符,大幅减少字体文件体积,尤其适合中文字体优化;
  4. 避免外部请求:谷歌字体自动托管在站点域名下,避免谷歌字体服务访问失败的问题。

8.3 脚本优化

第三方脚本(统计、广告、监控等)是导致页面性能下降的重要原因,Next.js 提供了内置的<Script>组件,自动优化第三方脚本的加载策略,避免阻塞页面渲染。

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
import Script from 'next/script';

export function Analytics() {
  return (
    <>
      {/* 策略1:afterInteractive:页面交互后加载,默认策略,适用于统计脚本 */}
      <Script
        src="https://analytics.example.com/script.js"
        strategy="afterInteractive"
        onLoad={() => console.log('统计脚本加载完成')}
      />

      {/* 策略2:beforeInteractive:页面交互前加载,适用于关键核心脚本 */}
      <Script
        src="https://core-lib.example.com/script.js"
        strategy="beforeInteractive"
      />

      {/* 策略3:lazyOnload:空闲时加载,适用于非关键脚本,如广告、评论 */}
      <Script
        src="https://ads.example.com/script.js"
        strategy="lazyOnload"
      />

      {/* 内联脚本 */}
      <Script id="inline-script" strategy="afterInteractive">
        {`console.log('内联脚本执行')`}
      </Script>
    </>
  );
}

核心优化能力

  1. 智能加载策略:根据脚本重要性选择加载时机,避免阻塞页面渲染,优化LCP和TTI指标;
  2. 去重加载:同一脚本多次引入,只会加载一次,避免重复请求;
  3. 离线缓存:自动缓存第三方脚本,提升重复访问速度;
  4. 生命周期回调:提供onLoadonReadyonError回调,方便处理脚本加载状态。

8.4 代码分割与懒加载

Next.js 自动实现代码分割,按路由拆分JS chunk,访问页面时仅加载当前路由所需的代码,大幅减少首屏JS体积。同时支持手动组件级懒加载,进一步优化加载性能。

组件懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use client';
import { useState, lazy, Suspense } from 'react';

// 懒加载组件,仅当组件渲染时才加载对应的JS代码
const HeavyComponent = lazy(() => import('@/components/HeavyComponent'));

export function LazyLoadDemo() {
  const [showHeavyComponent, setShowHeavyComponent] = useState(false);

  return (
    <div>
      <button onClick={() => setShowHeavyComponent(true)}>
        加载重型组件
      </button>
      {/* 懒加载组件必须用Suspense包裹,提供加载兜底 */}
      {showHeavyComponent && (
        <Suspense fallback={<div>组件加载中...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}

8.5 Core Web Vitals 优化最佳实践

Core Web Vitals是Google定义的用户体验核心指标,也是搜索引擎排名的关键因素,Next.js 针对性的优化方案如下:

  1. LCP(最大内容绘制)优化
    • 首屏关键图片使用<Image>组件的priority属性开启预加载;
    • 减少首屏关键资源的体积,拆分非首屏代码;
    • 使用CDN加速静态资源访问;
    • 优化服务端响应时间,减少TTFB。
  2. INP(交互到下一次绘制)优化
    • 减少长任务,避免阻塞主线程;
    • 重型计算使用Web Worker,避免占用主线程;
    • 减少客户端组件的重渲染,使用React.memo、useMemo、useCallback优化;
    • 开启React Compiler,自动优化渲染性能。
  3. CLS(累积布局偏移)优化
    • 所有图片使用<Image>组件,指定宽高,避免布局偏移;
    • 使用next/font优化字体加载,避免字体加载导致的布局偏移;
    • 避免插入动态内容,提前为动态内容预留空间;
    • 避免使用无尺寸的广告位,提前设置广告容器的宽高。

九、认证与权限管理

企业级Next.js应用必须完善的认证与权限体系,Next.js 生态中最主流的方案是NextAuth.js(Auth.js),配合中间件、服务端组件、Server Actions可实现完整的鉴权与权限控制。

9.1 NextAuth.js(Auth.js)集成

NextAuth.js 是专为Next.js设计的开源认证库,支持邮箱密码、OAuth、手机号、魔法链接等多种登录方式,开箱即用,与Next.js深度集成。

基础集成

  1. 安装依赖:
    1
    
    npm install next-auth@beta
    
  2. 创建认证配置文件:
    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
    
    // src/auth.ts
    import NextAuth from 'next-auth';
    import Credentials from 'next-auth/providers/credentials';
    import Google from 'next-auth/providers/google';
    import { z } from 'zod';
    import { verifyPassword } from '@/lib/password';
    import { db } from '@/lib/db';
       
    // 认证配置
    export const { handlers, auth, signIn, signOut } = NextAuth({
      providers: [
        // 谷歌OAuth登录
        Google({
          clientId: process.env.GOOGLE_CLIENT_ID!,
          clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        }),
        // 邮箱密码登录
        Credentials({
          credentials: {
            email: { label: '邮箱', type: 'email' },
            password: { label: '密码', type: 'password' },
          },
          // 校验用户凭证
          async authorize(credentials) {
            // 校验入参
            const parsedCredentials = z
              .object({ email: z.string().email(), password: z.string().min(6) })
              .safeParse(credentials);
       
            if (!parsedCredentials.success) return null;
       
            const { email, password } = parsedCredentials.data;
            // 查询用户
            const user = await db.user.findUnique({ where: { email } });
            if (!user) return null;
            // 校验密码
            const passwordValid = await verifyPassword(password, user.hashedPassword);
            if (!passwordValid) return null;
            // 返回用户信息
            return { id: user.id, name: user.name, email: user.email, role: user.role };
          },
        }),
      ],
      // 会话配置
      session: {
        strategy: 'jwt',
        maxAge: 30 * 24 * 60 * 60, // 30天有效期
      },
      // 回调函数
      callbacks: {
        //  jwt回调,自定义token内容
        async jwt({ token, user }) {
          if (user) {
            token.userId = user.id;
            token.role = user.role;
          }
          return token;
        },
        // session回调,自定义session内容
        async session({ session, token }) {
          if (session.user) {
            session.user.id = token.userId as string;
            session.user.role = token.role as string;
          }
          return session;
        },
      },
      // 登录页路径
      pages: {
        signIn: '/login',
      },
    });
    
  3. 创建API路由:
    1
    2
    3
    4
    
    // src/app/api/auth/[...nextauth]/route.ts
    import { handlers } from '@/auth';
    // 导出GET和POST处理程序
    export const { GET, POST } = handlers;
    
  4. 提供Session上下文:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    // src/app/SessionProvider.tsx
    'use client';
    import { SessionProvider } from 'next-auth/react';
       
    export default function AuthSessionProvider({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return <SessionProvider>{children}</SessionProvider>;
    }
    
  5. 根布局中引入:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    // src/app/layout.tsx
    import AuthSessionProvider from './SessionProvider';
       
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="zh-CN">
          <body>
            <AuthSessionProvider>{children}</AuthSessionProvider>
          </body>
        </html>
      );
    }
    

鉴权使用

  1. 服务端组件/Server Actions/中间件中鉴权
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    // src/app/dashboard/page.tsx
    import { auth } from '@/auth';
    import { redirect } from 'next/navigation';
       
    export default async function DashboardPage() {
      // 服务端组件中获取会话,鉴权
      const session = await auth();
      // 未登录重定向到登录页
      if (!session?.user) {
        redirect('/login');
      }
       
      return <div>欢迎你,{session.user.name}</div>;
    }
    
  2. 客户端组件中鉴权
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    'use client';
    import { useSession, signIn, signOut } from 'next-auth/react';
       
    export function UserInfo() {
      const { data: session, status } = useSession();
       
      if (status === 'loading') return <div>加载中...</div>;
      if (status === 'unauthenticated') {
        return <button onClick={() => signIn()}>登录</button>;
      }
       
      return (
        <div>
          <p>当前用户:{session?.user?.name}</p>
          <button onClick={() => signOut()}>退出登录</button>
        </div>
      );
    }
    
  3. 中间件全局鉴权
    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
    
    // src/middleware.ts
    import { auth } from '@/auth';
    import { NextResponse } from 'next/server';
       
    export default auth((req) => {
      const isAuth = !!req.auth;
      const isProtectedRoute = req.nextUrl.pathname.startsWith('/dashboard');
      const isAuthPage = req.nextUrl.pathname.startsWith('/login');
       
      // 未登录访问受保护路由,重定向到登录页
      if (isProtectedRoute && !isAuth) {
        return NextResponse.redirect(new URL('/login', req.url));
      }
       
      // 已登录访问登录页,重定向到后台
      if (isAuthPage && isAuth) {
        return NextResponse.redirect(new URL('/dashboard', req.url));
      }
       
      return NextResponse.next();
    });
       
    export const config = {
      matcher: ['/dashboard/:path*', '/login'],
    };
    

9.2 权限控制

基于用户角色实现精细化的权限控制,分为页面级权限和操作级权限。

页面级权限控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/app/admin/page.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
import { notFound } from 'next/navigation';

export default async function AdminPage() {
  const session = await auth();
  // 未登录重定向
  if (!session?.user) {
    redirect('/login');
  }
  // 非管理员角色,返回404,隐藏路由存在
  if (session.user.role !== 'admin') {
    notFound();
  }

  return <div>管理员后台</div>;
}

操作级权限控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use server';
import { auth } from '@/auth';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';

export async function deleteUser(userId: string) {
  const session = await auth();
  // 鉴权:仅超级管理员可删除用户
  if (!session?.user || session.user.role !== 'super_admin') {
    throw new Error('无权限执行此操作');
  }

  await db.user.delete({ where: { id: userId } });
  revalidateTag('users');
}

十、部署与运维

Next.js 提供了多种部署方案,官方优先推荐Vercel一键部署,同时支持Docker自托管、Node.js服务部署、静态导出等多种方式,适配所有主流的云服务平台。

10.1 Vercel 一键部署

Vercel 是Next.js的开发公司,提供了全球边缘网络、自动构建、自动部署、预览环境等全流程能力,是Next.js的最佳部署平台,零配置即可实现最佳性能。

部署步骤

  1. 将项目代码推送到GitHub/GitLab/Bitbucket仓库;
  2. 登录Vercel,导入项目仓库;
  3. 配置环境变量;
  4. 点击部署,Vercel自动完成构建、优化、部署,生成可访问的域名;

核心优势

  • 零配置部署,自动识别Next.js项目,无需手动配置;
  • 全球边缘网络,自动部署到全球边缘节点,访问速度极致;
  • 自动预览环境,每个PR都会生成独立的预览环境,方便测试;
  • 自动HTTPS,免费提供SSL证书;
  • 内置监控与分析,自动监控性能、错误、访问量等指标;
  • 与GitHub深度集成,代码提交自动触发部署。

10.2 Docker 自托管部署

Docker是企业级自托管的首选方案,可实现跨环境一致部署,适配所有支持Docker的云平台和私有服务器。

部署步骤

  1. 配置next.config.ts,开启独立构建模式:
    1
    2
    3
    4
    5
    6
    7
    
    import type { NextConfig } from 'next';
       
    const nextConfig: NextConfig = {
      output: 'standalone', // 生成最小化独立部署包
    };
       
    export default nextConfig;
    
  2. 创建Dockerfile
    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
    
    # 多阶段构建:构建阶段
    FROM node:18-alpine AS base
       
    # 安装依赖阶段
    FROM base AS deps
    WORKDIR /app
    COPY package.json package-lock.json* ./
    RUN npm ci
       
    # 构建阶段
    FROM base AS builder
    WORKDIR /app
    COPY --from=deps /app/node_modules ./node_modules
    COPY . .
    # 构建生产环境代码
    RUN npm run build
       
    # 生产阶段:最小化镜像
    FROM base AS runner
    WORKDIR /app
       
    ENV NODE_ENV production
    # 非root用户运行,提升安全性
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nextjs
       
    # 复制构建产物
    COPY --from=builder /app/public ./public
    COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
    COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
       
    USER nextjs
    EXPOSE 3000
    ENV PORT 3000
       
    # 启动服务
    CMD ["node", "server.js"]
    
  3. 创建.dockerignore文件,排除不必要的文件:
    1
    2
    3
    4
    5
    6
    7
    8
    
    .git
    .gitignore
    node_modules
    npm-debug.log
    .next
    Dockerfile
    .dockerignore
    .env*.local
    
  4. 构建Docker镜像:
    1
    
    docker build -t nextjs-app .
    
  5. 运行容器:
    1
    
    docker run -p 3000:3000 --env-file .env.production nextjs-app
    

10.3 Node.js 服务部署

可将Next.js项目部署到任何支持Node.js的服务器,如阿里云、腾讯云、AWS EC2等。

部署步骤

  1. 项目构建:
    1
    
    npm run build
    
  2. 将构建后的.nextpackage.jsonnode_modulespublic等文件上传到服务器;
  3. 服务器上启动生产环境服务:
    1
    
    npm run start
    
  4. 使用PM2进行进程管理,保证服务稳定运行:
    1
    2
    3
    4
    
    npm install -g pm2
    pm2 start npm --name "nextjs-app" -- run start
    pm2 startup
    pm2 save
    
  5. 使用Nginx做反向代理,配置HTTPS和负载均衡。

10.4 静态导出部署

对于纯静态站点,可将Next.js项目导出为纯静态HTML文件,部署到任何静态文件托管平台,如GitHub Pages、Netlify、Cloudflare Pages、OSS等。

部署步骤

  1. 配置next.config.ts,开启静态导出:
    1
    2
    3
    4
    5
    6
    7
    
    import type { NextConfig } from 'next';
       
    const nextConfig: NextConfig = {
      output: 'export', // 静态导出
    };
       
    export default nextConfig;
    
  2. 执行构建:
    1
    
    npm run build
    
  3. 构建完成后,会生成out目录,包含所有静态HTML文件,将该目录部署到静态托管平台即可。

注意事项:静态导出模式下,无法使用服务端渲染、API路由、增量静态再生、中间件等依赖Node.js运行时的特性。


十一、企业级开发最佳实践

  1. 项目结构规范
    • 使用src目录隔离源码与配置文件;
    • 按业务模块拆分代码,app目录仅存放路由相关文件,通用组件、工具函数、hooks、类型定义等放在src/componentssrc/libsrc/hookssrc/types等目录中;
    • 路由组按业务模块划分,避免app目录下文件夹过多导致结构混乱。
  2. 渲染策略规范
    • 服务端组件优先,仅在需要交互时使用客户端组件,最小化客户端JS体积;
    • 静态优先,动态兜底,尽可能使用静态渲染,仅在必要时使用动态渲染;
    • 合理拆分Suspense,避免慢请求阻塞整个页面,优先展示用户关注的内容;
    • 动态路由尽可能使用generateStaticParams预渲染,提升访问性能。
  3. 数据处理规范
    • 服务端组件中直接获取数据,避免客户端不必要的请求;
    • 给fetch请求添加标签,通过revalidateTag精准失效缓存,避免全量刷新;
    • 所有用户输入必须在服务端做二次校验,禁止信任客户端校验结果;
    • 敏感数据操作必须使用Server Actions,禁止在客户端暴露API密钥和数据库凭证;
    • 数据库操作必须使用ORM(Prisma/Drizzle),禁止直接拼接SQL,避免SQL注入。
  4. 安全规范
    • 所有敏感路由必须做鉴权,中间件全局鉴权+页面/操作级二次鉴权双重保障;
    • 环境变量严格区分服务端和客户端,敏感信息禁止使用NEXT_PUBLIC_前缀;
    • 服务端动作必须做权限校验,防止越权操作;
    • 开启React严格模式和Next.js ESLint校验,提前发现安全问题;
    • 移除X-Powered-By响应头,避免泄露技术栈;
    • 配置内容安全策略(CSP),防止XSS攻击。
  5. 性能规范
    • 所有图片使用<Image>组件,所有字体使用next/font优化,避免布局偏移;
    • 第三方脚本使用<Script>组件,选择合适的加载策略,避免阻塞渲染;
    • 非首屏组件使用懒加载,减少首屏JS体积;
    • 避免客户端组件不必要的重渲染,优化交互性能;
    • 定期分析构建产物,拆分过大的chunk,优化加载性能。
  6. 工程化规范
    • 使用TypeScript严格模式,保证类型安全;
    • 统一代码规范,使用ESLint+Prettier格式化代码;
    • 配置git hooks,提交前执行代码校验和单元测试;
    • 完善自动化测试,覆盖核心业务逻辑;
    • 环境变量按环境拆分,本地、测试、生产环境严格隔离。

十二、常见问题与避坑指南

  1. 水合不匹配错误
    • 原因:客户端组件渲染的内容与服务端渲染的HTML不一致,最常见的原因是在渲染期间使用了浏览器API、生成了随机数、根据窗口尺寸渲染不同内容。
    • 解决方案:将依赖浏览器API的逻辑放到useEffect中执行,使用useState延迟到客户端渲染,避免服务端与客户端内容不一致。
  2. 服务端组件中使用浏览器API报错
    • 原因:服务端组件运行在Node.js环境,没有window、document等浏览器API,直接使用会报错。
    • 解决方案:将依赖浏览器API的逻辑拆分到客户端组件中,添加'use client'声明。
  3. 客户端组件导入服务端组件报错
    • 原因:Next.js 严格禁止客户端组件直接导入服务端组件,客户端环境无法执行服务端组件的代码。
    • 解决方案:将服务端组件以children或其他props的形式传递给客户端组件,实现二者解耦。
  4. 动态路由params获取报错
    • 原因:Next.js 15+中,params变为异步Promise,必须通过await获取,直接解构会报错。
    • 解决方案:const { slug } = await params;,必须在async函数中使用。
  5. 缓存不生效/页面不更新
    • 原因:fetch请求默认被永久缓存,数据更新后页面仍显示旧内容。
    • 解决方案:数据修改后通过revalidateTagrevalidatePath手动失效缓存,或给fetch请求设置合理的revalidate时效。
  6. Server Actions 重定向不生效
    • 原因:redirect函数会抛出一个特殊的错误,若在try/catch中捕获了所有错误,会导致重定向失效。
    • 解决方案:在catch中判断错误类型,排除NEXT_REDIRECT错误,不捕获重定向抛出的错误。
  7. 中间件中使用Node.js API报错
    • 原因:中间件默认运行在Edge Runtime,仅支持Web API,不支持Node.js专属API。
    • 解决方案:Next.js 15.5+可配置runtime: 'nodejs'使用Node.js Runtime,或改用Web API实现对应逻辑。
  8. 多根布局跨路由导航页面全量刷新
    • 原因:不同路由组的根布局是完全独立的,跨根布局导航会触发页面全量重新加载,无法实现客户端无刷新导航。
    • 解决方案:尽量使用单一根布局,通过嵌套布局实现不同页面的布局差异,避免多根布局。

总结

Next.js 已经从最初的React SSR框架,演进为如今一站式的全栈Web开发解决方案,基于React Server Component构建的App Router体系,彻底重构了现代Web应用的开发模式,让开发者无需关注复杂的工程化配置、渲染优化、后端服务搭建,专注于业务逻辑的实现。

本文全面覆盖了Next.js的核心特性,从路由系统、渲染体系、数据获取与缓存、样式方案、SEO优化,到工程化配置、性能优化、认证权限、部署运维,完整讲解了企业级Next.js应用开发的全流程知识。无论是入门学习还是企业级项目开发,掌握这些核心能力,都能高效构建出高性能、高安全、易维护的现代Web应用。

Next.js 生态仍在快速迭代,新特性持续推出,建议开发者始终以官方文档为核心参考,结合最佳实践,不断优化开发方案,充分发挥Next.js的全栈能力。

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