第一部分:核心基础
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 --tailwind | Tailwind 是 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.js 和 pages/_document.js 实现全局布局和 HTML 文档结构。- 数据获取:主要依赖 getStaticProps、getServerSideProps、getStaticPaths 等生命周期方法。- 组件模型:所有组件默认都是客户端组件(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.jsexport default function Dashboard() { return <h1>Dashboard</h1>;} | - 这是路由的必需文件。 - 默认是 Server Component,可以直接进行数据获取。 |
Layout 文件(layout.js / layout.tsx)
| 方法/文件名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Root Layout | export default function RootLayout({ children }) { ... } | 定义整个应用的根布局,通常包含 <html> 和 <body> 标签。 | // app/layout.jsexport default function RootLayout({ children }) { return ( <html lang="en"> <body>{children}</body> </html> );} | - 必须存在于 app 目录的根级别。- 只能有一个 Root Layout。 |
| Nested Layout | export default function Layout({ children }) { ... } | 定义特定路由段及其子路由的共享 UI 布局。 | // app/dashboard/layout.jsexport 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.jsexport 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.jsexport async function GET() { const users = await db.users.findMany(); return Response.json(users);} | - 文件名必须是 route.js。- 可以导出 GET、POST、PUT、PATCH、DELETE 等函数来处理对应的 HTTP 方法。- request 是 Web Request 对象,context 包含 params 等路由信息。 |
| POST 处理器 | export async function POST(request, context) { ... } | 定义对该路由的 HTTP POST 请求的处理逻辑。 | // app/api/users/route.jsexport 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.js 或 layout.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(此时 slug 为 undefined)以及 /post/a/b。 | - 当没有匹配到任何动态部分时,params.slug 为 undefined,而不是空数组。这使得根路径 /post 也能被此路由处理。 |
并行路由与拦截路由
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 并行路由 | 允许在同一层级同时渲染多个独立的路由。通过在文件夹名前加 @ 符号来定义。例如,app/@modal 和 app/@sidebar 可以与 app/page.js 并行存在。 | - 主要用于实现模态框、侧边栏等不影响主 URL 的 UI 状态。 - 在父级 layout.js 中,这些并行路由会作为 props 传入(如 modal、sidebar)。 |
| 拦截路由 | 允许在一个路由中渲染另一个路由的 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 / getStaticProps 的 context.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 是从 getStaticProps 或 getServerSideProps 获取的数据。- 此文件是可选的,但非常常用。 |
_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。
Link 组件
| 方法/属性 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
href | <Link href="/dashboard">Dashboard</Link> | 指定要导航到的目标路径。 | <Link href={`/blog/${post.slug}`}>Read more</Link> | - 支持字符串路径和带有 pathname、query 等属性的对象。- 默认启用预取功能,在生产环境中,当 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
| 方法/属性 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
push | router.push('/new-route') | 导航到新路由,并将新 URL 添加到浏览器历史记录栈中。 | const handleNavigate = () => { router.push('/profile'); }; | - 必须在 Client Component("use client")中使用。 |
replace | router.replace('/new-route') | 导航到新路由,但替换当前历史记录条目。 | // 通常用于表单提交后重定向router.replace('/success'); | - 用户点击后退按钮时,不会回到被替换的页面。 |
refresh | router.refresh() | 重新获取当前路由的数据并刷新页面,但会保留客户端 React 状态。 | // 用于在客户端操作(如删除数据)后更新页面await deleteUser();router.refresh(); | - 这是一个非常有用的功能,可以在不丢失本地 UI 状态的情况下更新服务端数据。 |
back | router.back() | 导航回上一个历史记录条目。 | <button onClick={() => router.back()}>Go Back</button> | - 等同于 window.history.back()。 |
forward | router.forward() | 导航到下一个历史记录条目。 | - | - 等同于 window.history.forward()。 |
3. 数据获取(Data Fetching)
3.1 App Router 数据获取策略
App Router 的数据获取围绕 React Server Components(RSC)和内置的 fetch API 优化展开。
服务端组件中的直接 fetch
| 概念/方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 直接 fetch | const 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)
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
getStaticProps | export 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(动态静态生成)
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
getStaticPaths | export 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> | - 仅用于带有动态路由的页面。 - 必须返回一个包含 paths 和 fallback 属性的对象。- fallback 选项(false、'blocking'、'true')决定了如何处理未在 paths 中预定义的路径。 |
getServerSideProps(服务器端渲染 SSR)
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
getServerSideProps | export 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 命令执行期间发出。 | getStaticProps 在 next 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 在每次用户访问该页面时被调用。 | - 可以访问请求上下文(req、res),例如用于身份验证。 |
| 缓存位置 | 通常不缓存,或由应用层控制。 | 由于禁用了缓存,每次请求都会重新生成。 | 框架本身不缓存 SSR 页面,但可以通过反向代理(如 Nginx)进行缓存。 | - 需要考虑服务器负载和响应时间。 |
4.3 增量静态再生(ISR)
ISR 是 SSG 的一个强大扩展。它允许你在构建后,在后台更新静态页面,而无需重新部署整个应用。
| 概念/方法 | 说明 | App Router 实现方式 | Pages Router 实现方式 | 注意事项 |
|---|---|---|---|---|
| 核心思想 | 页面在构建时生成,但在指定时间后,下一次用户请求会触发后台重新生成,并将新版本缓存起来供后续请求使用。 | 在 fetch 中设置 { next: { revalidate: number } }。 | 在 getStaticProps 的返回对象中设置 revalidate: number。 | - 完美平衡了 SSG 的性能和数据的时效性。 - 用户永远不会看到旧的、过期的数据;他们要么看到缓存的旧版本,要么触发并等待新版本(首次触发者)。 - 后续请求会立即获得新生成的页面。 |
| 重新验证触发 | 基于时间或手动。 | 时间:通过 revalidate 选项。手动:通过 revalidateTag 或 revalidatePath。 | 时间:通过 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 Hydration | Next.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/hello。 | js<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 标准,使用 Request 和 Response 对象。
| 概念/方法 | 说明 | 语法/代码示例 | 注意事项 |
|---|---|---|---|
| 基础用法 | 在 app 目录下创建 route.js 文件。该文件所在目录的路径即为 API 路径。例如,app/api/hello/route.js 对应 /api/hello。 | js<br>// app/api/hello/route.js<br>export async function GET(request) {<br> return Response.json({ message: 'Hello from Route Handler!' });<br>}<br> | - 必须导出名为 GET、POST、PUT、PATCH、DELETE 的异步函数来处理对应的 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。 |
| 访问动态参数 | 动态段参数通过第二个参数 context 的 params 属性获取。 | 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、原始数据等作为参数。 - 可以直接访问数据库或后端服务。 - 可以调用 revalidatePath 或 revalidateTag 来更新缓存。 |
| 在 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 和 useFormState | React 提供了 useFormStatus 和 useFormState 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} /> | - 必须提供 width 和 height 属性(或通过 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.domains 或 images.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 Fonts | import { 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 属性来使用可变字体。- 字体文件会被自动处理并包含在构建产物中。 |
| 应用字体 | 使用 className 或 style | 将优化后的字体应用到页面。 | <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:在浏览器空闲时加载,用于非关键脚本。 |
| 事件处理 | onLoad、onError | 监听脚本加载成功或失败的事件。 | <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/dynamic 是 React.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/link 的 Link 组件。 | - 仅在生产环境(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> | - postcss 和 autoprefixer 是 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" ... /> | - 这是存放静态资源的标准位置。 - 无法在此目录下使用 require 或 import。 |
| 导入静态资源 | 对于较小的资源(如 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 会将导入的文件处理并输出到构建目录,并提供一个包含 src 和 height/width 等元数据的对象。- 这种方式适用于需要在 JS 逻辑中操作的资源。 |
| next/image 与静态资源 | next/image 组件的 src 属性可以直接使用 public 目录下的路径。 | <Image src="/images/photo.jpg" width={500} height={500} alt="Photo" /> | - 这是处理 public 目录中图片的推荐方式,因为它会自动应用图像优化。 |
| next/font 与本地字体 | 本地字体文件应放在 public 目录或项目内部,并通过 next/font 的 localFont 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.js 或 middleware.ts,并放在项目根目录或 src 目录下。- 它运行在 Edge Runtime 上,因此必须使用 Web 标准 API(如 Request、Response),不能使用 Node.js 特有的模块(如 fs、path)。 |
| 执行时机 | 在任何缓存(如 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 路由的信息。- ip 和 geo 等信息需要部署平台(如 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(默认)和 database。 | ts<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 目录下,为每种语言创建一个以语言代码命名的子文件夹(如 en、fr)。使用 [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 时,必须将完整的本地化路径(包含语言前缀)传递给 href。 | tsx<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.js | 在 next.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.push 或 router.replace 进行导航。 |
| 第三方库集成 | 可以集成如 react-intl、i18next 等成熟的国际化库,它们提供了更多高级功能(如复数、日期/数字格式化)。 | - | - 对于复杂需求(如 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/image、next/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@latest2. 测试示例(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-dev2. 配置(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-http 或 supertest 来模拟 req 和 res 对象。 | 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/standalone:next 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 是静态生成的。 |