logo

Next.js Cheat Sheet


title: Next.js date: 2024-01-01 background: bg-black tags: - react - ssr - framework - frontend categories: - Frontend intro: | Next.js 是一个基于 React 的全栈框架,支持服务端渲染(SSR)、静态生成(SSG)、API 路由等功能。本速查表涵盖 Next.js 14+ App Router 的核心概念和常用 API。

入门 {.cols-2}

创建项目

# 使用 create-next-app
npx create-next-app@latest my-app

# 使用 TypeScript
npx create-next-app@latest my-app --typescript

# 使用 Tailwind CSS
npx create-next-app@latest my-app --tailwind

# 完整选项
npx create-next-app@latest my-app --ts --tailwind --eslint --app --src-dir

项目结构 (App Router)

my-app/
├── app/                 # App Router 目录
│   ├── layout.tsx       # 根布局
│   ├── page.tsx         # 首页
│   ├── globals.css      # 全局样式
│   ├── api/             # API 路由
│   │   └── route.ts
│   └── [slug]/          # 动态路由
│       └── page.tsx
├── components/          # 组件目录
├── lib/                 # 工具函数
├── public/              # 静态资源
├── next.config.js       # Next.js 配置
└── package.json

开发命令

# 开发模式
npm run dev

# 构建生产版本
npm run build

# 启动生产服务器
npm run start

# 代码检查
npm run lint

路由 {.cols-2}

文件系统路由

文件路由
app/page.tsx/
app/about/page.tsx/about
app/blog/[slug]/page.tsx/blog/:slug
app/shop/[...slug]/page.tsx/shop/*
app/(marketing)/about/page.tsx/about (分组)

动态路由

// app/blog/[slug]/page.tsx
interface Props {
	params: { slug: string };
}

export default function BlogPost({ params }: Props) {
	return <h1>Post: {params.slug}</h1>;
}

// 生成静态参数
export async function generateStaticParams() {
	const posts = await getPosts();
	return posts.map(post => ({
		slug: post.slug
	}));
}

Catch-all 路由

// app/shop/[...slug]/page.tsx
// 匹配 /shop/a, /shop/a/b, /shop/a/b/c

interface Props {
	params: { slug: string[] };
}

export default function Shop({ params }: Props) {
	// /shop/a/b/c => slug = ['a', 'b', 'c']
	return <div>{params.slug.join('/')}</div>;
}

路由组

app/
├── (marketing)/        # 不影响 URL
│   ├── about/page.tsx  # /about
│   └── blog/page.tsx   # /blog
├── (shop)/
│   └── cart/page.tsx   # /cart
└── layout.tsx

并行路由

// 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
// app/@analytics/page.tsx

页面与布局 {.cols-2}

页面组件

// app/page.tsx
export default function Home() {
	return (
		<main>
			<h1>Welcome to Next.js</h1>
		</main>
	);
}

布局组件

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export const metadata = {
	title: 'My App',
	description: 'My awesome app'
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<html lang="en">
			<body className={inter.className}>{children}</body>
		</html>
	);
}

嵌套布局

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
	return (
		<div className="dashboard">
			<nav>Dashboard Nav</nav>
			<main>{children}</main>
		</div>
	);
}

模板 (Template)

// app/template.tsx
// 每次导航都会重新挂载

export default function Template({ children }: { children: React.ReactNode }) {
	return <div>{children}</div>;
}

Loading UI

// app/dashboard/loading.tsx
export default function Loading() {
	return (
		<div className="loading">
			<div className="spinner" />
			<p>Loading...</p>
		</div>
	);
}

Error 处理

'use client';

// app/dashboard/error.tsx
export default function Error({
	error,
	reset
}: {
	error: Error & { digest?: string };
	reset: () => void;
}) {
	return (
		<div>
			<h2>Something went wrong!</h2>
			<button onClick={() => reset()}>Try again</button>
		</div>
	);
}

Not Found

// app/not-found.tsx
import Link from 'next/link';

export default function NotFound() {
	return (
		<div>
			<h2>Not Found</h2>
			<p>Could not find requested resource</p>
			<Link href="/">Return Home</Link>
		</div>
	);
}

数据获取 {.cols-2}

服务端组件获取数据

// 默认是服务端组件
async function getData() {
	const res = await fetch('https://api.example.com/data');
	if (!res.ok) throw new Error('Failed to fetch');
	return res.json();
}

export default async function Page() {
	const data = await getData();
	return <main>{data.title}</main>;
}

缓存策略

// 默认缓存 (等同于 SSG)
fetch('https://api.example.com/data');

// 不缓存 (等同于 SSR)
fetch('https://api.example.com/data', {
	cache: 'no-store'
});

// 定时重新验证 (ISR)
fetch('https://api.example.com/data', {
	next: { revalidate: 3600 } // 1小时
});

// 基于标签重新验证
fetch('https://api.example.com/data', {
	next: { tags: ['posts'] }
});

重新验证

// app/actions.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

// 重新验证路径
export async function updatePost() {
	// 更新数据库
	revalidatePath('/blog');
}

// 重新验证标签
export async function refreshPosts() {
	revalidateTag('posts');
}

并行数据获取

async function Page() {
	// 并行请求
	const [user, posts] = await Promise.all([getUser(), getPosts()]);

	return (
		<div>
			<UserProfile user={user} />
			<PostList posts={posts} />
		</div>
	);
}

客户端数据获取

'use client';

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then(res => res.json());

export default function Profile() {
	const { data, error, isLoading } = useSWR('/api/user', fetcher);

	if (isLoading) return <div>Loading...</div>;
	if (error) return <div>Error</div>;
	return <div>Hello {data.name}</div>;
}

Server Actions {.cols-2}

定义 Server Action

// app/actions.ts
'use server';

export async function createPost(formData: FormData) {
	const title = formData.get('title');
	const content = formData.get('content');

	await db.post.create({
		data: { title, content }
	});

	revalidatePath('/posts');
}

在表单中使用

// app/page.tsx
import { createPost } from './actions';

export default function Page() {
	return (
		<form action={createPost}>
			<input name="title" />
			<textarea name="content" />
			<button type="submit">Create</button>
		</form>
	);
}

带参数的 Action

'use server';

export async function updatePost(id: string, formData: FormData) {
	const title = formData.get('title');
	await db.post.update({
		where: { id },
		data: { title }
	});
}

// 使用 bind
<form action={updatePost.bind(null, post.id)}>
	<input name="title" />
	<button>Update</button>
</form>;

使用 useFormState

'use client';

import { useFormState } from 'react-dom';
import { createPost } from './actions';

const initialState = { message: '' };

export default function Form() {
	const [state, formAction] = useFormState(createPost, initialState);

	return (
		<form action={formAction}>
			<input name="title" />
			<button>Create</button>
			<p>{state.message}</p>
		</form>
	);
}

使用 useFormStatus

'use client';

import { useFormStatus } from 'react-dom';

function SubmitButton() {
	const { pending } = useFormStatus();

	return <button disabled={pending}>{pending ? 'Submitting...' : 'Submit'}</button>;
}

API 路由 {.cols-2}

基本路由

// app/api/hello/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
	return NextResponse.json({ message: 'Hello' });
}

export async function POST(request: Request) {
	const body = await request.json();
	return NextResponse.json({ received: body });
}

动态路由

// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request, { params }: { params: { id: string } }) {
	const post = await getPost(params.id);
	return NextResponse.json(post);
}

请求处理

// app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
	// 查询参数
	const searchParams = request.nextUrl.searchParams;
	const query = searchParams.get('q');

	// Headers
	const headersList = request.headers;
	const token = headersList.get('authorization');

	// Cookies
	const cookieStore = request.cookies;
	const session = cookieStore.get('session');

	return NextResponse.json({ query });
}

设置响应头

export async function GET() {
	return new NextResponse('Hello', {
		status: 200,
		headers: {
			'Content-Type': 'text/plain',
			'Cache-Control': 'no-store'
		}
	});
}

设置 Cookies

import { cookies } from 'next/headers';

export async function GET() {
	const cookieStore = cookies();

	// 读取
	const token = cookieStore.get('token');

	// 设置
	cookieStore.set('token', 'value', {
		httpOnly: true,
		secure: true,
		maxAge: 60 * 60 * 24 // 1 day
	});

	// 删除
	cookieStore.delete('token');

	return NextResponse.json({ success: true });
}

导航 {.cols-2}

import Link from 'next/link';

export default function Nav() {
	return (
		<nav>
			<Link href="/">Home</Link>
			<Link href="/about">About</Link>
			<Link href="/blog/hello-world">Blog Post</Link>

			{/* 预取 */}
			<Link href="/dashboard" prefetch={false}>
				Dashboard
			</Link>

			{/* 替换历史 */}
			<Link href="/login" replace>
				Login
			</Link>
		</nav>
	);
}

useRouter Hook

'use client';

import { useRouter } from 'next/navigation';

export default function Page() {
	const router = useRouter();

	return (
		<div>
			<button onClick={() => router.push('/dashboard')}>Dashboard</button>
			<button onClick={() => router.replace('/login')}>Login</button>
			<button onClick={() => router.back()}>Go Back</button>
			<button onClick={() => router.refresh()}>Refresh</button>
		</div>
	);
}

usePathname

'use client';

import { usePathname } from 'next/navigation';

export default function Nav() {
	const pathname = usePathname();

	return (
		<nav>
			<Link href="/" className={pathname === '/' ? 'active' : ''}>
				Home
			</Link>
		</nav>
	);
}

useSearchParams

'use client';

import { useSearchParams } from 'next/navigation';

export default function Search() {
	const searchParams = useSearchParams();
	const query = searchParams.get('q');

	return <div>Search: {query}</div>;
}

redirect

import { redirect } from 'next/navigation';

async function Page() {
	const user = await getUser();

	if (!user) {
		redirect('/login');
	}

	return <div>Welcome {user.name}</div>;
}

Metadata {.cols-2}

静态 Metadata

// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
	title: 'My App',
	description: 'My app description',
	keywords: ['Next.js', 'React'],
	authors: [{ name: 'Author' }],
	openGraph: {
		title: 'My App',
		description: 'My app description',
		images: ['/og-image.png']
	},
	twitter: {
		card: 'summary_large_image'
	}
};

动态 Metadata

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

interface Props {
	params: { slug: string };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
	const post = await getPost(params.slug);

	return {
		title: post.title,
		description: post.excerpt,
		openGraph: {
			images: [post.image]
		}
	};
}

Title 模板

// app/layout.tsx
export const metadata: Metadata = {
	title: {
		template: '%s | My App',
		default: 'My App'
	}
};

// app/about/page.tsx
export const metadata: Metadata = {
	title: 'About' // => "About | My App"
};

优化 {.cols-2}

Image 组件

import Image from 'next/image';

export default function Page() {
	return (
		<>
			{/* 本地图片 */}
			<Image src="/hero.png" alt="Hero" width={800} height={600} priority />

			{/* 远程图片 */}
			<Image
				src="https://example.com/photo.jpg"
				alt="Photo"
				width={400}
				height={300}
				placeholder="blur"
				blurDataURL="data:image/..."
			/>

			{/* 填充容器 */}
			<div style={{ position: 'relative', height: 300 }}>
				<Image src="/bg.png" alt="Background" fill style={{ objectFit: 'cover' }} />
			</div>
		</>
	);
}

Font 优化

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';

const inter = Inter({
	subsets: ['latin'],
	display: 'swap',
	variable: '--font-inter'
});

const robotoMono = Roboto_Mono({
	subsets: ['latin'],
	variable: '--font-roboto-mono'
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<html className={`${inter.variable} ${robotoMono.variable}`}>
			<body>{children}</body>
		</html>
	);
}

Script 组件

import Script from 'next/script';

export default function Page() {
	return (
		<>
			{/* 延迟加载 */}
			<Script src="https://example.com/script.js" strategy="lazyOnload" />

			{/* 页面交互后加载 */}
			<Script src="https://example.com/analytics.js" strategy="afterInteractive" />

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

Middleware {.cols-2}

基本 Middleware

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
	// 重定向
	if (request.nextUrl.pathname === '/old') {
		return NextResponse.redirect(new URL('/new', request.url));
	}

	// 重写
	if (request.nextUrl.pathname.startsWith('/api')) {
		return NextResponse.rewrite(new URL('/api/proxy', request.url));
	}

	return NextResponse.next();
}

// 配置匹配路径
export const config = {
	matcher: ['/old', '/api/:path*']
};

认证检查

// 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 && request.nextUrl.pathname.startsWith('/dashboard')) {
		return NextResponse.redirect(new URL('/login', request.url));
	}

	return NextResponse.next();
}

export const config = {
	matcher: '/dashboard/:path*'
};

设置请求头

export function middleware(request: NextRequest) {
	// 克隆请求头
	const requestHeaders = new Headers(request.headers);
	requestHeaders.set('x-custom-header', 'value');

	// 返回带新头的响应
	const response = NextResponse.next({
		request: {
			headers: requestHeaders
		}
	});

	// 设置响应头
	response.headers.set('x-response-header', 'value');

	return response;
}

配置 {.cols-2}

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
	// 严格模式
	reactStrictMode: true,

	// 图片域名
	images: {
		remotePatterns: [
			{
				protocol: 'https',
				hostname: 'example.com'
			}
		]
	},

	// 环境变量
	env: {
		API_URL: process.env.API_URL
	},

	// 重定向
	async redirects() {
		return [
			{
				source: '/old',
				destination: '/new',
				permanent: true
			}
		];
	},

	// 重写
	async rewrites() {
		return [
			{
				source: '/api/:path*',
				destination: 'https://api.example.com/:path*'
			}
		];
	},

	// Headers
	async headers() {
		return [
			{
				source: '/:path*',
				headers: [
					{
						key: 'X-Frame-Options',
						value: 'DENY'
					}
				]
			}
		];
	}
};

module.exports = nextConfig;

环境变量

# .env.local
DATABASE_URL=postgresql://...
API_SECRET=secret

# 客户端可见 (需要 NEXT_PUBLIC_ 前缀)
NEXT_PUBLIC_API_URL=https://api.example.com
// 服务端
const dbUrl = process.env.DATABASE_URL;

// 客户端
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

部署 {.cols-2}

Vercel 部署

# 安装 Vercel CLI
npm i -g vercel

# 部署
vercel

# 生产部署
vercel --prod

静态导出

// next.config.js
const nextConfig = {
	output: 'export'
};
# 构建静态文件
npm run build
# 输出到 out/ 目录

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 --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV production

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]
// next.config.js
const nextConfig = {
	output: 'standalone'
};

常用 Hooks {.cols-2}

路由相关

'use client';

import { useRouter, usePathname, useSearchParams, useParams } from 'next/navigation';

export function Component() {
	const router = useRouter();
	const pathname = usePathname();
	const searchParams = useSearchParams();
	const params = useParams();

	// router.push('/path')
	// router.replace('/path')
	// router.back()
	// router.refresh()

	return null;
}

服务端工具函数

import { headers, cookies } from 'next/headers';

async function ServerComponent() {
	// 读取请求头
	const headersList = headers();
	const userAgent = headersList.get('user-agent');

	// 读取 cookies
	const cookieStore = cookies();
	const token = cookieStore.get('token');

	return null;
}

另见 {.cols-1}

🎨 前端开发

Next.js

Next.js Cheat Sheet - 快速参考指南,收录常用语法、命令与实践。

📂 分类 · 前端开发🧭 Markdown 速查🏷️ 3 个标签
#nextjs#react#ssr
向下滚动查看内容
返回全部 Cheat Sheets

入门

创建项目
BASH
滚动查看更多
# 使用 create-next-app
npx create-next-app@latest my-app

# 使用 TypeScript
npx create-next-app@latest my-app --typescript

# 使用 Tailwind CSS
npx create-next-app@latest my-app --tailwind

# 完整选项
npx create-next-app@latest my-app --ts --tailwind --eslint --app --src-dir
项目结构 (App Router)
BASH
滚动查看更多
my-app/
├── app/                 # App Router 目录
│   ├── layout.tsx       # 根布局
│   ├── page.tsx         # 首页
│   ├── globals.css      # 全局样式
│   ├── api/             # API 路由
│   │   └── route.ts
│   └── [slug]/          # 动态路由
│       └── page.tsx
├── components/          # 组件目录
├── lib/                 # 工具函数
├── public/              # 静态资源
├── next.config.js       # Next.js 配置
└── package.json
开发命令
BASH
滚动查看更多
# 开发模式
npm run dev

# 构建生产版本
npm run build

# 启动生产服务器
npm run start

# 代码检查
npm run lint

路由

文件系统路由
文件路由
app/page.tsx/
app/about/page.tsx/about
app/blog/[slug]/page.tsx/blog/:slug
app/shop/[...slug]/page.tsx/shop/*
app/(marketing)/about/page.tsx/about (分组)
动态路由
TSX
滚动查看更多
// app/blog/[slug]/page.tsx
interface Props {
	params: { slug: string };
}

export default function BlogPost({ params }: Props) {
	return <h1>Post: {params.slug}</h1>;
}

// 生成静态参数
export async function generateStaticParams() {
	const posts = await getPosts();
	return posts.map(post => ({
		slug: post.slug
	}));
}
Catch-all 路由
TSX
滚动查看更多
// app/shop/[...slug]/page.tsx
// 匹配 /shop/a, /shop/a/b, /shop/a/b/c

interface Props {
	params: { slug: string[] };
}

export default function Shop({ params }: Props) {
	// /shop/a/b/c => slug = ['a', 'b', 'c']
	return <div>{params.slug.join('/')}</div>;
}
路由组
BASH
滚动查看更多
app/
├── (marketing)/        # 不影响 URL
│   ├── about/page.tsx  # /about
│   └── blog/page.tsx   # /blog
├── (shop)/
│   └── cart/page.tsx   # /cart
└── 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}
		</>
	);
}

// app/@team/page.tsx
// app/@analytics/page.tsx

页面与布局

页面组件
TSX
滚动查看更多
// app/page.tsx
export default function Home() {
	return (
		<main>
			<h1>Welcome to Next.js</h1>
		</main>
	);
}
布局组件
TSX
滚动查看更多
// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export const metadata = {
	title: 'My App',
	description: 'My awesome app'
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<html lang="en">
			<body className={inter.className}>{children}</body>
		</html>
	);
}
嵌套布局
TSX
滚动查看更多
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
	return (
		<div className="dashboard">
			<nav>Dashboard Nav</nav>
			<main>{children}</main>
		</div>
	);
}
模板 (Template)
TSX
滚动查看更多
// app/template.tsx
// 每次导航都会重新挂载

export default function Template({ children }: { children: React.ReactNode }) {
	return <div>{children}</div>;
}
Loading UI
TSX
滚动查看更多
// app/dashboard/loading.tsx
export default function Loading() {
	return (
		<div className="loading">
			<div className="spinner" />
			<p>Loading...</p>
		</div>
	);
}
Error 处理
TSX
滚动查看更多
'use client';

// app/dashboard/error.tsx
export default function Error({
	error,
	reset
}: {
	error: Error & { digest?: string };
	reset: () => void;
}) {
	return (
		<div>
			<h2>Something went wrong!</h2>
			<button onClick={() => reset()}>Try again</button>
		</div>
	);
}
Not Found
TSX
滚动查看更多
// app/not-found.tsx
import Link from 'next/link';

export default function NotFound() {
	return (
		<div>
			<h2>Not Found</h2>
			<p>Could not find requested resource</p>
			<Link href="/">Return Home</Link>
		</div>
	);
}

数据获取

服务端组件获取数据
TSX
滚动查看更多
// 默认是服务端组件
async function getData() {
	const res = await fetch('https://api.example.com/data');
	if (!res.ok) throw new Error('Failed to fetch');
	return res.json();
}

export default async function Page() {
	const data = await getData();
	return <main>{data.title}</main>;
}
缓存策略
TSX
滚动查看更多
// 默认缓存 (等同于 SSG)
fetch('https://api.example.com/data');

// 不缓存 (等同于 SSR)
fetch('https://api.example.com/data', {
	cache: 'no-store'
});

// 定时重新验证 (ISR)
fetch('https://api.example.com/data', {
	next: { revalidate: 3600 } // 1小时
});

// 基于标签重新验证
fetch('https://api.example.com/data', {
	next: { tags: ['posts'] }
});
重新验证
TSX
滚动查看更多
// app/actions.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

// 重新验证路径
export async function updatePost() {
	// 更新数据库
	revalidatePath('/blog');
}

// 重新验证标签
export async function refreshPosts() {
	revalidateTag('posts');
}
并行数据获取
TSX
滚动查看更多
async function Page() {
	// 并行请求
	const [user, posts] = await Promise.all([getUser(), getPosts()]);

	return (
		<div>
			<UserProfile user={user} />
			<PostList posts={posts} />
		</div>
	);
}
客户端数据获取
TSX
滚动查看更多
'use client';

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then(res => res.json());

export default function Profile() {
	const { data, error, isLoading } = useSWR('/api/user', fetcher);

	if (isLoading) return <div>Loading...</div>;
	if (error) return <div>Error</div>;
	return <div>Hello {data.name}</div>;
}

Server Actions

定义 Server Action
TSX
滚动查看更多
// app/actions.ts
'use server';

export async function createPost(formData: FormData) {
	const title = formData.get('title');
	const content = formData.get('content');

	await db.post.create({
		data: { title, content }
	});

	revalidatePath('/posts');
}
在表单中使用
TSX
滚动查看更多
// app/page.tsx
import { createPost } from './actions';

export default function Page() {
	return (
		<form action={createPost}>
			<input name="title" />
			<textarea name="content" />
			<button type="submit">Create</button>
		</form>
	);
}
带参数的 Action
TSX
滚动查看更多
'use server';

export async function updatePost(id: string, formData: FormData) {
	const title = formData.get('title');
	await db.post.update({
		where: { id },
		data: { title }
	});
}

// 使用 bind
<form action={updatePost.bind(null, post.id)}>
	<input name="title" />
	<button>Update</button>
</form>;
使用 useFormState
TSX
滚动查看更多
'use client';

import { useFormState } from 'react-dom';
import { createPost } from './actions';

const initialState = { message: '' };

export default function Form() {
	const [state, formAction] = useFormState(createPost, initialState);

	return (
		<form action={formAction}>
			<input name="title" />
			<button>Create</button>
			<p>{state.message}</p>
		</form>
	);
}
使用 useFormStatus
TSX
滚动查看更多
'use client';

import { useFormStatus } from 'react-dom';

function SubmitButton() {
	const { pending } = useFormStatus();

	return <button disabled={pending}>{pending ? 'Submitting...' : 'Submit'}</button>;
}

API 路由

基本路由
TSX
滚动查看更多
// app/api/hello/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
	return NextResponse.json({ message: 'Hello' });
}

export async function POST(request: Request) {
	const body = await request.json();
	return NextResponse.json({ received: body });
}
动态路由
TSX
滚动查看更多
// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request, { params }: { params: { id: string } }) {
	const post = await getPost(params.id);
	return NextResponse.json(post);
}
请求处理
TSX
滚动查看更多
// app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
	// 查询参数
	const searchParams = request.nextUrl.searchParams;
	const query = searchParams.get('q');

	// Headers
	const headersList = request.headers;
	const token = headersList.get('authorization');

	// Cookies
	const cookieStore = request.cookies;
	const session = cookieStore.get('session');

	return NextResponse.json({ query });
}
设置响应头
TSX
滚动查看更多
export async function GET() {
	return new NextResponse('Hello', {
		status: 200,
		headers: {
			'Content-Type': 'text/plain',
			'Cache-Control': 'no-store'
		}
	});
}
设置 Cookies
TSX
滚动查看更多
import { cookies } from 'next/headers';

export async function GET() {
	const cookieStore = cookies();

	// 读取
	const token = cookieStore.get('token');

	// 设置
	cookieStore.set('token', 'value', {
		httpOnly: true,
		secure: true,
		maxAge: 60 * 60 * 24 // 1 day
	});

	// 删除
	cookieStore.delete('token');

	return NextResponse.json({ success: true });
}

导航

Link 组件
TSX
滚动查看更多
import Link from 'next/link';

export default function Nav() {
	return (
		<nav>
			<Link href="/">Home</Link>
			<Link href="/about">About</Link>
			<Link href="/blog/hello-world">Blog Post</Link>

			{/* 预取 */}
			<Link href="/dashboard" prefetch={false}>
				Dashboard
			</Link>

			{/* 替换历史 */}
			<Link href="/login" replace>
				Login
			</Link>
		</nav>
	);
}
useRouter Hook
TSX
滚动查看更多
'use client';

import { useRouter } from 'next/navigation';

export default function Page() {
	const router = useRouter();

	return (
		<div>
			<button onClick={() => router.push('/dashboard')}>Dashboard</button>
			<button onClick={() => router.replace('/login')}>Login</button>
			<button onClick={() => router.back()}>Go Back</button>
			<button onClick={() => router.refresh()}>Refresh</button>
		</div>
	);
}
usePathname
TSX
滚动查看更多
'use client';

import { usePathname } from 'next/navigation';

export default function Nav() {
	const pathname = usePathname();

	return (
		<nav>
			<Link href="/" className={pathname === '/' ? 'active' : ''}>
				Home
			</Link>
		</nav>
	);
}
useSearchParams
TSX
滚动查看更多
'use client';

import { useSearchParams } from 'next/navigation';

export default function Search() {
	const searchParams = useSearchParams();
	const query = searchParams.get('q');

	return <div>Search: {query}</div>;
}
redirect
TSX
滚动查看更多
import { redirect } from 'next/navigation';

async function Page() {
	const user = await getUser();

	if (!user) {
		redirect('/login');
	}

	return <div>Welcome {user.name}</div>;
}

Metadata

静态 Metadata
TSX
滚动查看更多
// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
	title: 'My App',
	description: 'My app description',
	keywords: ['Next.js', 'React'],
	authors: [{ name: 'Author' }],
	openGraph: {
		title: 'My App',
		description: 'My app description',
		images: ['/og-image.png']
	},
	twitter: {
		card: 'summary_large_image'
	}
};
动态 Metadata
TSX
滚动查看更多
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

interface Props {
	params: { slug: string };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
	const post = await getPost(params.slug);

	return {
		title: post.title,
		description: post.excerpt,
		openGraph: {
			images: [post.image]
		}
	};
}
Title 模板
TSX
滚动查看更多
// app/layout.tsx
export const metadata: Metadata = {
	title: {
		template: '%s | My App',
		default: 'My App'
	}
};

// app/about/page.tsx
export const metadata: Metadata = {
	title: 'About' // => "About | My App"
};

优化

Image 组件
TSX
滚动查看更多
import Image from 'next/image';

export default function Page() {
	return (
		<>
			{/* 本地图片 */}
			<Image src="/hero.png" alt="Hero" width={800} height={600} priority />

			{/* 远程图片 */}
			<Image
				src="https://example.com/photo.jpg"
				alt="Photo"
				width={400}
				height={300}
				placeholder="blur"
				blurDataURL="data:image/..."
			/>

			{/* 填充容器 */}
			<div style={{ position: 'relative', height: 300 }}>
				<Image src="/bg.png" alt="Background" fill style={{ objectFit: 'cover' }} />
			</div>
		</>
	);
}
Font 优化
TSX
滚动查看更多
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';

const inter = Inter({
	subsets: ['latin'],
	display: 'swap',
	variable: '--font-inter'
});

const robotoMono = Roboto_Mono({
	subsets: ['latin'],
	variable: '--font-roboto-mono'
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<html className={`${inter.variable} ${robotoMono.variable}`}>
			<body>{children}</body>
		</html>
	);
}
Script 组件
TSX
滚动查看更多
import Script from 'next/script';

export default function Page() {
	return (
		<>
			{/* 延迟加载 */}
			<Script src="https://example.com/script.js" strategy="lazyOnload" />

			{/* 页面交互后加载 */}
			<Script src="https://example.com/analytics.js" strategy="afterInteractive" />

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

Middleware

基本 Middleware
TSX
滚动查看更多
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
	// 重定向
	if (request.nextUrl.pathname === '/old') {
		return NextResponse.redirect(new URL('/new', request.url));
	}

	// 重写
	if (request.nextUrl.pathname.startsWith('/api')) {
		return NextResponse.rewrite(new URL('/api/proxy', request.url));
	}

	return NextResponse.next();
}

// 配置匹配路径
export const config = {
	matcher: ['/old', '/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 && request.nextUrl.pathname.startsWith('/dashboard')) {
		return NextResponse.redirect(new URL('/login', request.url));
	}

	return NextResponse.next();
}

export const config = {
	matcher: '/dashboard/:path*'
};
设置请求头
TSX
滚动查看更多
export function middleware(request: NextRequest) {
	// 克隆请求头
	const requestHeaders = new Headers(request.headers);
	requestHeaders.set('x-custom-header', 'value');

	// 返回带新头的响应
	const response = NextResponse.next({
		request: {
			headers: requestHeaders
		}
	});

	// 设置响应头
	response.headers.set('x-response-header', 'value');

	return response;
}

配置

next.config.js
JS
滚动查看更多
/** @type {import('next').NextConfig} */
const nextConfig = {
	// 严格模式
	reactStrictMode: true,

	// 图片域名
	images: {
		remotePatterns: [
			{
				protocol: 'https',
				hostname: 'example.com'
			}
		]
	},

	// 环境变量
	env: {
		API_URL: process.env.API_URL
	},

	// 重定向
	async redirects() {
		return [
			{
				source: '/old',
				destination: '/new',
				permanent: true
			}
		];
	},

	// 重写
	async rewrites() {
		return [
			{
				source: '/api/:path*',
				destination: 'https://api.example.com/:path*'
			}
		];
	},

	// Headers
	async headers() {
		return [
			{
				source: '/:path*',
				headers: [
					{
						key: 'X-Frame-Options',
						value: 'DENY'
					}
				]
			}
		];
	}
};

module.exports = nextConfig;
环境变量
BASH
滚动查看更多
# .env.local
DATABASE_URL=postgresql://...
API_SECRET=secret

# 客户端可见 (需要 NEXT_PUBLIC_ 前缀)
NEXT_PUBLIC_API_URL=https://api.example.com
TSX
滚动查看更多
// 服务端
const dbUrl = process.env.DATABASE_URL;

// 客户端
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

部署

Vercel 部署
BASH
滚动查看更多
# 安装 Vercel CLI
npm i -g vercel

# 部署
vercel

# 生产部署
vercel --prod
静态导出
JS
滚动查看更多
// next.config.js
const nextConfig = {
	output: 'export'
};
BASH
滚动查看更多
# 构建静态文件
npm run build
# 输出到 out/ 目录
Docker 部署
DOCKERFILE
滚动查看更多
# 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 --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV production

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]
JS
滚动查看更多
// next.config.js
const nextConfig = {
	output: 'standalone'
};

常用 Hooks

路由相关
TSX
滚动查看更多
'use client';

import { useRouter, usePathname, useSearchParams, useParams } from 'next/navigation';

export function Component() {
	const router = useRouter();
	const pathname = usePathname();
	const searchParams = useSearchParams();
	const params = useParams();

	// router.push('/path')
	// router.replace('/path')
	// router.back()
	// router.refresh()

	return null;
}
服务端工具函数
TSX
滚动查看更多
import { headers, cookies } from 'next/headers';

async function ServerComponent() {
	// 读取请求头
	const headersList = headers();
	const userAgent = headersList.get('user-agent');

	// 读取 cookies
	const cookieStore = cookies();
	const token = cookieStore.get('token');

	return null;
}

另见

相关 Cheat Sheets