Article

全栈框架 Next.js

更新于:2026-07-10

第一部分:核心基础

1. 项目初始化与目录结构

1.1 create-next-app 脚手架使用

create-next-app 是官方提供的 CLI 工具,用于快速生成一个配置完备的 Next.js 项目骨架。

方法/参数名称语法用途代码示例注意事项
基本用法npx create-next-app@latest [project-name]创建一个名为 [project-name] 的新 Next.js 项目。如果省略项目名,会在当前目录创建。npx create-next-app@latest my-next-app强烈建议使用 @latest 标签以确保使用最新版本。npx 会自动下载并执行该命令,无需全局安装。
TypeScript--typescript-t在项目中启用 TypeScript 支持。npx create-next-app@latest my-app --typescript这是现代 Next.js 项目的推荐选择,提供更好的开发体验和类型安全。
ESLint--eslint-e在项目中集成 ESLint 进行代码质量检查。npx create-next-app@latest my-app --eslint官方推荐启用,有助于保持代码风格一致性和发现潜在错误。
Tailwind CSS--tailwind-T在项目中预配置 Tailwind CSS 作为 CSS 框架。npx create-next-app@latest my-app --tailwindTailwind 是 Next.js 官方深度集成的 CSS 方案,非常流行。
App Router--app-a使用新的 app 目录(App Router)而非传统的 pages 目录(Pages Router)。npx create-next-app@latest my-app --app重要:Next.js 13+ 的未来发展方向,默认推荐使用 App Router。
Src Directory--src-dir-s将 app(或 pages)、public 等核心目录放在 src 文件夹内。npx create-next-app@latest my-app --src-dir这是一种常见的项目组织方式,可以将源代码与配置文件等分离,使根目录更整洁。
Import Alias--import-alias@/* 设置导入别名,指向 ./src(或根目录)。npx create-next-app@latest my-app --import-alias "@/*"避免深层嵌套的相对路径导入(如 ../../../../components),提升代码可读性。通常与 --src-dir 一起使用。

1.2 App Router(app/)与 Pages Router(pages/)目录对比

Next.js 提供了两种路由系统,它们在架构理念、功能特性和目录结构上有显著区别。

概念名称说明注意事项
Pages Router(传统)- 目录:所有页面必须放在 pages 目录下。
- 路由:文件路径直接映射为 URL。例如 pages/about.js 对应 /about
- 布局:通过 pages/_app.jspages/_document.js 实现全局布局和 HTML 文档结构。
- 数据获取:主要依赖 getStaticPropsgetServerSidePropsgetStaticPaths 等生命周期方法。
- 组件模型:所有组件默认都是客户端组件(Client Components)。
- 这是 Next.js 早期版本的默认方案,成熟稳定。
- 对于需要精细控制每个页面渲染策略的简单应用来说很直观。
- 缺乏内置的、灵活的嵌套路由和布局系统。
App Router(新一代)- 目录:所有路由相关文件放在 app 目录下。
- 路由:同样基于文件系统,但引入了更强大的概念,如布局(layout.js)、加载状态(loading.js)、错误处理(error.js)、路由处理器(route.js)。
- 布局:通过 layout.js 文件实现嵌套和共享 UI,支持持久化状态。
- 数据获取:在服务端组件(Server Components)中直接使用 async/await 进行 fetch,框架自动处理缓存和水合。
- 组件模型:默认使用 React Server Components(RSC),仅在需要交互时才将组件标记为客户端组件("use client")。
- 官方推荐用于新项目,代表了 Next.js 的未来。
- 提供了更符合直觉的、声明式的路由和布局管理。
- RSC 模型能显著减少客户端 JavaScript 包体积,提升性能和 SEO。
- 功能更强大,但也引入了新的概念(如 Server/Client 组件边界),学习曲线稍陡。

1.3 核心约定文件(page.js、layout.js、loading.js、error.js、route.js)

这些是 App Router(app/ 目录)中的特殊文件,Next.js 会根据其名称和位置自动应用特定的行为。

Page 文件(page.js / page.tsx)

方法/文件名语法用途代码示例注意事项
Page 组件export default function Page() { ... }定义一个路由的 UI。每个路由段必须有一个 page.js 文件。// app/dashboard/page.js
export default function Dashboard() {
return <h1>Dashboard</h1>;
}
- 这是路由的必需文件。
- 默认是 Server Component,可以直接进行数据获取。

Layout 文件(layout.js / layout.tsx)

方法/文件名语法用途代码示例注意事项
Root Layoutexport default function RootLayout({ children }) { ... }定义整个应用的根布局,通常包含 <html><body> 标签。// app/layout.js
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
- 必须存在于 app 目录的根级别。
- 只能有一个 Root Layout。
Nested Layoutexport default function Layout({ children }) { ... }定义特定路由段及其子路由的共享 UI 布局。// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
return (
<div className="dashboard-container">
<Sidebar />
{children}
</div>
);
}
- 可以在任何子目录中定义,作用于该目录下的所有页面。
- 支持布局嵌套,形成层级结构。
- 默认是 Server Component。

Loading 文件(loading.js / loading.tsx)

方法/文件名语法用途代码示例注意事项
Loading 组件export default function Loading() { ... }为该路由段及其子路由定义加载状态的 UI。当页面数据在服务器上获取时显示。// app/dashboard/loading.js
export default function DashboardLoading() {
return <p>Loading dashboard...</p>;
}
- 利用 React Suspense 机制自动工作。
- 当导航到一个需要在服务器获取数据的页面时,会立即显示此 Loading 组件,直到数据准备好。

Error 文件(error.js / error.tsx)

方法/文件名语法用途代码示例注意事项
Error 组件export default function Error({ error, reset }) { ... }捕获该路由段及其子路由中发生的 JavaScript 错误,并展示降级 UI。// app/dashboard/error.js
"use client";
export default function DashboardError({ error, reset }) {
return (
<div>
<p>Something went wrong!</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
- 必须是 Client Component("use client"),因为需要处理客户端的交互(如重试按钮)。
- error 对象包含错误信息,reset 函数用于尝试重新渲染出错的子树。

Route Handler 文件(route.js / route.ts)

方法/文件名语法用途代码示例注意事项
GET 处理器export async function GET(request, context) { ... }定义对该路由的 HTTP GET 请求的处理逻辑。// app/api/users/route.js
export async function GET() {
const users = await db.users.findMany();
return Response.json(users);
}
- 文件名必须是 route.js
- 可以导出 GETPOSTPUTPATCHDELETE 等函数来处理对应的 HTTP 方法。
- request 是 Web Request 对象,context 包含 params 等路由信息。
POST 处理器export async function POST(request, context) { ... }定义对该路由的 HTTP POST 请求的处理逻辑。// app/api/users/route.js
export async function POST(request) {
const data = await request.json();
const user = await db.users.create({ data });
return Response.json(user, { status: 201 });
}
- 返回值必须是一个 Web Response 对象,可以使用 Response.json() 快捷方法。

2. 路由系统(Routing)

2.1 App Router 路由机制

App Router 基于文件系统的路由提供了更强大和灵活的功能。

静态路由

概念名称说明注意事项
静态路由路由路径与 app 目录下的文件/文件夹结构完全对应。例如,app/about/page.js 对应 /about 路由。- 这是最基础的路由形式。
- 文件名 page.js 是必需的,它定义了该路由的 UI。

动态路由([param])

概念名称说明注意事项
动态段使用方括号 [] 包裹的文件或文件夹名表示一个动态路由参数。例如,app/blog/[slug]/page.js 可以匹配 /blog/hello-world/blog/nextjs-13- 在 page.jslayout.js 中,可以通过 props.params 访问动态参数。在 Server Component 中,params 作为函数组件的 prop 传入;在 Route Handler 中,通过 context.params 访问。
- 参数名(如 slug)即为对象的键名。
多个动态段可以在同一路径中使用多个动态段。例如,app/shop/[category]/[product]/page.js- 每个动态段都会成为 params 对象中的一个独立属性。

可选动态路由([[…param]])

概念名称说明注意事项
Catch-all 段使用三个点 [...param] 可以捕获路径中未被其他路由定义的部分,并将其作为一个数组传递。例如,app/docs/[...slug]/page.js 可以匹配 /docs/a/docs/a/b/docs/a/b/c- params.slug 将是一个字符串数组,如 ['a', 'b']
- 必须放在路由段的末尾。
Optional Catch-all 段在 Catch-all 段外再加一层方括号 [[...param]] 使其变为可选。例如,app/post/[[...slug]]/page.js 可以匹配 /post(此时 slugundefined)以及 /post/a/b- 当没有匹配到任何动态部分时,params.slugundefined,而不是空数组。这使得根路径 /post 也能被此路由处理。

并行路由与拦截路由

概念名称说明注意事项
并行路由允许在同一层级同时渲染多个独立的路由。通过在文件夹名前加 @ 符号来定义。例如,app/@modalapp/@sidebar 可以与 app/page.js 并行存在。- 主要用于实现模态框、侧边栏等不影响主 URL 的 UI 状态。
- 在父级 layout.js 中,这些并行路由会作为 props 传入(如 modalsidebar)。
拦截路由允许在一个路由中渲染另一个路由的 UI,但 URL 保持不变。通过在文件夹名前加 (.)(..)(...) 来实现不同层级的拦截。- 常用于图片预览、模态登录等场景。用户点击一个链接,URL 不变,但页面上显示了目标路由的内容。
- 一旦用户刷新页面或直接访问目标 URL,则会正常导航到该路由。

路由组

概念名称说明注意事项
路由组使用圆括号 () 包裹文件夹名可以创建一个路由组。该文件夹本身不会影响 URL 结构。例如,app/(marketing)/about/page.js 仍然对应 /about- 主要用于组织代码、对路由进行逻辑分组或为不同组应用不同的布局/配置,而无需改变最终的 URL 路径。

2.2 Pages Router 路由机制(传统)

Pages Router 是 Next.js 早期版本的路由方案,虽然现在推荐使用 App Router,但仍有大量项目在使用。

基础页面路由

概念名称说明注意事项
基础路由路由路径与 pages 目录下的文件/文件夹结构完全对应。例如,pages/about.js 对应 /about 路由。- pages/index.js 对应根路径 /
- 所有页面组件都必须是默认导出(export default)。

动态路由

概念名称说明注意事项
动态页面使用方括号 [] 包裹的文件名表示一个动态路由参数。例如,pages/post/[id].js 可以匹配 /post/1/post/abc- 在页面组件中,可以通过 useRouter().query(客户端)或 getServerSideProps / getStaticPropscontext.params(服务端)访问动态参数 id
- query 对象包含了所有的查询参数和动态路由参数。
Catch-all 路由使用 [...slug].js 可以捕获任意深度的路径。例如,pages/post/[...slug].js 可以匹配 /post/a/b/c- query.slug 将是一个字符串数组 ['a', 'b', 'c']

自定义 _app.js 和 _document.js

文件名用途代码示例注意事项
_app.js自定义全局应用的行为。可以在此处初始化全局状态、添加全局布局、向页面组件传递额外的 props。js<br>// pages/_app.js<br>export default function MyApp({ Component, pageProps }) {<br> return (<br> <Layout><br> <Component {...pageProps} /><br> </Layout><br> );<br>}<br>- Component 是当前要渲染的页面组件。
- pageProps 是从 getStaticPropsgetServerSideProps 获取的数据。
- 此文件是可选的,但非常常用。
_document.js自定义应用的 HTML 和 <body> 标签。通常用于添加全局的 <meta> 标签、字体样式表或修改 html/body 的属性。js<br>// pages/_document.js<br>import { Html, Head, Main, NextScript } from 'next/document';<br>export default function Document() {<br> return (<br> <Html><br> <Head><br> <link rel="preconnect" href="https://fonts.googleapis.com" /><br> </Head><br> <body><br> <Main /><br> <NextScript /><br> </body><br> </Html><br> );<br>}<br>- 仅在服务器端渲染,不能用于初始化客户端状态。
- 必须包含 <Html><Head><Main><NextScript> 组件。
- 此文件很少需要自定义。

2.3 导航组件(next/navigation)

next/navigation 是 App Router 推荐使用的导航 API,提供了 Link 组件和 useRouter Hook。

方法/属性语法用途代码示例注意事项
href<Link href="/dashboard">Dashboard</Link>指定要导航到的目标路径。<Link href={`/blog/${post.slug}`}>Read more</Link>- 支持字符串路径和带有 pathnamequery 等属性的对象。
- 默认启用预取功能,在生产环境中,当 Link 进入视口时,Next.js 会自动在后台获取目标页面所需的 JavaScript 和数据,实现瞬间导航。
replace<Link href="/new-path" replace>Replace</Link>导航时替换当前历史记录条目,而不是添加新条目。-- 类似于 router.replace()
scroll<Link href="/top" scroll={false}>Top</Link>导航后是否滚动到页面顶部。默认为 true-- 设置为 false 可以保持当前滚动位置。

useRouter Hook

方法/属性语法用途代码示例注意事项
pushrouter.push('/new-route')导航到新路由,并将新 URL 添加到浏览器历史记录栈中。const handleNavigate = () => { router.push('/profile'); };- 必须在 Client Component("use client")中使用。
replacerouter.replace('/new-route')导航到新路由,但替换当前历史记录条目。// 通常用于表单提交后重定向
router.replace('/success');
- 用户点击后退按钮时,不会回到被替换的页面。
refreshrouter.refresh()重新获取当前路由的数据并刷新页面,但会保留客户端 React 状态。// 用于在客户端操作(如删除数据)后更新页面
await deleteUser();
router.refresh();
- 这是一个非常有用的功能,可以在不丢失本地 UI 状态的情况下更新服务端数据。
backrouter.back()导航回上一个历史记录条目。<button onClick={() => router.back()}>Go Back</button>- 等同于 window.history.back()
forwardrouter.forward()导航到下一个历史记录条目。-- 等同于 window.history.forward()

3. 数据获取(Data Fetching)

3.1 App Router 数据获取策略

App Router 的数据获取围绕 React Server Components(RSC)和内置的 fetch API 优化展开。

服务端组件中的直接 fetch

概念/方法语法用途代码示例注意事项
直接 fetchconst res = await fetch('https://...');
const data = await res.json();
在 Server Component 中直接使用原生 fetch API 获取数据。Next.js 会自动处理请求的缓存、水合和流式传输。js<br>// app/page.js<br>async function getData() {<br> const res = await fetch('https://api.example.com/posts');<br> return res.json();<br>}<br><br>export default async function Home() {<br> const posts = await getData();<br> return (<div>{posts.map(...)}</div>);<br>}<br>- 只能在 Server Component(默认)中使用。
- 这是 App Router 推荐的数据获取方式,简洁且高效。
- fetch 请求在构建时(对于静态页面)或请求时(对于动态页面)在服务器上执行。

缓存与重新验证策略

概念/选项语法用途代码示例注意事项
默认缓存fetch(url)Next.js 默认对 fetch 请求进行持久化缓存(直到构建时重建)。适用于不常变化的数据。const res = await fetch('https://api.example.com/static-data');- 这是 SSG(静态生成)的基础。
禁用缓存{ cache: 'no-store' }禁用缓存,每次请求都向源服务器发起新请求。适用于需要实时数据的场景。const res = await fetch('https://api.example.com/live-data', { cache: 'no-store' });- 这会导致该页面变为 SSR(服务器端渲染),因为无法在构建时确定内容。
强制重新验证{ next: { revalidate: number } }设置一个以秒为单位的重新验证时间(ISR - 增量静态再生)。在指定时间后,下一次请求会触发后台重新获取数据并更新缓存。const res = await fetch('https://api.example.com/blog-posts', { next: { revalidate: 60 } }); // 每60秒重新验证- 结合了 SSG 的性能优势和数据的近实时性。
- 如果在重新验证期间有多个请求,它们会共享同一个后台重新验证过程,避免”惊群效应”。
请求级缓存{ next: { tags: ['tag1', 'tag2'] } }为 fetch 请求打上标签,以便后续通过 revalidateTag 手动清除特定标签的缓存。const res = await fetch('https://api.example.com/user/123', { next: { tags: ['user-123'] } });- 需要配合 next/cache 中的 revalidateTag 函数使用,通常在 Route Handler 或 Server Action 中调用。

并行与串行数据获取

概念说明代码示例注意事项
并行获取同时发起多个独立的 fetch 请求,不等待前一个完成。可以显著减少总数据获取时间。js<br>async function Page() {<br> const [usersRes, postsRes] = await Promise.all([<br> fetch('https://api.example.com/users'),<br> fetch('https://api.example.com/posts')<br> ]);<br> const users = await usersRes.json();<br> const posts = await postsRes.json();<br> // ...<br>}<br>- 使用 Promise.all 是实现并行获取的标准方法。
- 适用于数据之间没有依赖关系的场景。
串行获取按顺序发起 fetch 请求,后一个请求依赖于前一个请求的结果。js<br>async function Page({ searchParams }) {<br> const query = searchParams.q;<br> if (!query) return <div>Enter a search term</div>;<br><br> const userRes = await fetch(`https://api.example.com/user?q=${query}`);<br> const user = await userRes.json();<br><br> const postsRes = await fetch(`https://api.example.com/posts?userId=${user.id}`);<br> const posts = await postsRes.json();<br> // ...<br>}<br>- 代码按顺序执行 await
- 适用于数据存在明确依赖关系的场景。

3.2 Pages Router 数据获取方法

Pages Router 通过特殊的异步函数在页面级别定义数据获取逻辑。

getStaticProps(静态生成 SSG)

方法语法用途代码示例注意事项
getStaticPropsexport async function getStaticProps(context) { ... }在构建时预渲染页面,并将获取的数据作为 props 传递给页面组件。生成的 HTML 和 JSON 数据会被缓存。js<br>// pages/index.js<br>export async function getStaticProps() {<br> const res = await fetch('https://api.example.com/posts');<br> const posts = await res.json();<br> return {<br> props: { posts },<br> revalidate: 60 // ISR: 每60秒重新生成<br> };<br>}<br><br>export default function Home({ posts }) { ... }<br>- 仅在 pages 目录下的页面文件中使用。
- 返回对象必须包含 props 键。
- 可以通过 revalidate 选项启用 ISR(增量静态再生)。

getStaticPaths(动态静态生成)

方法语法用途代码示例注意事项
getStaticPathsexport async function getStaticPaths() { ... }getStaticProps 配合使用,用于具有动态路由([id].js)的页面。它指定在构建时应该为哪些路径预渲染页面。js<br>// pages/post/[id].js<br>export async function getStaticPaths() {<br> const res = await fetch('https://api.example.com/posts');<br> const posts = await res.json();<br> const paths = posts.map(post => ({<br> params: { id: post.id.toString() }<br> }));<br> return { paths, fallback: 'blocking' };<br>}<br><br>export async function getStaticProps({ params }) {<br> const res = await fetch(`https://api.example.com/post/${params.id}`);<br> const post = await res.json();<br> return { props: { post } };<br>}<br>- 仅用于带有动态路由的页面。
- 必须返回一个包含 pathsfallback 属性的对象。
- fallback 选项(false'blocking''true')决定了如何处理未在 paths 中预定义的路径。

getServerSideProps(服务器端渲染 SSR)

方法语法用途代码示例注意事项
getServerSidePropsexport async function getServerSideProps(context) { ... }在每次请求时都在服务器上预渲染页面,并将获取的数据作为 props 传递给页面组件。js<br>// pages/profile.js<br>export async function getServerSideProps(context) {<br> const { req } = context;<br> const session = getAuthSession(req); // 从请求中获取认证信息<br> if (!session) {<br> return {<br> redirect: { destination: '/login', permanent: false }<br> };<br> }<br> const user = await fetchUser(session.userId);<br> return { props: { user } };<br>}<br><br>export default function Profile({ user }) { ... }<br>- 仅在 pages 目录下的页面文件中使用。
- 适用于需要基于请求上下文(如 cookies、HTTP 头)或实时数据来渲染页面的场景。
- 性能开销比 SSG 大,因为每次请求都需要在服务器上执行。

4. 渲染策略(Rendering Strategies)

4.1 静态站点生成(SSG)

SSG 是在构建时(即运行 next build 时)将页面预先渲染成 HTML 和 JSON 文件。这些静态文件可以被 CDN 缓存,提供极快的加载速度。

概念/方法说明App Router 实现方式Pages Router 实现方式注意事项
核心思想页面内容在部署前就已确定并生成为静态文件。在 Server Component 中使用 fetch,且不设置 { cache: 'no-store' } 或设置了较长的 revalidate 时间。在页面组件中导出 getStaticProps 函数。- 最适合营销页面、博客、文档等不经常变化的内容。
- 提供最佳的性能和 SEO 效果。
- 构建时间会随着页面数量增加而增长。
数据获取时机构建时(Build Time)。fetch 请求在 next build 命令执行期间发出。getStaticPropsnext build 命令执行期间被调用。- 如果数据源在构建时不可用,会导致构建失败。
缓存位置CDN / 边缘网络。生成的 HTML 和 RSC payload 被缓存。生成的 HTML 和 JSON props 文件被缓存。- 用户请求直接从离他们最近的 CDN 节点获取,无需触及应用服务器。

4.2 服务器端渲染(SSR)

SSR 是在每次用户请求时,在服务器上动态生成 HTML。这确保了用户总是看到最新的数据。

概念/方法说明App Router 实现方式Pages Router 实现方式注意事项
核心思想页面内容在每次 HTTP 请求到达服务器时实时生成。在 Server Component 中使用 fetch 并设置 { cache: 'no-store' }在页面组件中导出 getServerSideProps 函数。- 最适合需要高度个性化或实时数据的页面,如用户仪表盘、支付页面。
- 性能低于 SSG,因为每次请求都需要服务器计算。
- 对服务器资源消耗更大。
数据获取时机请求时(Request Time)。fetch 请求在每次用户访问该页面时发出。getServerSideProps 在每次用户访问该页面时被调用。- 可以访问请求上下文(reqres),例如用于身份验证。
缓存位置通常不缓存,或由应用层控制。由于禁用了缓存,每次请求都会重新生成。框架本身不缓存 SSR 页面,但可以通过反向代理(如 Nginx)进行缓存。- 需要考虑服务器负载和响应时间。

4.3 增量静态再生(ISR)

ISR 是 SSG 的一个强大扩展。它允许你在构建后,在后台更新静态页面,而无需重新部署整个应用。

概念/方法说明App Router 实现方式Pages Router 实现方式注意事项
核心思想页面在构建时生成,但在指定时间后,下一次用户请求会触发后台重新生成,并将新版本缓存起来供后续请求使用。在 fetch 中设置 { next: { revalidate: number } }getStaticProps 的返回对象中设置 revalidate: number- 完美平衡了 SSG 的性能和数据的时效性。
- 用户永远不会看到旧的、过期的数据;他们要么看到缓存的旧版本,要么触发并等待新版本(首次触发者)。
- 后续请求会立即获得新生成的页面。
重新验证触发基于时间或手动。时间:通过 revalidate 选项。
手动:通过 revalidateTagrevalidatePath
时间:通过 revalidate 选项。
手动:通过 res.revalidate(在 API Route 中)。
- 手动重新验证对于内容管理系统(CMS)更新后立即刷新页面非常有用。
Fallback 行为对于未预渲染的路径。不直接适用,但 App Router 的动态路由默认支持按需渲染。getStaticPaths 中设置 fallback: 'blocking''true'- fallback: 'blocking' 会在请求未生成的页面时,在服务器上完整渲染它,然后缓存。对用户是透明的。

4.4 客户端渲染(CSR)与 React Hydration

CSR 是指页面的初始 HTML 是一个空壳(或包含基本骨架),所有内容和交互逻辑都由客户端 JavaScript 在浏览器中加载和执行。React Hydration 是 Next.js 将服务端生成的 HTML”激活”为可交互的 React 应用的过程。

概念/方法说明App Router 实现方式Pages Router 实现方式注意事项
客户端渲染(CSR)初始 HTML 不包含具体内容,内容由浏览器中的 JS 动态填充。在 Client Component("use client")中使用 useEffect 进行数据获取。在页面组件的 useEffect 中进行数据获取。- 应尽量避免作为主要渲染策略,因为它对 SEO 不友好,且首次内容绘制(FCP)时间较长。
- 适用于高度交互式、不依赖 SEO 的内部工具或小部件。
React HydrationNext.js 将服务端生成的、带有正确内容的 HTML 结构,在客户端用 React”接管”,使其变得可交互。自动发生。Server Component 生成的 HTML 会被 Client Component Hydrate。自动发生。getStaticProps/getServerSideProps 生成的 HTML 会被页面组件 Hydrate。- 这是 Next.js 的默认和推荐模式。它结合了服务端渲染的 SEO/性能优势和客户端渲染的交互性。
- Hydration 过程必须保证服务端和客户端渲染的 DOM 结构完全一致,否则会报错。
混合渲染现代 Next.js 应用通常是混合模式:主体内容通过 SSG/SSR 在服务端渲染,而特定的交互元素(如评论框、购物车图标)作为 Client Component 在客户端激活。通过 "use client" 指令精确控制哪些组件需要在客户端运行。通过在 useEffect 中获取数据或使用状态来创建交互式组件。- App Router 的 Server/Client 组件模型使得这种混合渲染更加精细和高效,能显著减少不必要的客户端 JS。

第二部分:核心功能与优化

5. API 路由与后端集成

5.1 Pages Router 中的 API Routes(pages/api/)

API Routes 允许你在 Next.js 应用内部创建 API 端点,而无需设置单独的后端服务器。所有文件放在 pages/api 目录下。

概念/方法说明语法/代码示例注意事项
基础用法pages/api 目录下创建文件,文件路径即为 API 路径。例如,pages/api/hello.js 对应 /api/hellojs<br>// pages/api/hello.js<br>export default function handler(req, res) {<br> res.status(200).json({ message: 'Hello from API Route!' });<br>}<br>- 处理函数接收 req(NextApiRequest)和 res(NextApiResponse)作为参数。
- 必须在函数内调用 res.end()res.json() 等方法来结束响应。
HTTP 方法处理可以根据 req.method 来处理不同的 HTTP 请求方法。js<br>// pages/api/users.js<br>export default function handler(req, res) {<br> if (req.method === 'GET') {<br> // 处理 GET 请求<br> res.status(200).json(users);<br> } else if (req.method === 'POST') {<br> // 处理 POST 请求<br> const newUser = createUser(req.body);<br> res.status(201).json(newUser);<br> } else {<br> res.setHeader('Allow', ['GET', 'POST']);<br> res.status(405).end(`Method ${req.method} Not Allowed`);<br> }<br>}<br>- 需要手动检查 req.method 并分发逻辑。
- 记得为不支持的方法返回 405 Method Not Allowed 状态码。
动态 API 路由使用方括号 [param].js 创建动态 API 端点。js<br>// pages/api/user/[id].js<br>export default function handler(req, res) {<br> const { id } = req.query; // 动态参数通过 req.query.id 获取<br> const user = getUserById(id);<br> res.status(200).json(user);<br>}<br>- 动态参数(如 id)可以通过 req.query 对象访问。
中间件可以使用自定义中间件或第三方中间件(如 cors、body-parser)。js<br>// pages/api/example.js<br>import Cors from 'cors';<br><br>const cors = Cors({ methods: ['GET', 'HEAD'] });<br><br>function runMiddleware(req, res, fn) {<br> return new Promise((resolve, reject) => {<br> fn(req, res, (result) => {<br> if (result instanceof Error) {<br> return reject(result);<br> }<br> return resolve(result);<br> });<br> });<br>}<br><br>export default async function handler(req, res) {<br> await runMiddleware(req, res, cors);<br> res.json({ message: 'Hello Everyone!' });<br>}<br>- Next.js API Routes 基于 Connect/Express,因此兼容许多 Express 中间件。
- 需要自己封装一个 runMiddleware 函数来运行基于回调的中间件。

5.2 App Router 中的 Route Handlers(app/api/route.js)

Route Handlers 是 App Router 中用于创建 API 端点的新方式,它遵循 Web 标准,使用 RequestResponse 对象。

概念/方法说明语法/代码示例注意事项
基础用法app 目录下创建 route.js 文件。该文件所在目录的路径即为 API 路径。例如,app/api/hello/route.js 对应 /api/hellojs<br>// app/api/hello/route.js<br>export async function GET(request) {<br> return Response.json({ message: 'Hello from Route Handler!' });<br>}<br>- 必须导出名为 GETPOSTPUTPATCHDELETE 的异步函数来处理对应的 HTTP 方法。
- 函数接收标准的 Web Request 对象,并必须返回一个 Web Response 对象。
处理不同 HTTP 方法为每个需要支持的 HTTP 方法导出一个独立的函数。js<br>// app/api/users/route.js<br>export async function GET() {<br> const users = await db.users.findMany();<br> return Response.json(users);<br>}<br><br>export async function POST(request) {<br> const data = await request.json();<br> const user = await db.users.create({ data });<br> return Response.json(user, { status: 201 });<br>}<br>- 代码更清晰、模块化,每个方法有自己独立的处理逻辑。
- 如果请求了未定义的方法,框架会自动返回 405 Method Not Allowed。
访问动态参数动态段参数通过第二个参数 contextparams 属性获取。js<br>// app/api/user/[id]/route.js<br>export async function GET(request, { params }) {<br> const { id } = params; // 动态参数直接从 params 解构<br> const user = await db.user.findUnique({ where: { id } });<br> return Response.json(user);<br>}<br>- params 对象包含了路由中所有动态段的值。
访问查询参数和请求体查询参数通过 request.nextUrl.searchParams 获取,请求体通过 request.json()request.text() 等方法获取。js<br>// GET 查询参数<br>const searchParams = request.nextUrl.searchParams;<br>const q = searchParams.get('q');<br><br>// POST 请求体<br>const body = await request.json();<br>- 完全遵循 Web Fetch API 标准,与浏览器环境一致。

5.3 Server Actions(App Router)

Server Actions 是一种革命性的模式,允许你直接从 Client Component 调用在 Server Component 中定义的异步函数,用于执行数据突变(如创建、更新、删除),而无需手动编写 API 路由。

概念/方法说明语法/代码示例注意事项
定义 Server Action在 Server Component 文件中(或单独的 .js 文件中),使用 'use server' 指令标记一个 async 函数。js<br>// app/actions.js<br>'use server';<br><br>import { revalidatePath } from 'next/cache';<br><br>export async function createPost(formData) {<br> const title = formData.get('title');<br> await db.post.create({ data: { title } });<br> revalidatePath('/'); // 重新验证首页缓存<br>}<br>- 必须使用 'use server' 指令。
- 函数可以接收 FormData、原始数据等作为参数。
- 可以直接访问数据库或后端服务。
- 可以调用 revalidatePathrevalidateTag 来更新缓存。
在 Client Component 中调用在 Client Component("use client")中,直接导入并像普通函数一样调用 Server Action。通常与表单的 action 属性或 formAction 属性结合使用。js<br>// app/create-post-form.js<br>"use client";<br>import { createPost } from './actions';<br><br>export default function CreatePostForm() {<br> return (<br> <form action={createPost}><br> <input type="text" name="title" /><br> <button type="submit">Create Post</button><br> </form><br> );<br>}<br>- 这是最简洁的用法,框架会自动处理网络请求、序列化和错误处理。
- 表单提交会自动触发 Server Action。
- 也可以在事件处理器中通过 startTransition 调用以获得更好的用户体验。
使用 useFormStatus 和 useFormStateReact 提供了 useFormStatususeFormState Hook 来处理表单提交的状态(如 pending 状态、错误信息)。js<br>// 在表单内部组件中使用 useFormStatus<br>"use client";<br>import { useFormStatus } from 'react-dom';<br><br>function SubmitButton() {<br> const { pending } = useFormStatus();<br> return (<br> <button type="submit" disabled={pending}><br> {pending ? 'Creating...' : 'Create Post'}<br> </button><br> );<br>}<br>- useFormStatus 必须在 <form> 标签内部的组件中使用。
- useFormState 可以用于管理表单的初始状态和服务器返回的错误状态。
优势- 简化架构:省去了手动创建 API 路由的步骤。
- 类型安全:直接在组件中调用函数,享受完整的 TypeScript 支持。
- 内置优化:自动处理防重放攻击、CSRF 保护等安全措施。
- 无缝集成:与 React 的过渡(Transitions)和状态管理完美结合。
-- Server Actions 是 App Router 推荐的数据突变方式,代表了未来的发展方向。

6. 性能优化

6.1 图像优化(next/image)

next/image 组件是 <img> 元素的扩展,提供了自动化的图像优化功能,如懒加载、响应式调整大小和现代格式转换。

组件/属性语法用途代码示例注意事项
基础用法<Image src="/path.jpg" alt="..." width={500} height={300} />替代原生 <img> 标签,自动优化图像。<Image src="/profile.jpg" alt="Profile Picture" width={400} height={400} />- 必须提供 widthheight 属性(或通过 layout="fill" 配合父容器),以防止累积布局偏移(CLS)。
- 本地图像路径相对于 public 目录。
远程图像<Image src="https://example.com/image.jpg" ... />优化来自外部域名的图像。<Image src="https://picsum.photos/200/300" alt="Remote Image" width={200} height={300} />- 必须在 next.config.js 中配置 images.domainsimages.remotePatterns 来指定允许的域名,以确保安全。
布局模式layout="intrinsic" | "fixed" | "responsive" | "fill"控制图像如何响应其容器大小。<div className="container"><Image src="/hero.jpg" alt="Hero" fill style={{ objectFit: 'cover' }} /></div>- fill:图像填充其父容器,父容器需设置 position: relative
- responsive:图像宽度 100%,高度自动。
- 默认为 intrinsic
懒加载loading="lazy" | "eager"控制图像何时开始加载。<Image src="/image.jpg" loading="lazy" ... />- 默认值为 lazy,即当图像接近视口时才加载。
- 对于首屏关键图像,应设为 eager
优先级priority={true}指示该图像是首屏关键资源,应立即加载。<Image src="/hero.jpg" priority={true} ... />- 通常用于 LCP(最大内容绘制)元素。
- 会覆盖 loading="lazy"

6.2 字体优化(next/font)

next/font 允许你自定义字体,并自动进行优化,包括自托管、子集化和预加载,以消除布局偏移并提升性能。

方法/属性语法用途代码示例注意事项
Google Fontsimport { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
从 Google Fonts 加载字体,并自动优化。js<br>// app/layout.js<br>import { Inter } from 'next/font/google';<br>const inter = Inter({ subsets: ['latin'] });<br><br>export default function RootLayout({ children }) {<br> return (<html className={inter.className}>...</html>);<br>}<br>- 无需在 <head> 中添加 <link> 标签。
- 字体会被自托管,避免了对外部服务的依赖和 FOUT/FOIT 问题。
- 必须指定 subsets
本地字体import localFont from 'next/font/local';
const myFont = localFont({ src: './my-font.woff2' });
优化项目 public 目录中的本地字体文件。js<br>// app/layout.js<br>import localFont from 'next/font/local';<br>const geistSans = localFont({<br> src: '../fonts/GeistVF.woff',<br> variable: '--font-geist-sans',<br>});<br><br>export default function RootLayout({ children }) {<br> return (<html className={geistSans.variable}>...</html>);<br>}<br>- 支持 variable 属性来使用可变字体。
- 字体文件会被自动处理并包含在构建产物中。
应用字体使用 classNamestyle将优化后的字体应用到页面。<body className={inter.className}>...</body><div style={inter.style}>...</div>- 推荐将字体类名应用到根 <html><body> 元素,以全局生效。

6.3 脚本加载优化(next/script)

next/script 组件是对原生 <script> 标签的扩展,提供了对第三方脚本加载策略的精细控制。

组件/属性语法用途代码示例注意事项
加载策略strategy="beforeInteractive" | "afterInteractive" | "lazyOnload"控制脚本的加载和执行时机。<Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXX" strategy="afterInteractive" />- beforeInteractive:在 hydrate React 之前加载,用于关键脚本(如核心框架)。
- afterInteractive(默认):在页面 hydrate 后加载,适用于大多数分析、广告脚本。
- lazyOnload:在浏览器空闲时加载,用于非关键脚本。
事件处理onLoadonError监听脚本加载成功或失败的事件。<Script id="stripe-js" src="https://js.stripe.com/v3/" onLoad={() => console.log('Stripe loaded!')} />- 可用于初始化依赖于该脚本的库。
内联脚本<Script id="...">{console.log('Hello')}</Script>安全地注入内联 JavaScript 代码。<Script id="theme-switcher">{(function() { // Your inline JS here })()}</Script>- Next.js 会自动为内联脚本生成一个唯一的 nonce,以符合 CSP(内容安全策略)。

6.4 动态导入(next/dynamic)

next/dynamicReact.lazy 的增强版,支持对 React 组件(包括带有依赖的组件)进行动态导入和代码分割。

方法/属性语法用途代码示例注意事项
基础动态导入dynamic(() => import('./Component'))将组件的加载延迟到需要时(例如,路由导航后或条件渲染时)。js<br>import dynamic from 'next/dynamic';<br>const DynamicComponent = dynamic(() => import('../components/HeavyComponent'));<br><br>export default function Home() {<br> return (<div><DynamicComponent /></div>);<br>}<br>- 会将 HeavyComponent 及其依赖打包到单独的 JS 文件中。
- 首次渲染时,该组件区域会为空,直到 JS 加载完成。
禁用 SSR{ ssr: false }仅在客户端加载和渲染组件。js<br>const NoSSRComponent = dynamic(() => import('../components/ClientOnlyMap'), {<br> ssr: false,<br> loading: () => <p>Loading map...</p>,<br>});<br>- 非常适合依赖浏览器 API(如 window)或大型客户端库(如地图、图表)的组件。
- 可以配合 loading 属性提供加载状态。
命名导出dynamic(() => import('./module').then(mod => mod.NamedExport))动态导入模块中的命名导出。js<br>const DynamicNamedComponent = dynamic(() => import('../components').then(mod => mod.NamedComponent));<br>- 需要使用 .then() 来提取命名导出。

6.5 路由预取

Next.js 会自动对视口内的 Link 组件进行路由预取,即提前获取目标页面所需的 JavaScript 代码和数据,从而实现近乎瞬间的页面切换。

概念/方法说明触发方式注意事项
自动预取Link 组件进入浏览器视口(通常是距离视口底部 200px 以内)时,Next.js 会在后台静默下载目标页面的必要资源。使用 next/linkLink 组件。- 仅在生产环境(next start)下启用。
- 仅适用于 App Router 的静态路由和 Pages Router。动态路由(如 [id])不会被自动预取,因为无法预知所有可能的路径。
- 可以通过 prefetch={false} 禁用单个 Link 的预取。
手动预取在需要时,通过 useRouter Hook 手动触发预取。js<br>const router = useRouter();<br>useEffect(() => {<br> router.prefetch('/dashboard');<br>}, []);<br>- 适用于需要在特定时机(如用户悬停在按钮上)预取动态路由或其他非自动预取路由的场景。
- router.prefetch() 返回一个 Promise,可以用于更复杂的逻辑。
优势- 极大提升用户体验:导航几乎是即时的。
- 智能资源管理:只预取必要的资源,且在浏览器空闲时进行,不影响当前页面性能。
-- 这是 Next.js 开箱即用的核心性能特性之一。

7. 样式与资产

7.1 CSS Modules

CSS Modules 是一种将 CSS 类名局部作用域化的技术,避免全局样式冲突。Next.js 开箱即用支持。

概念/方法说明语法/代码示例注意事项
文件命名CSS 文件必须以 .module.css 结尾。Button.module.css- 这是 Next.js 识别 CSS Modules 文件的约定。
导入与使用在组件中导入该 CSS 文件,它会返回一个对象,其键为原始类名,值为唯一的哈希化类名。js<br>// Button.js<br>import styles from './Button.module.css';<br><br>export default function Button() {<br> return (<button className={styles.primary}>Click me</button>);<br>}<br>

css<br>/* Button.module.css */<br>.primary {<br> background: blue;<br> color: white;<br>}<br>
- 导入的对象(如 styles)将 .primary 映射到类似 Button_primary__aBc12 的唯一类名。
- 这确保了 .primary 样式只作用于这个特定的 Button 组件,不会影响其他组件。
组合类名可以使用 composes 关键字来复用其他类或外部 CSS 文件中的样式。css<br>/* shared.css */<br>.error { color: red; }<br><br>/* Component.module.css */<br>.special {<br> composes: error from './shared.css';<br> font-weight: bold;<br>}<br>- composes 允许构建可复用的样式原子。

7.2 Tailwind CSS 集成

Next.js 提供了与 Tailwind CSS 的一流集成,只需简单配置即可使用。

概念/步骤说明操作细节注意事项
安装依赖安装 Tailwind CSS 及其对等依赖。bash<br>npm install -D tailwindcss postcss autoprefixer<br>npx tailwindcss init -p<br>- postcssautoprefixer 是 Tailwind 正常工作所必需的。
配置 tailwind.config.js告诉 Tailwind 在哪些文件中扫描类名。js<br>/** @type {import('tailwindcss').Config} */<br>module.exports = {<br> content: [<br> "./app/**/*.{js,ts,jsx,tsx}",<br> "./pages/**/*.{js,ts,jsx,tsx}",<br> "./components/**/*.{js,ts,jsx,tsx}",<br> ],<br> theme: { extend: {} },<br> plugins: [],<br>}<br>- content 数组至关重要,它指定了 Tailwind 应该扫描的文件路径,以确定哪些类名在使用,从而进行摇树优化(Tree Shaking)。
引入 Tailwind 指令在全局 CSS 文件中注入 Tailwind 的 base、components 和 utilities 样式。css<br>/* app/globals.css */<br>@tailwind base;<br>@tailwind components;<br>@tailwind utilities;<br>- 这个全局 CSS 文件需要在根布局(app/layout.js)中导入。
使用类名直接在组件的 className 属性中使用 Tailwind 的工具类。js<br>// app/page.js<br>export default function Home() {<br> return (<main className="flex min-h-screen flex-col p-24">...</main>);<br>}<br>- Tailwind 的 JIT(即时编译)模式会根据 content 中的文件按需生成 CSS,保证最终产物极小。

7.3 Sass 支持

Next.js 内置了对 Sass(.scss.sass)的支持,无需额外配置。

概念/方法说明语法/代码示例注意事项
全局 Sass直接导入 .scss.sass 文件作为全局样式。js<br>// app/layout.js<br>import '../styles/globals.scss';<br>

scss<br>/* styles/globals.scss */<br>$primary-color: #3b82f6;<br>body {<br> color: $primary-color;<br>}<br>
- 全局 Sass 文件中的变量、混合宏(mixins)和规则会对整个应用生效。
Sass Modules通过将文件命名为 .module.scss.module.sass 来使用模块化 Sass。scss<br>// Button.module.scss<br>$button-padding: 1rem;<br>.primary {<br> padding: $button-padding;<br> background: blue;<br>}<br>

js<br>// Button.js<br>import styles from './Button.module.scss';<br>... className={styles.primary}<br>
- 结合了 Sass 的强大功能(嵌套、变量、函数等)和 CSS Modules 的局部作用域优势。
自定义配置如需自定义 Sass 编译器选项(如 includePaths),可在 next.config.js 中配置。js<br>// next.config.js<br>const path = require('path');<br>module.exports = {<br> sassOptions: {<br> includePaths: [path.join(__dirname, 'styles')],<br> },<br>};<br>- 大多数情况下,开箱即用的功能已足够,无需自定义配置。

7.4 静态资源处理

Next.js 提供了简单的方式来处理和引用静态资源,如图片、字体、PDF 等。

概念/方法说明语法/代码示例注意事项
public 目录所有放在项目根目录 public 文件夹下的文件都会被直接映射到服务器根路径 /- 文件:public/my-image.png
- 引用:<img src="/my-image.png" alt="..." /><Image src="/my-image.png" ... />
- 这是存放静态资源的标准位置。
- 无法在此目录下使用 requireimport
导入静态资源对于较小的资源(如 SVG 图标),可以直接在 JavaScript/TypeScript 文件中导入。js<br>import logoImg from '../assets/logo.svg';<br><br>function Header() {<br> return <img src={logoImg.src} alt="Logo" />;<br>}<br>- Next.js 会将导入的文件处理并输出到构建目录,并提供一个包含 srcheight/width 等元数据的对象。
- 这种方式适用于需要在 JS 逻辑中操作的资源。
next/image 与静态资源next/image 组件的 src 属性可以直接使用 public 目录下的路径。<Image src="/images/photo.jpg" width={500} height={500} alt="Photo" />- 这是处理 public 目录中图片的推荐方式,因为它会自动应用图像优化。
next/font 与本地字体本地字体文件应放在 public 目录或项目内部,并通过 next/fontlocalFont API 引入。ts<br>// fonts.ts<br>import localFont from 'next/font/local';<br>export const myFont = localFont({ src: '../public/fonts/my-font.woff2' });<br>- 使用 next/font 是处理字体的最佳实践,它能自动优化字体加载。

8. 中间件(Middleware)

8.1 中间件基础概念与执行时机

中间件是一个在请求-响应循环中运行的函数,它可以在请求到达最终路由处理器(如 Page 或 Route Handler)之前或之后执行代码。

概念/方法说明语法/代码示例注意事项
核心思想在服务器接收到请求后、页面或 API 路由渲染前,提供一个拦截和处理请求的机会。js<br>// middleware.js<br>import { NextResponse } from 'next/server';<br><br>export function middleware(request) {<br> // 在此处编写逻辑<br> return NextResponse.next(); // 继续到下一个中间件或路由<br>}<br>- 中间件文件必须命名为 middleware.jsmiddleware.ts,并放在项目根目录或 src 目录下。
- 它运行在 Edge Runtime 上,因此必须使用 Web 标准 API(如 RequestResponse),不能使用 Node.js 特有的模块(如 fspath)。
执行时机在任何缓存(如 SSG、ISR)之前执行。这意味着即使是静态生成的页面,每次请求也会先经过中间件。-- 这使得中间件非常适合需要基于请求头(如 Cookie、Authorization)进行决策的场景,例如认证和重定向。
- 因为它在缓存前运行,所以可以修改请求以影响后续的缓存键。
可用对象request(NextRequest)、event(FetchEvent)js<br>export function middleware(request, event) {<br> const url = request.nextUrl;<br> const ip = request.ip; // 需要配置<br> const geo = request.geo; // 需要配置<br>}<br>- request.nextUrl 是一个增强版的 URL 对象,包含了更多关于 Next.js 路由的信息。
- ipgeo 等信息需要部署平台(如 Vercel)支持,并可能需要额外配置。
返回值必须返回一个 Response 对象。js<br>// 1. 继续执行<br>return NextResponse.next();<br><br>// 2. 重定向<br>return NextResponse.redirect(new URL('/login', request.url));<br><br>// 3. 自定义响应<br>return new Response('Unauthorized', { status: 401 });<br>- 如果不显式返回,中间件会默认继续执行(NextResponse.next())。

8.2 常见用例:认证、重定向、A/B 测试、国际化

中间件是实现这些横切关注点的理想场所。

用例操作细节代码示例注意事项
认证(Authentication)检查请求是否包含有效的认证令牌(如 Cookie 或 Authorization Header),如果没有则重定向到登录页。js<br>export function middleware(request) {<br> const token = request.cookies.get('auth-token')?.value;<br> const isLoggedIn = !!token;<br><br> // 保护 /dashboard 路径<br> if (request.nextUrl.pathname.startsWith('/dashboard') && !isLoggedIn) {<br> return NextResponse.redirect(new URL('/login', request.url));<br> }<br><br> // 阻止已登录用户访问登录页<br> if (request.nextUrl.pathname === '/login' && isLoggedIn) {<br> return NextResponse.redirect(new URL('/dashboard', request.url));<br> }<br><br> return NextResponse.next();<br>}<br>- 认证逻辑应尽可能轻量,因为每次请求都会执行。
- 敏感操作(如验证 JWT 签名)应在后端 API 中进行,中间件只做初步检查。
重定向(Redirects)基于各种条件(如旧 URL、地域、设备类型)将用户重定向到新 URL。js<br>// 旧 URL 重定向<br>if (request.nextUrl.pathname === '/old-page') {<br> return NextResponse.redirect(new URL('/new-page', request.url));<br>}<br><br>// 地域重定向<br>const { country } = request.geo;<br>if (country === 'US') {<br> return NextResponse.redirect(new URL('/us', request.url));<br>}<br>- NextResponse.redirect() 会发送一个 HTTP 3xx 状态码,导致浏览器 URL 发生变化。
A/B 测试根据 Cookie 或其他标识符,将用户分配到不同的实验组,并重写 URL 以加载不同的页面版本。js<br>export function middleware(request) {<br> let variant = request.cookies.get('ab-test')?.value;<br><br> if (!variant) {<br> // 随机分配 A 或 B<br> variant = Math.random() < 0.5 ? 'A' : 'B';<br> const response = NextResponse.next();<br> response.cookies.set('ab-test', variant);<br> return response;<br> }<br><br> // 重写 URL 以指向特定变体的页面<br> const url = request.nextUrl.clone();<br> url.pathname = `/variants/${variant}${url.pathname}`;<br> return NextResponse.rewrite(url);<br>}<br>- NextResponse.rewrite() 用于在服务器内部重写请求,对用户是透明的(URL 不变)。
- 需要确保 /variants/A/.../variants/B/... 路径存在。
国际化(i18n)根据用户的 Accept-Language 头或 Cookie,自动将用户重定向到其偏好的语言版本。js<br>const PUBLIC_ROUTES = ['/', '/about'];<br>const LOCALES = ['en', 'es', 'fr'];<br><br>export function middleware(request) {<br> const { pathname } = request.nextUrl;<br><br> // 如果路径已经包含语言前缀,则跳过<br> if (LOCALES.some(loc => pathname.startsWith(`/${loc}`))) {<br> return NextResponse.next();<br> }<br><br> // 如果是公共资源或 API,也跳过<br> if (pathname.startsWith('/_next') || pathname.startsWith('/api')) {<br> return NextResponse.next();<br> }<br><br> // 获取用户首选语言<br> const locale = request.headers.get('accept-language')?.split(',')?.[0]?.split('-')?.[0];<br> const safeLocale = LOCALES.includes(locale) ? locale : 'en';<br><br> return NextResponse.redirect(new URL(`/${safeLocale}${pathname}`, request.url));<br>}<br>- 中间件是处理国际化的高效方式,因为它在缓存层之前运行。
- 注意避免对已包含语言前缀的路径进行重复重定向,防止重定向循环。

8.3 中间件配置与匹配器

通过配置,可以精确控制中间件在哪些路由上运行,避免不必要的执行。

配置项说明语法/代码示例注意事项
matcher定义中间件应该运行的路由模式。可以是字符串、字符串数组或正则表达式。js<br>// middleware.js<br>export const config = {<br> matcher: [<br> /*<br> * 匹配所有路由,除了:<br> * - 以 _next/ 开头的(Next.js 内部资源)<br> * - 以 /api/ 开头的(API 路由)<br> * - 包含 . 的(通常是静态文件,如 favicon.ico)<br> */<br> '/((?!_next|api|.*\\..*).*)',<br> ],<br>};<br>- matcher 极大地提高了中间件的性能,确保它只在需要时执行。
- 正则表达式是配置精确匹配模式的首选方式。
skipMiddlewareUrlNormalize(高级)禁用 Next.js 对 URL 的标准化处理,允许匹配原始 URL。js<br>export const config = {<br> skipMiddlewareUrlNormalize: true,<br> matcher: '/api/:path*',<br>};<br>- 在极少数需要匹配编码后 URL 的场景下使用。通常不需要。
skipTrailingSlashRedirect(高级)禁用 Next.js 的尾部斜杠重定向,以便在中间件中处理。js<br>export const config = {<br> skipTrailingSlashRedirect: true,<br>};<br>- 如果你需要自定义尾部斜杠的行为,可以使用此选项。

第三部分:高级主题与工程化

9. 认证与安全(Authentication & Security)

9.1 使用 NextAuth.js / Auth.js

NextAuth.js(现已升级为 Auth.js)是一个灵活、开源的身份验证库,专为 Next.js 设计,支持多种 OAuth 提供商、数据库适配器和无状态 JWT 会话。

概念/方法说明语法/代码示例注意事项
基础配置auth.ts(App Router)或 pages/api/auth/[...nextauth].js(Pages Router)中创建身份验证处理程序。ts<br>// auth.ts (App Router)<br>import NextAuth from "next-auth";<br>import GitHub from "next-auth/providers/github";<br><br>export const { handlers, auth, signIn, signOut } = NextAuth({<br> providers: [GitHub],<br> // ...其他配置<br>});<br>- App Router 推荐使用 auth.ts 的新集成方式。
- 必须设置 NEXTAUTH_SECRET 环境变量用于加密。
Providers(提供者)配置身份验证提供者,如 OAuth(Google、GitHub)、Credentials(用户名/密码)、Email/Passwordless 等。ts<br>// 使用 Credentials Provider<br>import Credentials from "next-auth/providers/credentials";<br>import bcrypt from "bcryptjs";<br><br>export const { handlers, auth } = NextAuth({<br> providers: [<br> Credentials({<br> async authorize(credentials) {<br> // 1. 在数据库中查找用户<br> const user = await db.user.findUnique({ where: { email: credentials.email } });<br> // 2. 验证密码<br> if (user && await bcrypt.compare(credentials.password, user.password)) {<br> return { id: user.id, email: user.email };<br> }<br> return null; // 认证失败<br> },<br> }),<br> ],<br>});<br>- Credentials Provider 不支持 App Router 的 GET 请求(如 signIn() 重定向),通常需要自定义登录表单并通过 POST 调用。
Session 管理Auth.js 支持两种会话策略:jwt(默认)和 databasets<br>// 配置数据库会话<br>export const { handlers, auth } = NextAuth({<br> session: { strategy: "database" },<br> adapter: PrismaAdapter(prisma), // 需要数据库适配器<br> providers: [...],<br>});<br>- JWT:无状态,会话数据存储在客户端 Cookie 的 JWT 中。适合 Serverless 环境。
- Database:有状态,会话数据存储在数据库中,Cookie 只存 Session ID。适合需要精确控制会话(如强制登出所有设备)的场景。
保护路由(App Router)在 Server Component 中,通过 auth() 函数获取当前会话。tsx<br>// app/dashboard/page.tsx<br>import { auth } from "@/auth";<br><br>export default async function Dashboard() {<br> const session = await auth();<br> if (!session) {<br> // 重定向到登录页<br> redirect("/login");<br> }<br> return <div>Welcome, {session.user?.name}!</div>;<br>}<br>- 这是在 Server Component 中检查认证状态的标准方式。
- 如果需要在 Client Component 中访问会话,应使用 useSession() Hook。

9.2 JWT 认证策略

当使用 Auth.js 的 jwt 策略或自建 JWT 认证系统时,需要理解其工作原理和最佳实践。

概念/方法说明语法/代码示例注意事项
JWT 结构JWT(JSON Web Token)由三部分组成:Header、Payload、Signature。Payload 包含声明(如 sub 用户ID、exp 过期时间)。js<br>// 示例 Payload<br>{<br> "sub": "1234567890",<br> "name": "John Doe",<br> "iat": 1516239022,<br> "exp": 1516242622<br>}<br>- 切勿在 JWT Payload 中存储敏感信息,因为它是 Base64 编码,可以被轻易解码。
自定义 JWT 回调在 Auth.js 中,可以通过 callbacks.jwt 来修改存储在 JWT 中的数据。ts<br>// auth.ts<br>export const { handlers, auth } = NextAuth({<br> callbacks: {<br> async jwt({ token, user }) {<br> // 初始登录时,user 对象存在<br> if (user) {<br> token.id = user.id;<br> token.role = user.role; // 从数据库获取角色<br> }<br> return token;<br> },<br> async session({ session, token }) {<br> // 将 JWT 中的数据传递给前端 session 对象<br> session.user.id = token.id;<br> session.user.role = token.role;<br> return session;<br> },<br> },<br> providers: [...],<br>});<br>- jwt 回调在每次创建或更新 JWT 时调用。
- session 回调在每次前端请求会话时调用,用于将 JWT 数据安全地暴露给客户端。
手动验证 JWT在 API Route 或 Route Handler 中,如果未使用 Auth.js,需要手动从请求头或 Cookie 中提取并验证 JWT。js<br>// utils/auth.js<br>import jwt from 'jsonwebtoken';<br><br>export function verifyToken(token) {<br> try {<br> return jwt.verify(token, process.env.JWT_SECRET);<br> } catch (error) {<br> return null; // 无效或已过期<br> }<br>}<br><br>// app/api/protected/route.js<br>import { verifyToken } from '@/utils/auth';<br><br>export async function GET(request) {<br> const token = request.cookies.get('auth-token')?.value;<br> const payload = verifyToken(token);<br> if (!payload) {<br> return new Response('Unauthorized', { status: 401 });<br> }<br> // ...执行受保护的操作<br>}<br>- 强烈建议使用成熟的库(如 jsonwebtoken)来处理 JWT,避免自己实现加密逻辑。
- 始终验证签名和过期时间(exp)。

9.3 与中间件集成进行保护

中间件是实施全站范围访问控制的理想位置,因为它在任何页面渲染之前运行。

概念/方法说明语法/代码示例注意事项
在中间件中验证会话使用 Auth.js 提供的 auth 函数(或自定义验证逻辑)在中间件中检查用户是否已认证。ts<br>// middleware.ts<br>import { auth } from "@/auth";<br>import { NextResponse } from "next/server";<br><br>export async function middleware(request) {<br> const session = await auth();<br><br> const isLoggedIn = !!session;<br> const isOnProtectedRoute = request.nextUrl.pathname.startsWith("/dashboard");<br><br> if (isOnProtectedRoute && !isLoggedIn) {<br> return NextResponse.redirect(new URL("/login", request.url));<br> }<br><br> return NextResponse.next();<br>}<br>- 这是保护一组路由最高效的方式,避免了在每个页面组件中重复检查逻辑。
- auth() 函数在中间件上下文中也能正常工作。
基于角色的访问控制(RBAC)在中间件中读取 JWT 或会话中的角色信息,并据此决定访问权限。ts<br>// middleware.ts<br>export async function middleware(request) {<br> const session = await auth();<br> const userRole = session?.user?.role;<br><br> if (request.nextUrl.pathname.startsWith("/admin") && userRole !== "ADMIN") {<br> return NextResponse.redirect(new URL("/unauthorized", request.url));<br> }<br><br> return NextResponse.next();<br>}<br>- 确保角色信息是通过安全的方式(如数据库查询)放入会话或 JWT 中的,不能由客户端篡改。
中间件配置匹配器使用 matcher 精确指定中间件应保护的路由,避免对公共路由(如 //login)进行不必要的检查。ts<br>// middleware.ts<br>export const config = {<br> matcher: ["/dashboard/:path*", "/admin/:path*"],<br>};<br><br>export async function middleware(request) { ... }<br>- 性能关键:只让中间件在真正需要保护的路由上运行,可以显著减少延迟。

10. 国际化(i18n)

10.1 App Router 国际化方案

App Router 采用了一种基于文件系统约定的、更灵活的国际化方法,取代了 Pages Router 的内置配置。

概念/方法说明语法/代码示例注意事项
文件夹结构app 目录下,为每种语言创建一个以语言代码命名的子文件夹(如 enfr)。使用 [locale] 动态段来捕获当前语言。<br>app/<br>├── [locale]/<br>│ ├── page.tsx<br>│ └── layout.tsx<br>├── en/<br>│ └── ... (可选,用于静态生成)<br>└── fr/<br> └── ... (可选,用于静态生成)<br>- 核心是 [locale] 动态段。所有需要国际化的页面都应放在这个动态段下。
- en/fr/ 等静态文件夹是可选的,主要用于 SSG 时预渲染特定语言版本。
generateStaticParams用于在构建时为所有支持的语言生成静态页面。tsx<br>// app/[locale]/page.tsx<br>import { locales } from '@/config';<br><br>export async function generateStaticParams() {<br> return locales.map((locale) => ({ locale }));<br>}<br><br>export default function Home({ params: { locale } }: { params: { locale: string } }) {<br> // 根据 locale 渲染内容<br>}<br>- 这是实现 SSG 国际化的关键函数。
- locales 是一个包含所有支持语言代码的数组,例如 ['en', 'fr']
获取当前语言在 Server Components 中,通过路由参数 params.locale 获取当前语言。tsx<br>// app/[locale]/layout.tsx<br>export default function RootLayout({<br> children,<br> params: { locale }<br>}: {<br> children: React.ReactNode;<br> params: { locale: string };<br>}) {<br> return (<html lang={locale}>...</html>);<br>}<br>- params.locale 是由 [locale] 动态段自动注入的。
链接(Link)使用 next/link 时,必须将完整的本地化路径(包含语言前缀)传递给 hreftsx<br>// 导航到法语首页<br><Link href="/fr">Français</Link><br><br>// 在组件内部动态构建链接<br>const pathnames = { '/': '/', '/about': '/about' };<br>const localizedPathname = `/${locale}${pathnames[pathname]}`;<br>- 不能只传递 /about,因为这会丢失语言上下文。必须构建为 /{locale}/about

10.2 Pages Router 内置 i18n 路由

Pages Router 提供了一个基于 next.config.js 配置的内置 i18n 路由系统。

概念/方法说明语法/代码示例注意事项
配置 next.config.jsnext.config.js 中定义 i18n 对象,指定支持的语言和默认语言。js<br>// next.config.js<br>module.exports = {<br> i18n: {<br> locales: ['en-US', 'fr', 'nl-NL'],<br> defaultLocale: 'en-US',<br> // domains: [...] // 可选,用于基于域名的区域设置<br> },<br>};<br>- 此功能仅在 Pages Router 中可用,App Router 不再支持。
- 配置后,Next.js 会自动处理带有语言前缀的路由(如 /fr/about)。
自动重定向当用户访问根路径 / 时,Next.js 会根据其 Accept-Language 请求头自动重定向到最匹配的语言版本(如 /en-US)。-- 此行为可以通过 localeDetection: false 在配置中禁用。
在页面中获取语言在 Pages Router 的页面组件中,可以通过 useRouter Hook 获取当前语言。js<br>// pages/index.js<br>import { useRouter } from 'next/router';<br><br>export default function Home() {<br> const router = useRouter();<br> const { locale, locales, defaultLocale } = router;<br> // ...<br>}<br>- router.locale 返回当前激活的语言。
- router.locales 返回所有支持的语言列表。
链接(Link)使用 next/link 时,可以利用 locale 属性来切换语言,而无需手动构建完整 URL。<Link href="/about" locale="fr">À propos</Link>- 这是 Pages Router i18n 的便利之处,Link 组件会自动处理 URL 前缀。

10.3 翻译管理与切换

无论使用哪种路由方案,都需要一个强大的翻译管理系统来处理多语言内容。

概念/方法说明语法/代码示例注意事项
翻译文件结构通常为每种语言创建一个 JSON 或 JS 文件,其中包含键值对形式的翻译。json<br>// public/locales/en/common.json<br>{ "welcome": "Welcome", "about": "About" }<br><br>// public/locales/fr/common.json<br>{ "welcome": "Bienvenue", "about": "À propos" }<br>- 组织良好的翻译文件(按功能或页面分组)对于大型项目至关重要。
加载翻译在 Server Component 中,根据当前 locale 动态导入对应的翻译文件。ts<br>// lib/i18n.ts<br>import en from '../public/locales/en/common.json';<br>import fr from '../public/locales/fr/common.json';<br><br>const translations = { en, fr };<br><br>export const getTranslations = (locale: string) => translations[locale];<br>

tsx<br>// app/[locale]/page.tsx<br>import { getTranslations } from '@/lib/i18n';<br>export default function Home({ params: { locale } }) {<br> const t = getTranslations(locale);<br> return <h1>{t.welcome}</h1>;<br>}<br>
- 在 Server Component 中加载可以确保翻译内容直接包含在 HTML 中,有利于 SEO 和首屏性能。
- 避免在 Client Component 中加载整个翻译包,以减小客户端 bundle 体积。
语言切换器创建一个 UI 组件,允许用户更改其首选语言,并重定向到新语言的对应页面。tsx<br>// components/LanguageSwitcher.tsx<br>'use client';<br>import { usePathname, useRouter } from 'next/navigation';<br><br>export default function LanguageSwitcher({ currentLocale }: { currentLocale: string }) {<br> const router = useRouter();<br> const pathname = usePathname();<br><br> const switchLanguage = (newLocale: string) => {<br> // 移除旧的语言前缀并添加新的<br> const newPath = pathname.replace(/^\/[^\/]+/, `/${newLocale}`);<br> router.push(newPath);<br> };<br><br> return (<select onChange={(e) => switchLanguage(e.target.value)} value={currentLocale}><br> <option value="en">English</option><br> <option value="fr">Français</option><br> </select>);<br>}<br>- 在 App Router 中,需要手动处理 URL 的语言前缀替换。
- 切换后应使用 router.pushrouter.replace 进行导航。
第三方库集成可以集成如 react-intli18next 等成熟的国际化库,它们提供了更多高级功能(如复数、日期/数字格式化)。-- 对于复杂需求(如 RTL 支持、复杂的格式化规则),这些库可能是更好的选择,但会增加项目复杂度。对于简单项目,自定义解决方案通常足够。

11. 测试(Testing)

11.1 单元测试与组件测试(Jest + React Testing Library)

单元测试和组件测试用于验证单个函数或 React 组件在隔离环境下的行为是否正确。

概念/方法说明语法/代码示例注意事项
安装依赖安装 Jest、React Testing Library 及其相关依赖。bash<br>npm install --save-dev jest @testing-library/react @testing-library/jest-dom @types/jest identity-obj-proxy<br>- identity-obj-proxy 用于模拟 CSS Modules 和静态资源导入,避免测试失败。
Jest 配置创建 jest.config.js 文件来配置测试环境。js<br>// jest.config.js<br>const nextJest = require('next/jest');<br><br>const createJestConfig = nextJest({ dir: './' });<br><br>const customJestConfig = {<br> setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],<br> moduleNameMapper: {<br> // 处理 CSS Modules<br> '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',<br> },<br> testEnvironment: 'jsdom',<br>};<br><br>module.exports = createJestConfig(customJestConfig);<br>- next/jest 提供了对 Next.js 特性的内置支持,如路径别名、CSS 处理。
- testEnvironment: 'jsdom' 为测试组件提供了浏览器环境的模拟。
设置文件(jest.setup.js)在此文件中导入全局的测试工具和匹配器。js<br>// jest.setup.js<br>import '@testing-library/jest-dom/extend-expect';<br>- 这使得可以在测试中使用 expect(element).toBeInTheDocument() 等便捷的断言。
编写组件测试使用 render 渲染组件,并使用 screen 查询元素进行断言。tsx<br>// __tests__/Button.test.tsx<br>import { render, screen } from '@testing-library/react';<br>import Button from '@/components/Button';<br><br>describe('Button', () => {<br> it('renders the correct text', () => {<br> render(<Button>Click me</Button>);<br> expect(screen.getByText('Click me')).toBeInTheDocument();<br> });<br><br> it('calls onClick when clicked', () => {<br> const handleClick = jest.fn();<br> render(<Button onClick={handleClick}>Click me</Button>);<br> screen.getByText('Click me').click();<br> expect(handleClick).toHaveBeenCalledTimes(1);<br> });<br>});<br>- 查询优先级:getByRole > getByLabelText > getByPlaceholderText > getByText > getByDisplayValue。这能写出更健壮、可访问性友好的测试。
模拟 Next.js 组件对于 next/imagenext/link 等内置组件,可能需要创建 mock。js<br>// __mocks__/next/image.js<br>export default function ImageMock({ src, alt }) {<br> return <img src={src} alt={alt} />;<br>}<br>- 将 mock 文件放在 __mocks__/next/ 目录下,Jest 会自动使用它们。

11.2 端到端测试(Cypress、Playwright)

端到端(E2E)测试模拟真实用户在浏览器中的操作,从头到尾验证整个应用流程。

概念/工具说明配置/代码示例注意事项
Playwright(推荐)由 Microsoft 开发的现代 E2E 测试工具,速度快、API 简洁、支持多浏览器。1. 安装:npm init playwright@latest
2. 测试示例(tests/example.spec.ts):
ts<br>import { test, expect } from '@playwright/test';<br><br>test('has title', async ({ page }) => {<br> await page.goto('/');<br> await expect(page).toHaveTitle(/Next\.js/);<br>});<br><br>test('get started link', async ({ page }) => {<br> await page.goto('/');<br> await page.getByRole('link', { name: 'Get started' }).click();<br> await expect(page).toHaveURL(/\/docs/);<br>});<br>
- 官方集成:Next.js 团队与 Playwright 合作紧密,提供了优秀的开箱即用体验。
- 并行执行:默认并行运行测试,速度极快。
- 自动等待:内置智能等待机制,减少 flaky tests。
Cypress成熟的 E2E 测试框架,拥有强大的开发者工具和社区。1. 安装:npm install cypress --save-dev
2. 配置(cypress.config.ts):
ts<br>import { defineConfig } from "cypress";<br>export default defineConfig({<br> e2e: {<br> baseUrl: 'http://localhost:3000',<br> setupNodeEvents(on, config) {},<br> },<br>});<br>
3. 测试示例(cypress/e2e/spec.cy.js):
js<br>describe('Navigation', () => {<br> it('should navigate to about page', () => {<br> cy.visit('/');<br> cy.contains('About').click();<br> cy.url().should('include', '/about');<br> });<br>});<br>
- 时间旅行调试:Cypress 的开发者工具允许回溯每一步操作的状态,调试体验极佳。
- 启动服务器:通常需要配合 start-server-and-test 包,在运行测试前启动 Next.js 开发服务器。
通用最佳实践- 使用语义化查询(如 getByRole)。
- 避免过度断言。
- 为测试创建专用的、可预测的数据(如使用测试数据库)。
-- E2E 测试较慢且脆弱,应聚焦于关键用户旅程,而非覆盖所有细节。

11.3 API 路由与中间件测试

测试 API 路由和中间件需要模拟请求-响应周期,并验证其输出。

概念/方法说明语法/代码示例注意事项
测试 API 路由(Pages Router)使用 node-mocks-httpsupertest 来模拟 reqres 对象。ts<br>// __tests__/api/hello.test.ts<br>import { createMocks } from 'node-mocks-http';<br>import handler from '@/pages/api/hello';<br><br>describe('/api/hello', () => {<br> it('returns a JSON response', async () => {<br> const { req, res } = createMocks({ method: 'GET' });<br> await handler(req, res);<br> expect(res._getStatusCode()).toBe(200);<br> expect(JSON.parse(res._getData())).toEqual({ name: 'John Doe' });<br> });<br>});<br>- Pages Router 的 API 路由是传统的 (req, res) 函数,易于模拟。
测试 Route Handlers(App Router)App Router 的 Route Handlers 是标准的 Web Request -> Response 函数,可以直接调用。ts<br>// __tests__/api/route.test.ts<br>import { GET } from '@/app/api/route';<br><br>describe('/api/route', () => {<br> it('returns a JSON response', async () => {<br> const request = new Request('http://localhost:3000/api/route');<br> const response = await GET(request);<br> expect(response.status).toBe(200);<br> const data = await response.json();<br> expect(data).toEqual({ message: 'Hello World' });<br> });<br>});<br>- 这是 App Router 的巨大优势:Route Handlers 是纯函数,不依赖 Next.js 特定的 req/res 对象,因此测试极其简单直接。
测试中间件中间件也是 Request -> Response 函数,测试方式与 Route Handlers 类似。ts<br>// __tests__/middleware.test.ts<br>import { middleware } from '@/middleware';<br><br>describe('middleware', () => {<br> it('redirects unauthenticated users', async () => {<br> const request = new Request('http://localhost:3000/dashboard');<br> // 模拟未认证的请求(无 cookie)<br> const response = await middleware(request);<br> expect(response.status).toBe(307); // Temporary Redirect<br> expect(response.headers.get('Location')).toBe('/login');<br> });<br>});<br>- 为了测试依赖外部状态(如数据库、Auth.js 会话)的中间件,需要使用 Mocking(如 vi.mock in Vitest、jest.mock in Jest)来模拟这些依赖。
环境变量处理在测试中,应为敏感或特定的环境变量提供测试值。js<br>// 在测试文件顶部或通过测试 runner 配置<br>process.env.MY_SECRET = 'test-secret';<br>- 避免在测试中使用真实的生产环境变量。

12. 部署(Deployment)

12.1 Vercel 一键部署(官方推荐)

Vercel 是 Next.js 的创建者 Vercel 公司提供的平台,为 Next.js 应用提供了深度集成和最佳体验。

概念/操作说明操作细节注意事项
Git 集成将项目连接到 GitHub、GitLab 或 Bitbucket 仓库。1. 在 Vercel Dashboard 中点击 “Add New” > “Project”。
2. 选择你的 Git 仓库。
3. Vercel 会自动检测是 Next.js 项目并应用默认设置。
- 零配置:大多数 Next.js 功能(如 ISR、Edge Functions、Middleware)在 Vercel 上开箱即用,无需额外配置。
自动构建与部署每次向指定分支(如 main)推送代码时,Vercel 会自动触发构建和部署。-- Preview Deployments:为每个 Pull Request 自动生成一个唯一的预览 URL,方便团队审查。
环境变量管理在 Vercel 项目设置中安全地存储环境变量。1. 进入项目 > Settings > Environment Variables。
2. 添加变量,可选择作用域(Production、Preview、Development)。
3. 支持纯文本和加密的 Secret。
- 敏感信息:API 密钥、数据库密码等必须作为 Secret 存储,它们不会暴露在客户端代码中。
边缘网络应用在全球 Vercel Edge Network 上运行,提供低延迟。-- Serverless Functions & Edge Functions:API 路由和中间件会自动部署到离用户最近的边缘节点。

12.2 其他平台部署(Netlify、AWS、Docker)

虽然 Vercel 是首选,但 Next.js 也可以部署到其他平台。

平台部署方式操作细节注意事项
Netlify类似 Vercel,支持 Git 集成和 Serverless Functions。1. 连接 Git 仓库。
2. 构建命令通常为 next build,发布目录为 .next
3. 需要配置 _redirects 文件来处理 SPA 路由或重定向。
- 功能限制:对 Next.js 最新特性(如 App Router 的全功能、Middleware)的支持可能不如 Vercel 及时和完整。
- 边缘处理:使用 Netlify Edge Handlers 来模拟 Middleware。
AWS(自托管)将 Next.js 应用打包成一个 Node.js 服务,在 EC2、ECS 或 EKS 上运行。1. 在服务器上运行 npm run build
2. 启动生产服务器:npm run start(默认端口 3000)。
3. 使用 Nginx 或 ALB 作为反向代理。
- 完全控制:你可以控制所有基础设施细节。
- 运维负担:需要自行处理扩展、负载均衡、SSL 证书、日志监控等。
Docker将应用容器化,便于在任何支持 Docker 的平台上部署。Dockerfile 示例:
dockerfile<br>FROM node:18-alpine AS deps<br>RUN apk add --no-cache libc6-compat<br>WORKDIR /app<br>COPY package.json yarn.lock* ./<br>RUN yarn install --frozen-lockfile<br><br>FROM node:18-alpine AS builder<br>WORKDIR /app<br>COPY --from=deps /app/node_modules ./node_modules<br>COPY . .<br>RUN yarn build<br><br>FROM node:18-alpine AS runner<br>WORKDIR /app<br>ENV NODE_ENV=production<br>RUN addgroup -g 1001 -S nodejs<br>RUN adduser -S nextjs -u 1001<br>COPY --from=builder /app/public ./public<br>COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./<br>COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static<br>USER nextjs<br>EXPOSE 3000<br>ENV PORT=3000<br>CMD ["node", "server.js"]<br>
- 使用 .next/standalonenext build 会生成一个最小化的 standalone 目录,只包含运行生产服务器必需的文件,减小镜像体积。
- 多阶段构建:如上所示,使用多阶段构建来优化最终镜像大小。

12.3 环境变量管理

在不同环境(开发、预发、生产)中安全地管理配置。

概念/方法说明语法/代码示例注意事项
.env.local用于本地开发的私有环境变量文件,不应提交到 Git。<br>// .env.local<br>NEXT_PUBLIC_API_URL=https://api.example.com<br>DB_PASSWORD=my-secret-password<br>- 前缀 NEXT_PUBLIC_:只有以此开头的变量才会被内联到客户端 bundle 中。其他变量仅在服务端可用。
平台环境变量在部署平台(如 Vercel、Netlify)的 UI 中设置环境变量。-- 覆盖本地变量:平台设置的变量会覆盖 .env 文件中的同名变量。
- 安全性:敏感变量(无 NEXT_PUBLIC_ 前缀)永远不会发送到客户端,即使在平台 UI 中设置也是如此。
运行时 vs 构建时理解变量何时被读取至关重要。- 构建时:next build 期间读取的变量(如 NEXT_PUBLIC_*)会被固化到静态资源中。
- 运行时:next start 期间读取的变量(如数据库连接字符串)可以在每次请求时动态获取。
- 动态内容:如果 API URL 需要在不同部署环境(如 staging vs prod)中变化,且不希望重新构建,应通过 API 路由在服务端代理请求,而不是使用 NEXT_PUBLIC_

12.4 静态导出(next export)

将 Next.js 应用构建为完全静态的 HTML/CSS/JS 文件,可以部署到任何静态文件服务器。

概念/操作说明操作细节注意事项
适用场景应用是完全静态的,不需要服务端逻辑(无 SSR、无 API Routes、无 Server Actions)。-- 功能限制:无法使用 getServerSideProps、动态路由(除非配合 getStaticPaths),以及任何需要 Node.js 服务器的功能。
配置next.config.js 中设置 output: 'export'js<br>// next.config.js<br>module.exports = {<br> output: 'export',<br> // 可选:指定输出目录,默认为 'out'<br> // distDir: 'dist',<br>};<br>- 自动禁用服务端功能:启用此选项后,Next.js 会在构建时检查并警告不兼容的用法。
构建与部署运行 next build,输出将位于 out 目录(或 distDir 指定的目录)。bash<br>npm run build # 会同时执行 build 和 export<br># 部署 out/ 目录下的所有文件<br>- 简单部署:生成的 out 目录可以上传到 GitHub Pages、S3 或任何 CDN,无需 Node.js 运行时。
混合渲染即使使用 output: 'export',App Router 仍支持在单个页面上混合使用 SSG 和 CSR。-- 这是静态导出的主要优势:你仍然可以拥有交互式 React 组件,只是初始 HTML 是静态生成的。