nextjs
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js Skill
Next.js 使用指南
Next.js is a React framework for building full-stack web applications with server-side rendering, static generation, and powerful optimization features built-in.
Next.js 是一款基于React的全栈Web应用框架,内置服务端渲染、静态生成功能以及强大的优化特性。
Reference
参考资料
When to Use This Skill
适用场景
Use this skill when:
- Building new Next.js applications (v15+)
- Implementing App Router architecture
- Working with Server Components and Client Components
- Setting up routing, layouts, and navigation
- Implementing data fetching patterns
- Optimizing images, fonts, and performance
- Configuring metadata and SEO
- Setting up API routes and route handlers
- Migrating from Pages Router to App Router
- Deploying Next.js applications
以下场景适用本指南:
- 构建全新Next.js应用(v15及以上版本)
- 实现App Router架构
- 使用Server Components与Client Components
- 配置路由、布局与导航
- 实现数据获取模式
- 优化图片、字体与性能
- 配置元数据与SEO
- 配置API路由与路由处理器
- 从Pages Router迁移至App Router
- 部署Next.js应用
Core Concepts
核心概念
App Router vs Pages Router
App Router 与 Pages Router 对比
App Router (Recommended for v13+):
- Modern architecture with React Server Components
- File-system based routing in directory
app/ - Layouts, loading states, and error boundaries
- Streaming and Suspense support
- Nested routing with layouts
Pages Router (Legacy):
- Traditional page-based routing in directory
pages/ - Uses ,
getStaticProps,getServerSidePropsgetInitialProps - Still supported for existing projects
App Router(v13及以上版本推荐使用):
- 基于React Server Components的现代化架构
- 基于目录的文件系统路由
app/ - 布局、加载状态与错误边界
- 支持流式渲染与Suspense
- 支持带布局的嵌套路由
Pages Router(旧版):
- 基于目录的传统页面路由
pages/ - 使用、
getStaticProps、getServerSidePropsgetInitialProps - 仍支持现有项目使用
Key Architectural Principles
核心架构原则
- Server Components by Default: Components in are Server Components unless marked with
app/'use client' - File-based Routing: File system defines application routes
- Nested Layouts: Share UI across routes with layouts
- Progressive Enhancement: Works without JavaScript when possible
- Automatic Optimization: Images, fonts, scripts auto-optimized
- 默认使用Server Components:目录下的组件默认是Server Components,除非标记
app/'use client' - 基于文件的路由:由文件系统定义应用路由
- 嵌套布局:通过布局在多个路由间共享UI
- 渐进式增强:在无JavaScript环境下也可正常运行(如果可能)
- 自动优化:图片、字体、脚本自动优化
Installation & Setup
安装与配置
Create New Project
创建新项目
bash
npx create-next-app@latest my-appbash
npx create-next-app@latest my-appor
or
yarn create next-app my-app
yarn create next-app my-app
or
or
pnpm create next-app my-app
pnpm create next-app my-app
or
or
bun create next-app my-app
**Interactive Setup Prompts:**
- TypeScript? (Yes recommended)
- ESLint? (Yes recommended)
- Tailwind CSS? (Optional)
- `src/` directory? (Optional)
- App Router? (Yes for new projects)
- Import alias? (Default: @/\*)bun create next-app my-app
**交互式配置提示:**
- 是否使用TypeScript?(推荐选择是)
- 是否使用ESLint?(推荐选择是)
- 是否使用Tailwind CSS?(可选)
- 是否创建`src/`目录?(可选)
- 是否使用App Router?(新项目推荐选择是)
- 是否配置导入别名?(默认:@/*)Manual Setup
手动配置
bash
npm install next@latest react@latest react-dom@latestpackage.json scripts:
json
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}bash
npm install next@latest react@latest react-dom@latestpackage.json 脚本配置:
json
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}Project Structure
项目结构
my-app/
├── app/ # App Router (v13+)
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── loading.tsx # Loading UI
│ ├── error.tsx # Error UI
│ ├── not-found.tsx # 404 page
│ ├── global.css # Global styles
│ └── [folder]/ # Route segments
├── public/ # Static assets
├── components/ # React components
├── lib/ # Utility functions
├── next.config.js # Next.js configuration
├── package.json
└── tsconfig.jsonmy-app/
├── app/ # App Router(v13+)
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 首页
│ ├── loading.tsx # 加载UI
│ ├── error.tsx # 错误UI
│ ├── not-found.tsx # 404页面
│ ├── global.css # 全局样式
│ └── [folder]/ # 路由片段
├── public/ # 静态资源
├── components/ # React组件
├── lib/ # 工具函数
├── next.config.js # Next.js配置
├── package.json
└── tsconfig.jsonRouting
路由
File Conventions
文件约定
- - Page UI for route
page.tsx - - Shared UI for segment and children
layout.tsx - - Loading UI (wraps page in Suspense)
loading.tsx - - Error UI (wraps page in Error Boundary)
error.tsx - - 404 UI
not-found.tsx - - API endpoint (Route Handler)
route.ts - - Re-rendered layout UI
template.tsx - - Parallel route fallback
default.tsx
- - 路由对应的页面UI
page.tsx - - 为当前路由片段及子路由共享的UI
layout.tsx - - 加载UI(将页面包裹在Suspense中)
loading.tsx - - 错误UI(将页面包裹在错误边界中)
error.tsx - - 404 UI
not-found.tsx - - API端点(路由处理器)
route.ts - - 可重新渲染的布局UI
template.tsx - - 并行路由的回退UI
default.tsx
Basic Routing
基础路由
Static Route:
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
└── blog/
└── page.tsx → /blogDynamic Route:
tsx
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Post: {params.slug}</h1>;
}Catch-all Route:
tsx
// app/shop/[...slug]/page.tsx
export default function Shop({ params }: { params: { slug: string[] } }) {
return <h1>Category: {params.slug.join("/")}</h1>;
}Optional Catch-all:
tsx
// app/docs/[[...slug]]/page.tsx
// Matches /docs, /docs/a, /docs/a/b, etc.静态路由:
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
└── blog/
└── page.tsx → /blog动态路由:
tsx
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>文章:{params.slug}</h1>;
}捕获所有路由:
tsx
// app/shop/[...slug]/page.tsx
export default function Shop({ params }: { params: { slug: string[] } }) {
return <h1>分类:{params.slug.join("/")}</h1>;
}可选捕获所有路由:
tsx
// app/docs/[[...slug]]/page.tsx
// 匹配 /docs、/docs/a、/docs/a/b 等路径Route Groups
路由组
Organize routes without affecting URL:
app/
├── (marketing)/ # Group without URL segment
│ ├── about/page.tsx → /about
│ └── blog/page.tsx → /blog
└── (shop)/
├── products/page.tsx → /products
└── cart/page.tsx → /cart在不影响URL的前提下组织路由:
app/
├── (marketing)/ # 路由组,不生成URL片段
│ ├── about/page.tsx → /about
│ └── blog/page.tsx → /blog
└── (shop)/
├── products/page.tsx → /products
└── cart/page.tsx → /cartParallel Routes
并行路由
Render multiple pages in same layout:
app/
├── @team/ # Slot
│ └── page.tsx
├── @analytics/ # Slot
│ └── page.tsx
└── layout.tsx # Uses both slotstsx
// app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<>
{children}
{team}
{analytics}
</>
);
}在同一布局中渲染多个页面:
app/
├── @team/ # 插槽
│ └── page.tsx
├── @analytics/ # 插槽
│ └── page.tsx
└── layout.tsx # 使用上述两个插槽tsx
// app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<>
{children}
{team}
{analytics}
</>
);
}Intercepting Routes
拦截路由
Intercept routes to show in modal:
app/
├── feed/
│ └── page.tsx
├── photo/
│ └── [id]/
│ └── page.tsx
└── (..)photo/ # Intercepts /photo/[id]
└── [id]/
└── page.tsx拦截路由以模态框形式展示:
app/
├── feed/
│ └── page.tsx
├── photo/
│ └── [id]/
│ └── page.tsx
└── (..)photo/ # 拦截 /photo/[id]
└── [id]/
└── page.tsxLayouts
布局
Root Layout (Required)
根布局(必填)
tsx
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}tsx
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}Nested Layouts
嵌套布局
tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<nav>Dashboard Nav</nav>
{children}
</section>
);
}Layouts are:
- Shared across multiple pages
- Preserve state on navigation
- Do not re-render on navigation
- Can fetch data
tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<nav>控制台导航</nav>
{children}
</section>
);
}布局特性:
- 在多个页面间共享
- 导航时保留状态
- 导航时不会重新渲染
- 可进行数据获取
Server and Client Components
Server Components 与 Client Components
Server Components (Default)
Server Components(默认)
Components in are Server Components by default:
app/tsx
// app/page.tsx (Server Component)
async function getData() {
const res = await fetch("https://api.example.com/data");
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{data.title}</div>;
}Benefits:
- Fetch data on server
- Access backend resources directly
- Keep sensitive data on server
- Reduce client-side JavaScript
- Improve initial page load
Limitations:
- Cannot use hooks (useState, useEffect)
- Cannot use browser APIs
- Cannot add event listeners
app/tsx
// app/page.tsx(Server Component)
async function getData() {
const res = await fetch("https://api.example.com/data");
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{data.title}</div>;
}优势:
- 在服务端获取数据
- 可直接访问后端资源
- 敏感数据保留在服务端
- 减少客户端JavaScript体积
- 提升初始页面加载速度
限制:
- 无法使用钩子(useState、useEffect)
- 无法使用浏览器API
- 无法添加事件监听器
Client Components
Client Components
Mark components with directive:
'use client'tsx
// components/counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}Use Client Components for:
- Interactive UI (onClick, onChange)
- State management (useState, useReducer)
- Effects (useEffect, useLayoutEffect)
- Browser APIs (localStorage, navigator)
- Custom hooks
- React class components
使用指令标记组件:
'use client'tsx
// components/counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>计数:{count}</button>;
}Client Components 适用场景:
- 交互式UI(onClick、onChange)
- 状态管理(useState、useReducer)
- 副作用(useEffect、useLayoutEffect)
- 浏览器API(localStorage、navigator)
- 自定义钩子
- React类组件
Composition Pattern
组合模式
tsx
// app/page.tsx (Server Component)
import { ClientComponent } from "./client-component";
export default function Page() {
return (
<div>
<h1>Server-rendered content</h1>
<ClientComponent />
</div>
);
}tsx
// app/page.tsx(Server Component)
import { ClientComponent } from "./client-component";
export default function Page() {
return (
<div>
<h1>服务端渲染内容</h1>
<ClientComponent />
</div>
);
}Data Fetching
数据获取
Server Component Data Fetching
Server Components 数据获取
tsx
// app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 }, // Revalidate every hour
});
if (!res.ok) {
throw new Error("Failed to fetch");
}
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}tsx
// app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 }, // 每小时重新验证一次
});
if (!res.ok) {
throw new Error("获取数据失败");
}
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}Caching Strategies
缓存策略
Force Cache (Default):
tsx
fetch("https://api.example.com/data", { cache: "force-cache" });No Store (Dynamic):
tsx
fetch("https://api.example.com/data", { cache: "no-store" });Revalidate:
tsx
fetch("https://api.example.com/data", {
next: { revalidate: 3600 }, // Seconds
});Tag-based Revalidation:
tsx
fetch("https://api.example.com/data", {
next: { tags: ["posts"] },
});
// Revalidate elsewhere:
import { revalidateTag } from "next/cache";
revalidateTag("posts");强制缓存(默认):
tsx
fetch("https://api.example.com/data", { cache: "force-cache" });不缓存(动态):
tsx
fetch("https://api.example.com/data", { cache: "no-store" });重新验证:
tsx
fetch("https://api.example.com/data", {
next: { revalidate: 3600 }, // 单位:秒
});基于标签的重新验证:
tsx
fetch("https://api.example.com/data", {
next: { tags: ["posts"] },
});
// 在其他地方触发重新验证:
import { revalidateTag } from "next/cache";
revalidateTag("posts");Parallel Data Fetching
并行数据获取
tsx
async function getData() {
const [posts, users] = await Promise.all([
fetch("https://api.example.com/posts").then((r) => r.json()),
fetch("https://api.example.com/users").then((r) => r.json()),
]);
return { posts, users };
}tsx
async function getData() {
const [posts, users] = await Promise.all([
fetch("https://api.example.com/posts").then((r) => r.json()),
fetch("https://api.example.com/users").then((r) => r.json()),
]);
return { posts, users };
}Sequential Data Fetching
顺序数据获取
tsx
async function getData() {
const post = await fetch(`https://api.example.com/posts/${id}`).then((r) =>
r.json(),
);
const author = await fetch(
`https://api.example.com/users/${post.authorId}`,
).then((r) => r.json());
return { post, author };
}tsx
async function getData() {
const post = await fetch(`https://api.example.com/posts/${id}`).then((r) =>
r.json(),
);
const author = await fetch(
`https://api.example.com/users/${post.authorId}`,
).then((r) => r.json());
return { post, author };
}Route Handlers (API Routes)
路由处理器(API路由)
Basic Route Handler
基础路由处理器
tsx
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: "Hello" });
}
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ received: body });
}tsx
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: "你好" });
}
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ received: body });
}Dynamic Route Handler
动态路由处理器
tsx
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } },
) {
const post = await getPost(params.id);
return Response.json(post);
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } },
) {
await deletePost(params.id);
return new Response(null, { status: 204 });
}tsx
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } },
) {
const post = await getPost(params.id);
return Response.json(post);
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } },
) {
await deletePost(params.id);
return new Response(null, { status: 204 });
}Request Helpers
请求辅助工具
tsx
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const cookies = request.headers.get("cookie");
return Response.json({ id });
}tsx
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const cookies = request.headers.get("cookie");
return Response.json({ id });
}Response Types
响应类型
tsx
// JSON
return Response.json({ data: "value" });
// Text
return new Response("Hello", { headers: { "Content-Type": "text/plain" } });
// Redirect
return Response.redirect("https://example.com");
// Status codes
return new Response("Not Found", { status: 404 });tsx
// JSON
return Response.json({ data: "值" });
// 文本
return new Response("你好", { headers: { "Content-Type": "text/plain" } });
// 重定向
return Response.redirect("https://example.com");
// 状态码
return new Response("未找到", { status: 404 });Navigation
导航
Link Component
Link 组件
tsx
import Link from "next/link";
export default function Page() {
return (
<>
<Link href="/about">About</Link>
<Link href="/blog/post-1">Post 1</Link>
<Link href={{ pathname: "/blog/[slug]", query: { slug: "post-1" } }}>
Post 1 (alternative)
</Link>
</>
);
}tsx
import Link from "next/link";
export default function Page() {
return (
<>
<Link href="/about">关于我们</Link>
<Link href="/blog/post-1">文章1</Link>
<Link href={{ pathname: "/blog/[slug]", query: { slug: "post-1" } }}>
文章1(写法二)
</Link>
</>
);
}useRouter Hook (Client)
useRouter 钩子(客户端)
tsx
"use client";
import { useRouter } from "next/navigation";
export function NavigateButton() {
const router = useRouter();
return <button onClick={() => router.push("/dashboard")}>Dashboard</button>;
}Router Methods:
- - Navigate to route
router.push(href) - - Replace current history
router.replace(href) - - Refresh current route
router.refresh() - - Navigate back
router.back() - - Navigate forward
router.forward() - - Prefetch route
router.prefetch(href)
tsx
"use client";
import { useRouter } from "next/navigation";
export function NavigateButton() {
const router = useRouter();
return <button onClick={() => router.push("/dashboard")}>控制台</button>;
}Router 方法:
- - 导航至指定路由
router.push(href) - - 替换当前历史记录
router.replace(href) - - 刷新当前路由
router.refresh() - - 返回上一页
router.back() - - 前进到下一页
router.forward() - - 预获取路由
router.prefetch(href)
Programmatic Navigation (Server)
程序化导航(服务端)
tsx
import { redirect } from "next/navigation";
export default async function Page() {
const session = await getSession();
if (!session) {
redirect("/login");
}
return <div>Protected content</div>;
}tsx
import { redirect } from "next/navigation";
export default async function Page() {
const session = await getSession();
if (!session) {
redirect("/login");
}
return <div>受保护内容</div>;
}Metadata & SEO
元数据与SEO
Static Metadata
静态元数据
tsx
// app/page.tsx
import { Metadata } from "next";
export const metadata: Metadata = {
title: "My Page",
description: "Page description",
keywords: ["nextjs", "react"],
openGraph: {
title: "My Page",
description: "Page description",
images: ["/og-image.jpg"],
},
twitter: {
card: "summary_large_image",
title: "My Page",
description: "Page description",
images: ["/twitter-image.jpg"],
},
};
export default function Page() {
return <div>Content</div>;
}tsx
// app/page.tsx
import { Metadata } from "next";
export const metadata: Metadata = {
title: "我的页面",
description: "页面描述",
keywords: ["nextjs", "react"],
openGraph: {
title: "我的页面",
description: "页面描述",
images: ["/og-image.jpg"],
},
twitter: {
card: "summary_large_image",
title: "我的页面",
description: "页面描述",
images: ["/twitter-image.jpg"],
},
};
export default function Page() {
return <div>内容</div>;
}Dynamic Metadata
动态元数据
tsx
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}tsx
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}Metadata Files
元数据文件
- ,
favicon.ico,icon.png- Faviconsapple-icon.png - ,
opengraph-image.png- Social imagestwitter-image.png - - Robots file
robots.txt - - Sitemap
sitemap.xml
- ,
favicon.ico,icon.png- 网站图标apple-icon.png - ,
opengraph-image.png- 社交分享图片twitter-image.png - - 爬虫规则文件
robots.txt - - 站点地图
sitemap.xml
Image Optimization
图片优化
Image Component
Image 组件
tsx
import Image from "next/image";
export default function Page() {
return (
<>
{/* Local image */}
<Image src="/profile.png" alt="Profile" width={500} height={500} />
{/* Remote image */}
<Image
src="https://example.com/image.jpg"
alt="Remote"
width={500}
height={500}
/>
{/* Responsive fill */}
<div style={{ position: "relative", width: "100%", height: "400px" }}>
<Image src="/hero.jpg" alt="Hero" fill style={{ objectFit: "cover" }} />
</div>
{/* Priority loading */}
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />
</>
);
}Image Props:
- - Image path (local or URL)
src - - Alt text (required)
alt - ,
width- Dimensions (required unless fill)height - - Fill parent container
fill - - Responsive sizes
sizes - - 1-100 (default 75)
quality - - Preload image
priority - - 'blur-sm' | 'empty'
placeholder - - Data URL for blur
blurDataURL
tsx
import Image from "next/image";
export default function Page() {
return (
<>
{/* 本地图片 */}
<Image src="/profile.png" alt="头像" width={500} height={500} />
{/* 远程图片 */}
<Image
src="https://example.com/image.jpg"
alt="远程图片"
width={500}
height={500}
/>
{/* 响应式填充 */}
<div style={{ position: "relative", width: "100%", height: "400px" }}>
<Image src="/hero.jpg" alt="首页横幅" fill style={{ objectFit: "cover" }} />
</div>
{/* 优先加载 */}
<Image src="/hero.jpg" alt="首页横幅" width={1200} height={600} priority />
</>
);
}Image 组件属性:
- - 图片路径(本地或URL)
src - - 替代文本(必填)
alt - ,
width- 尺寸(使用fill时可选)height - - 填充父容器
fill - - 响应式尺寸
sizes - - 图片质量1-100(默认75)
quality - - 预加载图片
priority - - 占位符:'blur-sm' | 'empty'
placeholder - - 模糊占位符的Data URL
blurDataURL
Remote Image Configuration
远程图片配置
js
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "example.com",
pathname: "/images/**",
},
],
},
};js
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "example.com",
pathname: "/images/**",
},
],
},
};Font Optimization
字体优化
Google Fonts
Google Fonts
tsx
// app/layout.tsx
import { Inter, Roboto_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
});
const robotoMono = Roboto_Mono({
subsets: ["latin"],
display: "swap",
variable: "--font-roboto-mono",
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.className} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
);
}tsx
// app/layout.tsx
import { Inter, Roboto_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
});
const robotoMono = Roboto_Mono({
subsets: ["latin"],
display: "swap",
variable: "--font-roboto-mono",
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.className} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
);
}Local Fonts
本地字体
tsx
import localFont from "next/font/local";
const myFont = localFont({
src: "./fonts/my-font.woff2",
display: "swap",
variable: "--font-my-font",
});tsx
import localFont from "next/font/local";
const myFont = localFont({
src: "./fonts/my-font.woff2",
display: "swap",
variable: "--font-my-font",
});Loading States
加载状态
Loading File
加载状态文件
tsx
// app/dashboard/loading.tsx
export default function Loading() {
return <div>Loading dashboard...</div>;
}tsx
// app/dashboard/loading.tsx
export default function Loading() {
return <div>正在加载控制台...</div>;
}Streaming with Suspense
结合Suspense的流式渲染
tsx
// app/page.tsx
import { Suspense } from "react";
async function Posts() {
const posts = await getPosts();
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
export default function Page() {
return (
<div>
<h1>My Posts</h1>
<Suspense fallback={<div>Loading posts...</div>}>
<Posts />
</Suspense>
</div>
);
}tsx
// app/page.tsx
import { Suspense } from "react";
async function Posts() {
const posts = await getPosts();
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
export default function Page() {
return (
<div>
<h1>我的文章</h1>
<Suspense fallback={<div>正在加载文章...</div>}>
<Posts />
</Suspense>
</div>
);
}Error Handling
错误处理
Error File
错误状态文件
tsx
// app/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}tsx
// app/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>出错了!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>重试</button>
</div>
);
}Global Error
全局错误处理
tsx
// app/global-error.tsx
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}tsx
// app/global-error.tsx
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>出错了!</h2>
<button onClick={() => reset()}>重试</button>
</body>
</html>
);
}Not Found
404 页面
tsx
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>404 - Not Found</h2>
<p>Could not find requested resource</p>
</div>
);
}
// Trigger programmatically
import { notFound } from "next/navigation";
export default async function Page({ params }) {
const post = await getPost(params.id);
if (!post) {
notFound();
}
return <div>{post.title}</div>;
}tsx
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>404 - 页面未找到</h2>
<p>无法找到请求的资源</p>
</div>
);
}
// 程序化触发404
import { notFound } from "next/navigation";
export default async function Page({ params }) {
const post = await getPost(params.id);
if (!post) {
notFound();
}
return <div>{post.title}</div>;
}Middleware
中间件
tsx
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Authentication check
const token = request.cookies.get("token");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Add custom header
const response = NextResponse.next();
response.headers.set("x-custom-header", "value");
return response;
}
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*"],
};tsx
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// 身份验证检查
const token = request.cookies.get("token");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// 添加自定义响应头
const response = NextResponse.next();
response.headers.set("x-custom-header", "value");
return response;
}
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*"],
};Environment Variables
环境变量
env
undefinedenv
undefined.env.local
.env.local
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
```tsx
// Server-side only
const dbUrl = process.env.DATABASE_URL;
// Client and server (NEXT_PUBLIC_ prefix)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
```tsx
// 仅服务端可用
const dbUrl = process.env.DATABASE_URL;
// 客户端与服务端均可用(需添加NEXT_PUBLIC_前缀)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;Configuration
配置
next.config.js
next.config.js
js
/** @type {import('next').NextConfig} */
const nextConfig = {
// React strict mode
reactStrictMode: true,
// Image domains
images: {
remotePatterns: [{ protocol: "https", hostname: "example.com" }],
},
// Redirects
async redirects() {
return [
{
source: "/old-page",
destination: "/new-page",
permanent: true,
},
];
},
// Rewrites
async rewrites() {
return [
{
source: "/api/:path*",
destination: "https://api.example.com/:path*",
},
];
},
// Headers
async headers() {
return [
{
source: "/(.*)",
headers: [{ key: "X-Frame-Options", value: "DENY" }],
},
];
},
// Environment variables
env: {
CUSTOM_KEY: "value",
},
};
module.exports = nextConfig;js
/** @type {import('next').NextConfig} */
const nextConfig = {
// React严格模式
reactStrictMode: true,
// 图片域名配置
images: {
remotePatterns: [{ protocol: "https", hostname: "example.com" }],
},
// 重定向规则
async redirects() {
return [
{
source: "/old-page",
destination: "/new-page",
permanent: true,
},
];
},
// 重写规则
async rewrites() {
return [
{
source: "/api/:path*",
destination: "https://api.example.com/:path*",
},
];
},
// 响应头配置
async headers() {
return [
{
source: "/(.*)",
headers: [{ key: "X-Frame-Options", value: "DENY" }],
},
];
},
// 环境变量
env: {
CUSTOM_KEY: "value",
},
};
module.exports = nextConfig;Best Practices
最佳实践
- Use Server Components: Default to Server Components, use Client Components only when needed
- Optimize Images: Always use for automatic optimization
next/image - Metadata: Set proper metadata for SEO
- Loading States: Provide loading UI with Suspense
- Error Handling: Implement error boundaries
- Route Handlers: Use for API endpoints instead of separate backend
- Caching: Leverage built-in caching strategies
- Layouts: Use nested layouts to share UI
- TypeScript: Enable TypeScript for type safety
- Performance: Use for above-fold images, lazy load below-fold
priority
- 使用Server Components:默认使用Server Components,仅在需要时使用Client Components
- 优化图片:始终使用组件以实现自动优化
next/image - 元数据配置:设置合适的元数据以优化SEO
- 加载状态:使用Suspense提供加载UI
- 错误处理:实现错误边界
- 路由处理器:使用路由处理器替代独立后端API
- 缓存策略:利用内置缓存策略
- 布局使用:使用嵌套布局共享UI
- TypeScript:启用TypeScript以保证类型安全
- 性能优化:首屏图片使用属性,首屏以下图片懒加载
priority
Common Patterns
常见模式
Protected Routes
受保护路由
tsx
// app/dashboard/layout.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
export default async function DashboardLayout({ children }) {
const session = await getSession();
if (!session) {
redirect("/login");
}
return <>{children}</>;
}tsx
// app/dashboard/layout.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
export default async function DashboardLayout({ children }) {
const session = await getSession();
if (!session) {
redirect("/login");
}
return <>{children}</>;
}Data Mutations (Server Actions)
数据变更(Server Actions)
tsx
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title");
await db.post.create({ data: { title } });
revalidatePath("/posts");
}
// app/posts/new/page.tsx
import { createPost } from "@/app/actions";
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" type="text" required />
<button type="submit">Create</button>
</form>
);
}tsx
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title");
await db.post.create({ data: { title } });
revalidatePath("/posts");
}
// app/posts/new/page.tsx
import { createPost } from "@/app/actions";
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" type="text" required />
<button type="submit">创建文章</button>
</form>
);
}Static Generation
静态生成
tsx
// Generate static params for dynamic routes
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function Post({ params }) {
const post = await getPost(params.slug);
return <article>{post.content}</article>;
}tsx
// 为动态路由生成静态参数
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function Post({ params }) {
const post = await getPost(params.slug);
return <article>{post.content}</article>;
}Deployment
部署
Vercel (Recommended)
Vercel(推荐)
bash
undefinedbash
undefinedInstall Vercel CLI
安装Vercel CLI
npm i -g vercel
npm i -g vercel
Deploy
部署
vercel
undefinedvercel
undefinedSelf-Hosting
自托管
bash
undefinedbash
undefinedBuild
构建生产版本
npm run build
npm run build
Start production server
启动生产服务器
npm start
**Requirements:**
- Node.js 18.17 or later
- `output: 'standalone'` in next.config.js (optional, reduces size)npm start
**要求:**
- Node.js 18.17或更高版本
- next.config.js中配置`output: 'standalone'`(可选,可减小包体积)Docker
Docker
dockerfile
FROM node:18-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]dockerfile
FROM node:18-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]Troubleshooting
故障排查
Common Issues
常见问题
-
Hydration errors
- Ensure server and client render same content
- Check for browser-only code in Server Components
- Verify no conditional rendering based on browser APIs
-
Images not loading
- Add remote domains to
next.config.js - Check image paths (use leading for public)
/ - Verify width/height provided
- Add remote domains to
-
API route 404
- Check file is named not
route.ts/jsindex.ts - Verify export named GET/POST not default export
- Ensure in directory
app/api/
- Check file is named
-
"use client" errors
- Add to components using hooks
'use client' - Import Client Components in Server Components, not vice versa
- Check event handlers have
'use client'
- Add
-
Metadata not updating
- Clear browser cache
- Check metadata export is named correctly
- Verify async generateMetadata returns Promise<Metadata>
-
Hydration错误
- 确保服务端与客户端渲染内容一致
- 检查Server Components中是否包含仅浏览器可用的代码
- 确认没有基于浏览器API的条件渲染
-
图片无法加载
- 在中添加远程图片域名
next.config.js - 检查图片路径(静态资源需以开头)
/ - 确认已提供width/height参数
- 在
-
API路由404
- 检查文件命名为而非
route.ts/jsindex.ts - 确认导出的是GET/POST等命名函数而非默认导出
- 确保文件位于目录下
app/api/
- 检查文件命名为
-
"use client"错误
- 为使用钩子的组件添加指令
'use client' - 在Server Components中导入Client Components,反之则不行
- 检查事件处理函数所在组件是否添加了
'use client'
- 为使用钩子的组件添加
-
元数据未更新
- 清除浏览器缓存
- 检查元数据导出的命名是否正确
- 确认异步generateMetadata返回的是Promise<Metadata>
Resources
资源
- Documentation: https://nextjs.org/docs
- Learn Course: https://nextjs.org/learn
- Examples: https://github.com/vercel/next.js/tree/canary/examples
- Blog: https://nextjs.org/blog
- GitHub: https://github.com/vercel/next.js
Implementation Checklist
实施检查清单
When building with Next.js:
- Create project with
create-next-app - Configure TypeScript and ESLint
- Set up root layout with metadata
- Implement routing structure
- Add loading and error states
- Configure image optimization
- Set up font optimization
- Implement data fetching patterns
- Add API routes as needed
- Configure environment variables
- Set up middleware if needed
- Optimize for production build
- Test in production mode
- Configure deployment platform
- Set up monitoring and analytics
使用Next.js构建应用时,请完成以下检查:
- 使用创建项目
create-next-app - 配置TypeScript与ESLint
- 设置带元数据的根布局
- 实现路由结构
- 添加加载与错误状态
- 配置图片优化
- 设置字体优化
- 实现数据获取模式
- 根据需要添加API路由
- 配置环境变量
- 按需配置中间件
- 优化生产构建
- 在生产模式下测试
- 配置部署平台
- 设置监控与分析