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 - 参数获取:访问
/shop时params为空对象,访问带参数路径时与全量捕获规则一致。
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 核心用途
- 按业务逻辑分组代码 将不同业务模块的路由、组件、工具函数按路由组隔离,避免 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 # 全局根布局
- 同层级路由使用不同布局 无需嵌套路由,即可为同一层级的不同路由设置独立的布局,避免布局嵌套冗余。
- 创建多个根布局 删除全局根布局
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. 仅生产环境生效,开发环境会直接显示错误堆栈。 |
核心文件代码示例
- 嵌套布局 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> ); }
- 加载界面 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> ); }
- 错误边界 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>
);
}
- 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 实现,相比原生页面跳转,仅更新页面必要组件,不会重新加载整个页面,大幅提升导航体验,同时支持预获取,提前加载目标路由的资源。
2.5.1 <Link> 组件(官方推荐首选)
<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.pushState 和 window.history.replaceState 可以直接更新浏览器历史记录,实现无刷新路由更新,常配合 usePathname() 和 useSearchParams() 使用,适用于列表排序、筛选条件更新等无需刷新页面的场景。
2.6 路由处理程序(Route Handlers)
路由处理程序是 Next.js 中用于自定义 API 接口的实现方案,替代传统前后端分离架构中的独立后端服务,基于 Web 标准的 Request 和 Response 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包的NextRequest和NextResponse,对 TypeScript 友好,封装了 Cookie、Headers 处理等便捷功能,也可使用原生 WebRequest/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 入参与参数处理
请求处理函数可接收两个可选入参,用于获取请求信息和动态路由参数:
- request:
NextRequest对象,原生 WebRequest的扩展,用于获取查询参数、请求体、Cookie、请求头等信息; - 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 请求有完善的缓存策略,生产环境下默认开启静态缓存,大幅提升接口响应速度。
- 默认缓存行为
- 开发模式(
npm run dev):无缓存,每次请求实时处理; - 生产模式(
npm run build && start):默认静态缓存,构建时预渲染接口结果,后续请求直接返回缓存内容。
- 开发模式(
- 退出缓存(转为动态渲染) 满足以下任一条件,GET 请求将取消缓存,每次请求实时处理:
- 函数中使用了
request参数(哪怕未实际使用); - 同一
route.ts中导出了非 GET 方法(如 POST/PUT); - 调用了
cookies()/headers()等动态函数; - 手动声明路由段配置
export const dynamic = 'force-dynamic'。
- 函数中使用了
- 缓存重新验证(定时更新) 无需退出缓存,仅设置缓存时效,到期后触发后台更新,平衡性能与数据实时性,两种实现方式:
- 路由段全局配置:
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 配置项,性能更优。
- 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' }],
},
],
};
- 条件语句匹配 在
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 明确了中间件与其他配置的执行顺序,从先到后为:
next.config.js中的headers配置next.config.js中的redirects配置- 中间件(Middleware)
next.config.js中的rewrites(beforeFiles)配置- 文件系统路由匹配(页面/接口)
next.config.js中的rewrites(afterFiles)配置- 动态路由匹配
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)的默认渲染模式,渲染工作完全在浏览器端执行:
- 浏览器先下载一个几乎空白的 HTML 文件和完整的 JS bundle;
- JS 加载完成后,在客户端执行 React 代码,发起数据请求;
- 数据返回后,React 渲染生成完整的 DOM 树,更新页面。
核心优缺点:
- 优点:开发体验好,前后端分离彻底,页面切换无刷新,交互体验好;
- 缺点:首屏加载速度慢,白屏时间长,SEO 不友好(搜索引擎难以抓取 JS 渲染的内容),bundle 体积大,低端设备体验差。
Next.js 实现方式:
- 使用 React
useEffect钩子在客户端发起数据请求; - 使用 SWR、TanStack Query 等客户端数据获取库。
3.1.2 SSR(服务端渲染)
服务端渲染将渲染工作放在服务端执行,解决了 CSR 的首屏和 SEO 问题:
- 浏览器发起请求,服务端接收请求后,获取页面所需的所有数据;
- 服务端将 React 组件渲染为完整的 HTML 字符串,返回给浏览器;
- 浏览器直接渲染 HTML,快速展示非交互页面,同时下载 JS bundle;
- JS 加载完成后,执行水合(Hydration)过程,为 HTML 绑定事件,赋予页面交互能力。
核心优缺点:
- 优点:首屏加载速度快,SEO 友好,内容可被搜索引擎直接抓取,首屏内容绘制时间短;
- 缺点:服务端计算压力大,首字节时间(TTFB)长,需等待所有数据获取完成才能返回 HTML,全量水合完成后页面才能交互,串行执行效率低。
Next.js Pages Router 实现方式: 页面组件中导出 getServerSideProps 异步函数,该函数每次请求都会被调用,获取的数据通过 props 传递给页面组件。
3.1.3 SSG(静态站点生成)
静态站点生成是 Next.js 推荐的默认渲染模式,渲染工作在构建阶段执行:
- 项目构建时,Next.js 提前获取所有静态页面所需的数据;
- 将每个页面预渲染为完整的静态 HTML 文件,保存在构建产物中;
- 用户访问时,直接返回预先生成的 HTML 文件,可搭配 CDN 实现全球加速。
核心优缺点:
- 优点:性能极致,访问速度最快,服务端无计算压力,稳定性高,SEO 友好;
- 缺点:内容实时性差,内容更新需要重新构建项目,不适合频繁更新的动态内容。
Next.js Pages Router 实现方式:
- 无数据请求的页面,默认会被预渲染为静态 HTML;
- 需数据的页面,导出
getStaticProps函数,构建时执行获取数据; - 动态路由页面,导出
getStaticPaths函数定义预渲染的路径列表。
3.1.4 ISR(增量静态再生)
增量静态再生是 SSG 的升级方案,解决了静态内容实时性差的问题,兼顾静态性能与动态内容实时性:
- 构建时预渲染指定的静态页面;
- 用户访问时,先返回预渲染的静态页面;
- 若访问时间超过设置的重新验证间隔,Next.js 会在后台重新渲染页面,更新静态缓存;
- 下一次用户访问时,返回更新后的静态页面。
核心优缺点:
- 优点:无需全量重新构建即可更新内容,兼顾静态性能与实时性,支持百万级页面的增量生成;
- 缺点:首次访问超时时长的用户,仍会看到旧内容,后台更新后才会展示新内容。
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 渲染模型存在三个无法兼顾的核心矛盾:好的用户体验、易于维护的代码结构、极致的性能,具体表现为:
- 客户端组件分组件请求数据,会产生多次 HTTP 往返,串行请求时性能极差;
- 所有组件代码都需要打包到客户端,哪怕是仅在服务端执行的逻辑,导致 bundle 体积过大,加载和水合成本高;
- 客户端组件无法直接访问后端数据库、私有 API,必须通过中间层接口转发,增加开发成本;
- 敏感逻辑放在客户端存在安全风险,容易被反编译和篡改。
RSC 的出现,彻底解决了这些矛盾,将组件分为服务端组件和客户端组件,各司其职,实现了性能、安全性、开发体验的平衡。
3.2.2 核心运行原理
- 组件拆分:React 将组件树分为服务端组件和客户端组件,服务端组件在服务端执行,客户端组件在浏览器端执行;
- 服务端渲染:服务端组件在服务端完成数据请求和渲染,可直接访问数据库、私有 API,无需通过接口转发;
- RSC Payload 生成:服务端将渲染结果序列化为特殊的 JSON 格式(RSC Payload),包含三部分内容:
- 服务端组件的渲染结果(DOM 结构和数据);
- 客户端组件的占位符和引用地址;
- 组件之间传递的 props 数据;
- 客户端重构:浏览器接收到 RSC Payload 后,React 重构完整的组件树,加载客户端组件的 JS 代码,填充占位符;
- 水合与交互:仅对客户端组件执行水合过程,绑定事件,赋予页面交互能力。
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 对两种组件的组合使用有严格的规则,核心是单向导入规则:
- 服务端组件可直接导入客户端组件,可在服务端组件中嵌套使用客户端组件;
- 客户端组件不能直接导入服务端组件,否则会报错,因为客户端环境无法执行服务端组件的代码;
- 解决方案:若需在客户端组件中使用服务端组件,可将服务端组件以
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 开发最佳实践
- 服务端组件优先原则:尽可能使用服务端组件,仅在需要用户交互、状态管理、浏览器 API 时才使用客户端组件,最小化客户端 JS 体积。
- 客户端组件尽可能下移:将客户端组件放在组件树的最底层,仅让需要交互的部分为客户端组件,外层布局、静态内容保持为服务端组件。
- 服务端代码防泄露:使用
server-only包,确保包含敏感逻辑的服务端模块不会被客户端组件引用,构建时会直接报错。1
npm install server-only1 2 3 4
// src/lib/db.ts import 'server-only'; // 包含数据库连接、API密钥等敏感信息的模块 export const db = connectDatabase(process.env.DATABASE_URL!);
- 三方包适配:若第三方包使用了客户端特性但未添加
'use client'声明,需自行封装一层客户端组件包裹该三方包,再在服务端组件中使用。 - React Context 使用:服务端组件不支持 React Context,需在客户端组件中创建
Context.Provider,再在根布局(服务端组件)中导入使用。 - props 序列化:服务端组件传递给客户端组件的 props 必须是可序列化的,不能传递函数、React 组件、类实例等不可序列化数据。
3.4 Suspense 与 Streaming 流式渲染
Streaming 流式渲染是 React 18 推出的核心特性,Next.js 基于 <Suspense> 组件原生支持流式渲染,彻底解决了传统 SSR 的串行阻塞问题。
3.4.1 传统 SSR 的核心弊端
传统 SSR 的执行流程是完全串行的,存在三个无法解决的阻塞问题:
- 数据获取阻塞渲染:必须等待页面所有数据获取完成,才能开始渲染 HTML;
- 渲染阻塞发送:必须等待整个页面渲染完成,才能将 HTML 发送给浏览器;
- 水合阻塞交互:必须等待所有组件的 JS 加载完成,才能开始水合,全量水合完成后页面才能交互。
任何一个环节的慢请求,都会导致整个页面的加载和交互延迟,用户体验极差。
3.4.2 Suspense 与 Streaming 的核心原理
Streaming 流式渲染基于 HTTP 的分块传输编码(Transfer-Encoding: chunked)实现,将页面 HTML 拆分为多个小块,逐步从服务端发送到客户端,无需等待整个页面渲染完成。
<Suspense> 组件是实现流式渲染的核心,它允许开发者推迟指定组件的渲染,直至数据加载完成,同时为组件设置 fallback 兜底 UI,慢请求的组件不会阻塞整个页面的渲染。
完整执行流程:
- 服务端接收到请求,先渲染页面中无需等待数据的部分,发送初始 HTML 给浏览器;
- 浏览器接收到初始 HTML,立即渲染页面的静态内容和
fallback兜底 UI,用户可快速看到页面内容; - 服务端异步加载慢组件的数据,数据加载完成后,渲染组件的 HTML,通过同一个 HTTP 连接分块发送给浏览器;
- 浏览器接收到新的 HTML 块,替换对应的
fallbackUI,更新页面内容; - 所有组件发送完成后,HTTP 连接关闭,同时客户端逐步加载组件的 JS 代码,优先水合用户交互的组件。
3.4.3 Next.js 中的两种实现方式
- 组件级 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>
);
}
- 页面级 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()、页面组件的searchParamsprops; - 未缓存的数据请求:fetch 请求添加
cache: 'no-store'、revalidate: 0,或在动态函数后使用的 fetch 请求。
关键提醒:动态渲染和数据缓存是两个独立的概念,动态渲染不代表数据请求一定不缓存,可在动态渲染的路由中使用缓存的 fetch 请求,平衡性能与实时性。
3.5.3 Streaming 流式渲染
无需手动配置,使用 loading.tsx 或 React <Suspense> 组件即可自动开启流式渲染,将页面拆分为多个块,逐步从服务端发送到客户端,优化慢请求场景的用户体验。
3.6 渲染体系最佳实践
- 静态优先,动态兜底:尽可能使用静态渲染,仅在必要时使用动态渲染,最大化页面性能;
- 最小化客户端组件:仅在需要交互的地方使用客户端组件,其余内容均使用服务端组件,减少客户端 JS 体积;
- 合理拆分 Suspense:将慢请求的组件单独用 Suspense 包裹,避免阻塞整个页面的渲染,优先展示用户关注的内容;
- 平衡实时性与性能:对于半动态内容,使用
revalidate设置合理的缓存时效,而非直接使用无缓存的动态渲染; - 避免水合不匹配:客户端组件中不要在渲染期间修改依赖浏览器 API 的内容,如
window.innerWidth,否则会导致水合错误,可通过useEffect或useState延迟到客户端执行。
四、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 重构了数据获取方案,是当前官方推荐的标准方式,核心特性:
- 服务端组件原生支持异步数据获取:直接在组件中使用
async/await获取数据,无需约定式函数,代码更简洁; - 原生 fetch 扩展:React 扩展了原生
fetchAPI,支持自动请求去重、缓存、重新验证,无需第三方数据获取库; - 并行数据获取:多个异步组件的 fetch 请求并行执行,无瀑布流问题,大幅提升数据获取效率;
- Server Actions 处理数据提交:无需单独编写 API 路由,直接在服务端函数中处理数据提交、修改、删除等写操作,类型安全,开发效率更高;
- 四大缓存层协同:从请求级到路由级,再到客户端级,实现全链路缓存自动化,无需手动管理缓存。
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 的 cache 和 next.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 配置设置缓存的有效时长,到期后自动触发重新验证,更新缓存内容,分为两个级别:
- fetch 级别:仅对单个 fetch 请求生效
1
fetch('https://api.example.com/posts', { next: { revalidate: 60 } });
- 路由段级别:对整个路由段的所有 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)
通过 revalidateTag 和 revalidatePath 函数,手动精准失效缓存,适用于数据提交后立即更新页面内容的场景,无需等待缓存超时。
- 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'); }
- 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 分为两种定义方式:
- 组件内定义:在服务端组件中定义,函数顶部添加
'use server'声明; - 单独文件定义:在单独的文件中定义,文件顶部添加
'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 安全最佳实践
- 强制服务端校验:永远不要信任客户端传递的数据,所有入参必须在服务端做二次校验,推荐使用Zod、Yup等校验库;
- 鉴权与权限控制:服务端动作顶部必须先做用户鉴权和权限校验,防止未授权访问,禁止在客户端控制权限;
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'); }
- 速率限制:对敏感操作(登录、注册、数据修改)添加速率限制,防止暴力攻击和恶意请求;
- 禁止暴露敏感信息:服务端动作的错误信息必须脱敏,禁止将数据库错误、系统路径等敏感信息返回给客户端;
- 严格的类型定义:使用TypeScript严格定义入参和返回值类型,避免类型漏洞。
4.6 数据获取与缓存最佳实践
- 服务端组件优先:尽可能在服务端组件中获取数据,减少客户端请求,提升性能和安全性;
- 并行数据获取:将无依赖的异步请求拆分到独立组件中,实现并行执行,避免请求瀑布流;
- 合理设置缓存策略:
- 静态不变内容:使用默认永久缓存,提升访问速度;
- 半动态内容:通过
revalidate设置合理的缓存时效,平衡性能与实时性; - 强实时内容:使用
cache: 'no-store'退出缓存,每次请求获取最新数据;
- 标签化缓存管理:给fetch请求添加标签,通过
revalidateTag精准失效缓存,避免全量刷新; - 错误边界兜底:给异步组件添加
error.tsx错误边界,处理数据请求失败的场景,提升用户体验; - 加载状态优化:通过
loading.tsx和Suspense拆分加载粒度,优先展示用户关注的内容,减少白屏时间; - 客户端数据请求:需要实时更新、频繁交互的数据,使用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
npx create-next-app@latest --tailwind - 手动集成:
1 2
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p
- 配置
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;
- 全局CSS文件中引入Tailwind指令:
1 2 3 4
/* src/app/globals.css */ @tailwind base; @tailwind components; @tailwind utilities;
- 在根布局中引入全局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后缀的文件实现样式隔离,避免全局样式污染,适合中大型项目的组件级样式管理。
基础使用
- 创建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; }
- 在组件中导入使用:
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 2
npm install styled-components npm install -D @types/styled-components babel-plugin-styled-components
- 配置
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;
- 创建客户端组件的样式注册表,处理服务端渲染的样式收集:
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> ); }
- 在根布局中引入注册表:
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.tsx或page.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 Graph | openGraph | 社交平台(微信、微博、Facebook)分享卡片优化 |
| Twitter Card | Twitter/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.png | Apple设备图标 | .png |
opengraph-image.png | Open Graph分享默认图片 | .png/.jpg/.jpeg |
twitter-image.png | Twitter分享默认图片 | .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 最佳实践
- 每个页面必须配置唯一的title和description,避免重复内容导致搜索引擎降权;
- 使用模板化标题,通过
title.template统一站点标题格式,提升品牌辨识度; - 完善Open Graph和Twitter Card配置,优化社交平台分享效果,提升点击率;
- 动态页面必须使用generateMetadata,根据内容生成个性化元数据,避免静态模板化内容;
- 配置sitemap.xml和robots.txt,引导搜索引擎高效抓取站点内容;
- 使用语义化HTML标签,配合元数据提升搜索引擎对内容的理解;
- 移动端适配:配置正确的viewport,保证移动端体验,适配搜索引擎移动优先索引;
- 结构化数据:添加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配置文件,内置完整的类型定义,无需额外配置。
核心类型支持
- 路由相关类型:
NextPage、LayoutProps、PageProps、Metadata; - API相关类型:
NextRequest、NextResponse、RouteHandler; - 中间件类型:
Middleware、NextFetchEvent; - 配置类型:
NextConfig。
最佳实践
- 开启严格模式:在
tsconfig.json中设置"strict": true,强制类型校验,减少运行时错误; - 禁止使用
any类型,所有数据必须定义明确的类型; - 服务端动作、API接口必须严格校验入参类型,配合Zod实现运行时校验;
- 使用
typescript-plugin-css-modules实现CSS Modules的类型提示。
7.3 环境变量
Next.js 提供了完善的环境变量管理方案,区分服务端环境变量和客户端环境变量,避免敏感信息泄露。
环境变量文件
Next.js 会自动加载以下环境变量文件,优先级从高到低:
.env.local:本地开发环境变量,不会被git提交,优先级最高;.env.development:开发环境(npm run dev)专属变量;.env.production:生产环境(npm run build && start)专属变量;.env:全局默认环境变量,所有环境均生效。
环境变量使用规则
- 服务端环境变量:默认所有环境变量仅在服务端可访问,不会暴露给客户端,可安全存储API密钥、数据库凭证等敏感信息;
1 2
// 服务端组件、路由处理程序、Server Actions中可直接使用 const db = connectDatabase(process.env.DATABASE_URL!);
- 客户端环境变量:必须以
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>; }
- 注意事项:永远不要将敏感信息放在以
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
最佳实践
- 配合Prettier实现代码格式化,统一团队代码风格;
- 在git提交前通过husky+lint-staged执行ESLint校验,禁止不合规代码提交;
- 生产环境构建时自动执行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>
);
}
核心优化能力
- 自动格式转换:根据浏览器支持情况,自动转换为AVIF、WebP等现代图片格式,体积比JPG/PNG小50%以上;
- 响应式图片:自动根据设备尺寸生成不同大小的图片,移动端加载小尺寸图片,避免带宽浪费;
- 避免布局偏移(CLS):强制要求指定宽高或使用
fill模式配合父容器,从根本上解决图片加载导致的布局偏移,优化Core Web Vitals的CLS指标; - 懒加载:非首屏图片默认开启懒加载,仅当图片进入视口时才加载,减少首屏请求数;
- 预加载:首屏关键图片通过
priority属性开启预加载,优化LCP(最大内容绘制)指标; - 模糊占位图:本地图片自动生成模糊占位图,远程图片可通过
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',
});
核心优化能力
- 零布局偏移:字体文件在构建时自动下载,与HTML内联在一起,避免字体加载导致的布局偏移,优化CLS指标;
- 预加载优化:自动预加载关键字体,减少首屏字体加载时间;
- 子集化处理:自动对字体进行子集化,仅加载页面使用的字符,大幅减少字体文件体积,尤其适合中文字体优化;
- 避免外部请求:谷歌字体自动托管在站点域名下,避免谷歌字体服务访问失败的问题。
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>
</>
);
}
核心优化能力
- 智能加载策略:根据脚本重要性选择加载时机,避免阻塞页面渲染,优化LCP和TTI指标;
- 去重加载:同一脚本多次引入,只会加载一次,避免重复请求;
- 离线缓存:自动缓存第三方脚本,提升重复访问速度;
- 生命周期回调:提供
onLoad、onReady、onError回调,方便处理脚本加载状态。
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 针对性的优化方案如下:
- LCP(最大内容绘制)优化:
- 首屏关键图片使用
<Image>组件的priority属性开启预加载; - 减少首屏关键资源的体积,拆分非首屏代码;
- 使用CDN加速静态资源访问;
- 优化服务端响应时间,减少TTFB。
- 首屏关键图片使用
- INP(交互到下一次绘制)优化:
- 减少长任务,避免阻塞主线程;
- 重型计算使用Web Worker,避免占用主线程;
- 减少客户端组件的重渲染,使用React.memo、useMemo、useCallback优化;
- 开启React Compiler,自动优化渲染性能。
- 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
npm install next-auth@beta - 创建认证配置文件:
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', }, });
- 创建API路由:
1 2 3 4
// src/app/api/auth/[...nextauth]/route.ts import { handlers } from '@/auth'; // 导出GET和POST处理程序 export const { GET, POST } = handlers;
- 提供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>; }
- 根布局中引入:
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> ); }
鉴权使用
- 服务端组件/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>; }
- 客户端组件中鉴权:
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> ); }
- 中间件全局鉴权:
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的最佳部署平台,零配置即可实现最佳性能。
部署步骤:
- 将项目代码推送到GitHub/GitLab/Bitbucket仓库;
- 登录Vercel,导入项目仓库;
- 配置环境变量;
- 点击部署,Vercel自动完成构建、优化、部署,生成可访问的域名;
核心优势:
- 零配置部署,自动识别Next.js项目,无需手动配置;
- 全球边缘网络,自动部署到全球边缘节点,访问速度极致;
- 自动预览环境,每个PR都会生成独立的预览环境,方便测试;
- 自动HTTPS,免费提供SSL证书;
- 内置监控与分析,自动监控性能、错误、访问量等指标;
- 与GitHub深度集成,代码提交自动触发部署。
10.2 Docker 自托管部署
Docker是企业级自托管的首选方案,可实现跨环境一致部署,适配所有支持Docker的云平台和私有服务器。
部署步骤
- 配置
next.config.ts,开启独立构建模式:1 2 3 4 5 6 7
import type { NextConfig } from 'next'; const nextConfig: NextConfig = { output: 'standalone', // 生成最小化独立部署包 }; export default nextConfig;
- 创建
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"]
- 创建
.dockerignore文件,排除不必要的文件:1 2 3 4 5 6 7 8
.git .gitignore node_modules npm-debug.log .next Dockerfile .dockerignore .env*.local
- 构建Docker镜像:
1
docker build -t nextjs-app .
- 运行容器:
1
docker run -p 3000:3000 --env-file .env.production nextjs-app
10.3 Node.js 服务部署
可将Next.js项目部署到任何支持Node.js的服务器,如阿里云、腾讯云、AWS EC2等。
部署步骤:
- 项目构建:
1
npm run build
- 将构建后的
.next、package.json、node_modules、public等文件上传到服务器; - 服务器上启动生产环境服务:
1
npm run start
- 使用PM2进行进程管理,保证服务稳定运行:
1 2 3 4
npm install -g pm2 pm2 start npm --name "nextjs-app" -- run start pm2 startup pm2 save
- 使用Nginx做反向代理,配置HTTPS和负载均衡。
10.4 静态导出部署
对于纯静态站点,可将Next.js项目导出为纯静态HTML文件,部署到任何静态文件托管平台,如GitHub Pages、Netlify、Cloudflare Pages、OSS等。
部署步骤:
- 配置
next.config.ts,开启静态导出:1 2 3 4 5 6 7
import type { NextConfig } from 'next'; const nextConfig: NextConfig = { output: 'export', // 静态导出 }; export default nextConfig;
- 执行构建:
1
npm run build
- 构建完成后,会生成
out目录,包含所有静态HTML文件,将该目录部署到静态托管平台即可。
注意事项:静态导出模式下,无法使用服务端渲染、API路由、增量静态再生、中间件等依赖Node.js运行时的特性。
十一、企业级开发最佳实践
- 项目结构规范:
- 使用
src目录隔离源码与配置文件; - 按业务模块拆分代码,
app目录仅存放路由相关文件,通用组件、工具函数、hooks、类型定义等放在src/components、src/lib、src/hooks、src/types等目录中; - 路由组按业务模块划分,避免
app目录下文件夹过多导致结构混乱。
- 使用
- 渲染策略规范:
- 服务端组件优先,仅在需要交互时使用客户端组件,最小化客户端JS体积;
- 静态优先,动态兜底,尽可能使用静态渲染,仅在必要时使用动态渲染;
- 合理拆分Suspense,避免慢请求阻塞整个页面,优先展示用户关注的内容;
- 动态路由尽可能使用
generateStaticParams预渲染,提升访问性能。
- 数据处理规范:
- 服务端组件中直接获取数据,避免客户端不必要的请求;
- 给fetch请求添加标签,通过
revalidateTag精准失效缓存,避免全量刷新; - 所有用户输入必须在服务端做二次校验,禁止信任客户端校验结果;
- 敏感数据操作必须使用Server Actions,禁止在客户端暴露API密钥和数据库凭证;
- 数据库操作必须使用ORM(Prisma/Drizzle),禁止直接拼接SQL,避免SQL注入。
- 安全规范:
- 所有敏感路由必须做鉴权,中间件全局鉴权+页面/操作级二次鉴权双重保障;
- 环境变量严格区分服务端和客户端,敏感信息禁止使用
NEXT_PUBLIC_前缀; - 服务端动作必须做权限校验,防止越权操作;
- 开启React严格模式和Next.js ESLint校验,提前发现安全问题;
- 移除
X-Powered-By响应头,避免泄露技术栈; - 配置内容安全策略(CSP),防止XSS攻击。
- 性能规范:
- 所有图片使用
<Image>组件,所有字体使用next/font优化,避免布局偏移; - 第三方脚本使用
<Script>组件,选择合适的加载策略,避免阻塞渲染; - 非首屏组件使用懒加载,减少首屏JS体积;
- 避免客户端组件不必要的重渲染,优化交互性能;
- 定期分析构建产物,拆分过大的chunk,优化加载性能。
- 所有图片使用
- 工程化规范:
- 使用TypeScript严格模式,保证类型安全;
- 统一代码规范,使用ESLint+Prettier格式化代码;
- 配置git hooks,提交前执行代码校验和单元测试;
- 完善自动化测试,覆盖核心业务逻辑;
- 环境变量按环境拆分,本地、测试、生产环境严格隔离。
十二、常见问题与避坑指南
- 水合不匹配错误:
- 原因:客户端组件渲染的内容与服务端渲染的HTML不一致,最常见的原因是在渲染期间使用了浏览器API、生成了随机数、根据窗口尺寸渲染不同内容。
- 解决方案:将依赖浏览器API的逻辑放到
useEffect中执行,使用useState延迟到客户端渲染,避免服务端与客户端内容不一致。
- 服务端组件中使用浏览器API报错:
- 原因:服务端组件运行在Node.js环境,没有window、document等浏览器API,直接使用会报错。
- 解决方案:将依赖浏览器API的逻辑拆分到客户端组件中,添加
'use client'声明。
- 客户端组件导入服务端组件报错:
- 原因:Next.js 严格禁止客户端组件直接导入服务端组件,客户端环境无法执行服务端组件的代码。
- 解决方案:将服务端组件以
children或其他props的形式传递给客户端组件,实现二者解耦。
- 动态路由params获取报错:
- 原因:Next.js 15+中,params变为异步Promise,必须通过await获取,直接解构会报错。
- 解决方案:
const { slug } = await params;,必须在async函数中使用。
- 缓存不生效/页面不更新:
- 原因:fetch请求默认被永久缓存,数据更新后页面仍显示旧内容。
- 解决方案:数据修改后通过
revalidateTag或revalidatePath手动失效缓存,或给fetch请求设置合理的revalidate时效。
- Server Actions 重定向不生效:
- 原因:
redirect函数会抛出一个特殊的错误,若在try/catch中捕获了所有错误,会导致重定向失效。 - 解决方案:在catch中判断错误类型,排除
NEXT_REDIRECT错误,不捕获重定向抛出的错误。
- 原因:
- 中间件中使用Node.js API报错:
- 原因:中间件默认运行在Edge Runtime,仅支持Web API,不支持Node.js专属API。
- 解决方案:Next.js 15.5+可配置
runtime: 'nodejs'使用Node.js Runtime,或改用Web API实现对应逻辑。
- 多根布局跨路由导航页面全量刷新:
- 原因:不同路由组的根布局是完全独立的,跨根布局导航会触发页面全量重新加载,无法实现客户端无刷新导航。
- 解决方案:尽量使用单一根布局,通过嵌套布局实现不同页面的布局差异,避免多根布局。
总结
Next.js 已经从最初的React SSR框架,演进为如今一站式的全栈Web开发解决方案,基于React Server Component构建的App Router体系,彻底重构了现代Web应用的开发模式,让开发者无需关注复杂的工程化配置、渲染优化、后端服务搭建,专注于业务逻辑的实现。
本文全面覆盖了Next.js的核心特性,从路由系统、渲染体系、数据获取与缓存、样式方案、SEO优化,到工程化配置、性能优化、认证权限、部署运维,完整讲解了企业级Next.js应用开发的全流程知识。无论是入门学习还是企业级项目开发,掌握这些核心能力,都能高效构建出高性能、高安全、易维护的现代Web应用。
Next.js 生态仍在快速迭代,新特性持续推出,建议开发者始终以官方文档为核心参考,结合最佳实践,不断优化开发方案,充分发挥Next.js的全栈能力。