关于 Next.js
Next.js 是面向 React Web 应用开发者的框架技能,侧重 App Router,也覆盖 Pages Router、服务端渲染、静态生成和 API 路由。内容包括路由、数据获取、错误处理与部署模式。
主要功能
- 01App Router 文件路由
- 02服务端与客户端组件指导
- 03数据获取和缓存模式
- 04加载、错误与未找到状态
- 05API 路由和中间件示例
- 06元数据与图像优化指导
适用场景
- 01规划 Next.js 应用的路由结构
- 02在服务端组件中获取并缓存数据
- 03实现 API 端点与访问检查
- 04为页面添加元数据和加载错误状态
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ oriolrius/ pki-manager-web --skill next.js安装后在 Claude Code 中输入/next.js确认已加载。
详细介绍
Next.js
为 Next.js React 框架和现代 Web 应用开发提供专业协助。
概览
Next.js 是一个 React 框架,包含:
- App Router(Next.js 13+):基于文件的路由,支持 React Server Components
- Pages Router(旧版):传统的 Next.js 路由
- 服务端渲染(SSR)
- 静态站点生成(SSG)
- API 路由
- 内置优化功能
本指南重点介绍 App Router(现代方式)。
项目设置
创建新项目
# Create Next.js app (interactive)
npx create-next-app@latest
# With specific options
npx create-next-app@latest my-app --typescript --tailwind --app --use-npm
# Project structure
my-app/
├── app/ # App Router directory
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── globals.css # Global styles
│ └── api/ # API routes
├── public/ # Static assets
├── components/ # React components
├── lib/ # Utility functions
└── next.config.js # Next.js configuration
开发命令
# Start dev server
npm run dev
# Build for production
npm run build
# Start production server
npm start
# Run linter
npm run lint
App Router(Next.js 13+)
基于文件的路由
特殊文件:
layout.tsx- 路由段的共享 UIpage.tsx- 路由的独有 UIloading.tsx- 加载 UIerror.tsx- 错误 UInot-found.tsx- 404 UIroute.tsx- API 端点
示例结构:
app/
├── layout.tsx # Root layout
├── page.tsx # / route
├── about/
│ └── page.tsx # /about route
├── blog/
│ ├── layout.tsx # Blog layout
│ ├── page.tsx # /blog route
│ └── [slug]/
│ └── page.tsx # /blog/:slug route
└── dashboard/
├── layout.tsx
├── page.tsx # /dashboard route
├── settings/
│ └── page.tsx # /dashboard/settings
└── [id]/
└── page.tsx # /dashboard/:id
根布局
// app/layout.tsx
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'My App',
description: 'Built with Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
基本页面
// app/page.tsx
export default function Home() {
return (
<main>
<h1>Welcome to Next.js</h1>
<p>This is a Server Component by default</p>
</main>
)
}
动态路由
// app/blog/[slug]/page.tsx
export default function BlogPost({
params,
}: {
params: { slug: string }
}) {
return <h1>Blog Post: {params.slug}</h1>
}
// Generate static params for SSG
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
全捕获路由
// app/shop/[...slug]/page.tsx - Matches /shop/a, /shop/a/b, etc.
// app/docs/[[...slug]]/page.tsx - Optional catch-all, also matches /docs
export default function Page({
params,
}: {
params: { slug: string[] }
}) {
return <div>Path: {params.slug.join('/')}</div>
}
路由组
// Group routes without affecting URL structure
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx # /about
│ └── blog/
│ └── page.tsx # /blog
└── (shop)/
├── layout.tsx # Shop layout
├── products/
│ └── page.tsx # /products
└── cart/
└── page.tsx # /cart
服务端组件与客户端组件
服务端组件(默认)
// app/posts/page.tsx
// Server Component by default - runs on server
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
)
}
优势:
- 可访问后端资源
- 将敏感数据保留在服务端
- 减少客户端 JavaScript
- 性能更佳
客户端组件
// components/Counter.tsx
'use client' // Required for client components
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}
在以下情况下使用:
- 状态(
useState、useReducer) - 副作用(
useEffect) - 浏览器 API
- 事件监听器
- 自定义钩子
组合模式
// app/page.tsx (Server Component)
import ClientComponent from '@/components/ClientComponent'
import ServerComponent from '@/components/ServerComponent'
export default async function Page() {
const data = await fetchData()
return (
<div>
<ServerComponent data={data} />
<ClientComponent />
</div>
)
}
数据获取
服务端组件数据获取
// 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 <PostsList posts={posts} />
}
缓存策略
// No caching (dynamic)
fetch('https://api.example.com/data', { cache: 'no-store' })
// Cache indefinitely (static)
fetch('https://api.example.com/data', { cache: 'force-cache' })
// Revalidate after time
fetch('https://api.example.com/data', {
next: { revalidate: 60 } // 60 seconds
})
// Revalidate with tag
fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
})
// Then revalidate programmatically
import { revalidateTag } from 'next/cache'
revalidateTag('posts')
并行数据获取
export default async function Page() {
// Initiate both requests in parallel
const userPromise = getUser()
const postsPromise = getPosts()
// Wait for both
const [user, posts] = await Promise.all([
userPromise,
postsPromise,
])
return <Dashboard user={user} posts={posts} />
}
顺序数据获取
export default async function Page() {
// Fetch user first
const user = await getUser()
// Then fetch user's posts
const posts = await getUserPosts(user.id)
return <Profile user={user} posts={posts} />
}
加载和错误状态
加载 UI
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2" />
</div>
)
}
错误处理
// app/dashboard/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>
)
}
未找到
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
export default async function BlogPost({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return <article>{post.content}</article>
}
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return <div>Blog post not found</div>
}
API 路由
路由处理程序
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
// GET /api/posts
export async function GET(request: NextRequest) {
const posts = await getPosts()
return NextResponse.json(posts)
}
// POST /api/posts
export async function POST(request: NextRequest) {
const body = await request.json()
const post = await createPost(body)
return NextResponse.json(post, { status: 201 })
}
动态 API 路由
// app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id)
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await deletePost(params.id)
return NextResponse.json({ success: true })
}
请求辅助工具
// app/api/search/route.ts
export async function GET(request: NextRequest) {
// Query parameters
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('q')
// Headers
const token = request.headers.get('authorization')
// Cookies
const session = request.cookies.get('session')
const results = await search(query)
// Set cookies in response
const response = NextResponse.json(results)
response.cookies.set('last-search', query || '', {
httpOnly: true,
secure: true,
maxAge: 3600,
})
return response
}
中间件
// middleware.ts (root level)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Check authentication
const token = request.cookies.get('token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Add custom header
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
// Configure which routes to run middleware on
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*',
],
}
导航
Link 组件
import Link from 'next/link'
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog/my-post">Blog Post</Link>
{/* With prefetching disabled */}
<Link href="/heavy-page" prefetch={false}>
Heavy Page
</Link>
</nav>
)
}
编程式导航
'use client'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
export default function NavigationExample() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const handleNavigate = () => {
router.push('/dashboard')
// router.replace('/dashboard') // No history entry
// router.back()
// router.forward()
// router.refresh() // Refresh current route
}
return (
<div>
<p>Current path: {pathname}</p>
<p>Query param: {searchParams.get('id')}</p>
<button onClick={handleNavigate}>Go to Dashboard</button>
</div>
)
}
元数据和 SEO
静态元数据
// app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn more about our company',
keywords: ['about', 'company', 'team'],
openGraph: {
title: 'About Us',
description: 'Learn more about our company',
images: ['/og-image.png'],
},
twitter: {
card: 'summary_large_image',
title: 'About Us',
description: 'Learn more about our company',
images: ['/twitter-image.png'],
},
}
export default function AboutPage() {
return <div>About Us</div>
}
动态元数据
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): 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],
},
}
}
图像优化
import Image from 'next/image'
export default function Gallery() {
return (
<div>
{/* Local image */}
<Image
src="/hero.jpg"
alt="Hero"
width={800}
height={600}
priority // Load eagerly
/>
{/* Remote image */}
<Image
src="https://example.com/image.jpg"
alt="Remote"
width={800}
height={600}
loading="lazy"
/>
{/* Fill container */}
<div className="relative h-64">
<Image
src="/background.jpg"
alt="Background"
fill
className="object-cover"
/>
</div>
</div>
)
}
环境变量
// .env.local
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
// Server component (private vars)
const dbUrl = process.env.DATABASE_URL
// Client component (public vars only)
const apiUrl = process.env.NEXT_PUBLIC_API_URL
配置
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Image domains
images: {
domains: ['example.com', 'cdn.example.com'],
},
// Redirects
async redirects() {
return [
{
source: '/old-path',
destination: '/new-path',
permanent: true,
},
]
},
// Rewrites
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
]
},
// Headers
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
],
},
]
},
// Environment variables
env: {
CUSTOM_VAR: 'value',
},
}
module.exports = nextConfig
最佳实践
1. 默认使用服务端组件
// ✅ Use Server Components when possible
// app/page.tsx
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>
}
// ❌ Don't use Client Component unnecessarily
'use client'
export default function Page() {
// This doesn't need to be a Client Component
return <div>Static content</div>
}
2. 正确获取数据
// ✅ Fetch in Server Components
async function Page() {
const data = await fetch('...').then(r => r.json())
return <List data={data} />
}
// ❌ Don't fetch in Client Components if avoidable
'use client'
function Page() {
const [data, setData] = useState([])
useEffect(() => {
fetch('...').then(r => r.json()).then(setData)
}, [])
return <List data={data} />
}
3. 加载状态
// ✅ Use loading.tsx for automatic loading UI
// app/dashboard/loading.tsx
export default function Loading() {
return <Skeleton />
}
// ✅ Or use Suspense for granular control
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
)
}
4. 错误处理
// ✅ Use error.tsx for error boundaries
// app/dashboard/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Error: {error.message}</h2>
<button onClick={reset}>Retry</button>
</div>
)
}
5. 元数据
// ✅ Always add metadata for SEO
export const metadata = {
title: 'Page Title',
description: 'Page description',
}
// ✅ Use dynamic metadata for dynamic routes
export async function generateMetadata({ params }) {
const data = await getData(params.id)
return { title: data.title }
}
常见模式
身份验证检查
// middleware.ts
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
受保护的 API 路由
// app/api/protected/route.ts
export async function GET(request: NextRequest) {
const token = request.headers.get('authorization')
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const data = await getProtectedData()
return NextResponse.json(data)
}
表单处理
'use client'
export default function ContactForm() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const res = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(formData)),
headers: { 'Content-Type': 'application/json' },
})
if (res.ok) {
alert('Submitted!')
}
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Submit</button>
</form>
)
}
部署
Vercel(推荐)
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Production deployment
vercel --prod
Docker
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:18-alpine 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"]
资源
评论
支持 Markdown