入门 {.cols-2}
创建项目
npx create-next-app@latest my-app
npx create-next-app@latest my-app --typescript
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/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── globals.css
│ ├── api/
│ │ └── route.ts
│ └── [slug]/
│ └── page.tsx
├── components/
├── lib/
├── public/
├── next.config.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 (分组) |
动态路由
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 路由
interface Props {
params: { slug: string[] };
}
export default function Shop({ params }: Props) {
return <div>{params.slug.join('/')}</div>;
}
路由组
app/
├── (marketing)/
│ ├── about/page.tsx
│ └── blog/page.tsx
├── (shop)/
│ └── cart/page.tsx
└── layout.tsx
并行路由
export default function Layout({
children,
team,
analytics
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<>
{children}
{team}
{analytics}
</>
);
}
页面与布局 {.cols-2}
页面组件
export default function Home() {
return (
<main>
<h1>Welcome to Next.js</h1>
</main>
);
}
布局组件
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>
);
}
嵌套布局
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard">
<nav>Dashboard Nav</nav>
<main>{children}</main>
</div>
);
}
模板 (Template)
export default function Template({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
Loading UI
export default function Loading() {
return (
<div className="loading">
<div className="spinner" />
<p>Loading...</p>
</div>
);
}
Error 处理
'use client';
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
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>;
}
缓存策略
fetch('https://api.example.com/data');
fetch('https://api.example.com/data', {
cache: 'no-store'
});
fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
});
fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
});
重新验证
'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
'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');
}
在表单中使用
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 }
});
}
<form action={updatePost.bind(null, post.id)}>
<input name="title" />
<button>Update</button>
</form>;
'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>
);
}
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Submitting...' : 'Submit'}</button>;
}
API 路由 {.cols-2}
基本路由
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 });
}
动态路由
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);
}
请求处理
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('q');
const headersList = request.headers;
const token = headersList.get('authorization');
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
});
cookieStore.delete('token');
return NextResponse.json({ success: true });
}
导航 {.cols-2}
Link 组件
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>;
}
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'
}
};
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 模板
export const metadata: Metadata = {
title: {
template: '%s | My App',
default: 'My App'
}
};
export const metadata: Metadata = {
title: 'About'
};
优化 {.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 优化
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
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*']
};
认证检查
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
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*'
}
];
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY'
}
]
}
];
}
};
module.exports = nextConfig;
环境变量
DATABASE_URL=postgresql://...
API_SECRET=secret
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 部署
npm i -g vercel
vercel
vercel --prod
静态导出
const nextConfig = {
output: 'export'
};
npm run build
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"]
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();
return null;
}
服务端工具函数
import { headers, cookies } from 'next/headers';
async function ServerComponent() {
const headersList = headers();
const userAgent = headersList.get('user-agent');
const cookieStore = cookies();
const token = cookieStore.get('token');
return null;
}
另见 {.cols-1}