mcpField
N
nextjs-development

nextjs-development

github.com/manutej开发62472 次安装39 周前更新
代码仓库

关于 nextjs-development

面向使用 App Router 和全栈模式构建 Next.js 应用的开发者,涵盖路由、渲染、数据获取、API 处理器及生产实践,适用于 Web 应用和内容站点。

主要功能

  1. 01App Router 与文件路由
  2. 02Server 和 Client Components
  3. 03缓存与数据重新验证
  4. 04API Route Handlers 和中间件
  5. 05元数据与 SEO 模式
  6. 06图像优化与 Server Actions

适用场景

  1. 01构建全栈 React 应用
  2. 02创建支持增量重新验证的静态站点
  3. 03实现 API 后端和路由处理器
  4. 04开发注重 SEO 的内容站点

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/manutej/luxor-claude-marketplace --skill nextjs-development

安装后在 Claude Code 中输入/nextjs-development确认已加载。

附带文件EXAMPLES.mdREADME.md

详细介绍

Next.js 开发技能

本技能根据 Next.js 官方文档,全面介绍如何使用 App Router、Server Components、数据获取模式、路由、API 路由、中间件和全栈开发技术构建现代 Next.js 应用。

何时使用此技能

在以下情况使用此技能:

  • 构建带有服务器端渲染(SSR)的全栈 React 应用
  • 创建支持增量静态再生成(ISR)的静态网站
  • 使用 React Server Components 开发现代 Web 应用
  • 使用无服务器路由处理程序构建 API 后端
  • 实现包含元数据和 Open Graph 的 SEO 优化应用
  • 使用内置优化功能创建可用于生产环境的 Web 应用
  • 构建电商网站、博客、仪表板或内容驱动型网站
  • 实现身份验证、数据获取和复杂路由模式
  • 自动优化图片、字体和性能
  • 部署具备边缘计算能力的无服务器应用

核心概念

App Router

App Router 是 Next.js 基于 React Server Components 构建的现代路由系统。它使用 app 目录实现基于文件的路由,并提供增强功能。

基本应用结构:

app/
├── layout.tsx          # Root layout (required)
├── page.tsx            # Home page
├── loading.tsx         # Loading UI
├── error.tsx           # Error UI
├── not-found.tsx       # 404 page
├── about/
│   └── page.tsx        # /about route
└── blog/
    ├── page.tsx        # /blog route
    ├── [slug]/
    │   └── page.tsx    # /blog/[slug] dynamic route
    └── layout.tsx      # Blog layout

根布局(必需):

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

页面组件:

// app/page.tsx
export default function HomePage() {
  return (
    <main>
      <h1>Welcome to Next.js</h1>
      <p>Building modern web applications</p>
    </main>
  )
}
Server Components

Server Components 是在服务器上渲染的 React 组件。它们是 App Router 中的默认组件,可提供更好的性能。

Server Component(默认):

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'force-cache' // Static generation
  })
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

Client Component(需要时使用):

'use client'

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>
  )
}

混合使用 Server 和 Client Components:

// app/dashboard/page.tsx (Server Component)
import ClientCounter from './ClientCounter'

async function getData() {
  const res = await fetch('https://api.example.com/data')
  return res.json()
}

export default async function DashboardPage() {
  const data = await getData()

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Server data: {data.value}</p>
      {/* Client component for interactivity */}
      <ClientCounter />
    </div>
  )
}
数据获取

Next.js 扩展了原生 fetch() API,加入了自动缓存和重新验证功能。

静态数据获取(默认):

async function getStaticData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache' // Default, equivalent to getStaticProps
  })
  return res.json()
}

export default async function Page() {
  const data = await getStaticData()
  return <div>{data.title}</div>
}

动态数据获取:

async function getDynamicData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'no-store' // Equivalent to getServerSideProps
  })
  return res.json()
}

export default async function Page() {
  const data = await getDynamicData()
  return <div>{data.title}</div>
}

重新验证(ISR):

async function getRevalidatedData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 } // Revalidate every 60 seconds
  })
  return res.json()
}

export default async function Page() {
  const data = await getRevalidatedData()
  return <div>{data.title}</div>
}

并行数据获取:

async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`)
  return res.json()
}

async function getUserPosts(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}/posts`)
  return res.json()
}

export default async function UserPage({ params }: { params: { id: string } }) {
  // Fetch in parallel
  const [user, posts] = await Promise.all([
    getUser(params.id),
    getUserPosts(params.id)
  ])

  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

顺序数据获取:

async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`)
  return res.json()
}

async function getRecommendations(preferences: string[]) {
  const res = await fetch('https://api.example.com/recommendations', {
    method: 'POST',
    body: JSON.stringify({ preferences })
  })
  return res.json()
}

export default async function UserPage({ params }: { params: { id: string } }) {
  // First fetch user
  const user = await getUser(params.id)

  // Then fetch recommendations based on user data
  const recommendations = await getRecommendations(user.preferences)

  return (
    <div>
      <h1>{user.name}</h1>
      <h2>Recommendations</h2>
      <ul>
        {recommendations.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  )
}
路由

Next.js 在 app 目录中使用基于文件系统的路由。

动态路由:

// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>Post: {params.slug}</h1>
}

// Generates static pages for these slugs at build time
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())

  return posts.map((post) => ({
    slug: post.slug,
  }))
}

全匹配路由:

// app/docs/[...slug]/page.tsx
export default function DocsPage({ params }: { params: { slug: string[] } }) {
  // /docs/a/b/c -> params.slug = ['a', 'b', 'c']
  return <h1>Docs: {params.slug.join('/')}</h1>
}

可选全匹配路由:

// app/shop/[[...slug]]/page.tsx
export default function ShopPage({ params }: { params: { slug?: string[] } }) {
  // /shop -> params.slug = undefined
  // /shop/clothes -> params.slug = ['clothes']
  // /shop/clothes/tops -> params.slug = ['clothes', 'tops']
  return <h1>Shop: {params.slug?.join('/') || 'All'}</h1>
}

路由组:

app/
├── (marketing)/          # Route group (not in URL)
│   ├── about/
│   │   └── page.tsx      # /about
│   └── contact/
│       └── page.tsx      # /contact
└── (shop)/
    ├── products/
    │   └── page.tsx      # /products
    └── cart/
        └── page.tsx      # /cart

并行路由:

app/
├── @analytics/
│   └── page.tsx
├── @team/
│   └── page.tsx
└── layout.tsx

// app/layout.tsx
export default function Layout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <>
      {children}
      {analytics}
      {team}
    </>
  )
}

拦截路由:

app/
├── feed/
│   └── page.tsx
├── photo/
│   └── [id]/
│       └── page.tsx
└── @modal/
    └── (.)photo/
        └── [id]/
            └── page.tsx  # Intercepts /photo/[id] when navigating from /feed
布局

布局包裹页面,并在导航过程中保留状态。

嵌套布局:

// app/layout.tsx (Root Layout)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>Global Navigation</nav>
        </header>
        {children}
        <footer>Global Footer</footer>
      </body>
    </html>
  )
}

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

// app/dashboard/page.tsx
export default function DashboardPage() {
  return <h1>Dashboard</h1>
}

模板(导航时重新渲染):

// app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
  return (
    <div>
      {/* This creates a new instance on each navigation */}
      {children}
    </div>
  )
}
加载 UI

特殊的 loading.tsx 文件可使用 React Suspense 创建加载状态。

加载状态:

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

// app/dashboard/page.tsx
async function getData() {
  const res = await fetch('https://api.example.com/data')
  return res.json()
}

export default async function DashboardPage() {
  const data = await getData()
  return <div>{data.content}</div>
}

使用 Suspense 流式传输:

// app/page.tsx
import { Suspense } from 'react'

async function SlowComponent() {
  await new Promise(resolve => setTimeout(resolve, 3000))
  return <div>Slow data loaded</div>
}

export default function Page() {
  return (
    <div>
      <h1>Page</h1>
      <Suspense fallback={<div>Loading slow component...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}
错误处理

特殊的 error.tsx 文件使用错误边界处理错误。

错误边界:

// app/error.tsx
'use client' // Error components must be Client Components

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/global-error.tsx
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <h2>Application Error</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

未找到:

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

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

路由处理程序允许你使用 Web Request 和 Response API 创建 API 端点。

基本 API 路由:

// app/api/hello/route.ts
export async function GET(request: Request) {
  return Response.json({ message: 'Hello from Next.js!' })
}

动态路由处理程序:

// app/api/posts/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const id = params.id
  const post = await db.post.findUnique({ where: { id } })

  if (!post) {
    return new Response('Post not found', { status: 404 })
  }

  return Response.json(post)
}

export async function DELETE(
  request: Request,
  { params }: { params: { id: string } }
) {
  await db.post.delete({ where: { id: params.id } })
  return new Response(null, { status: 204 })
}

带请求正文的 POST 请求:

// app/api/posts/route.ts
export async function POST(request: Request) {
  const body = await request.json()

  const post = await db.post.create({
    data: {
      title: body.title,
      content: body.content,
    }
  })

  return Response.json(post, { status: 201 })
}

带标头的请求:

// app/api/protected/route.ts
export async function GET(request: Request) {
  const token = request.headers.get('authorization')

  if (!token) {
    return new Response('Unauthorized', { status: 401 })
  }

  const user = await verifyToken(token)
  return Response.json({ user })
}

搜索参数:

// app/api/search/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const query = searchParams.get('q')
  const page = searchParams.get('page') || '1'

  const results = await search(query, parseInt(page))

  return Response.json(results)
}

CORS 标头:

// app/api/public/route.ts
export async function GET(request: Request) {
  const data = { message: 'Public API' }

  return Response.json(data, {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}

export async function OPTIONS(request: Request) {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}
中间件

中间件在请求完成之前运行,可让你修改响应。

基本中间件:

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

export function middleware(request: NextRequest) {
  // Clone the request headers
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-custom-header', 'custom-value')

  // Return response with modified headers
  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  })
}

export const config = {
  matcher: '/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')?.value

  // Redirect to login if no token
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // Redirect to dashboard if already logged in
  if (token && request.nextUrl.pathname === '/login') {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/login'],
}

地理位置与重写:

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

export function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US'

  // Rewrite based on country
  if (country === 'GB') {
    return NextResponse.rewrite(new URL('/gb' + request.nextUrl.pathname, request.url))
  }

  return NextResponse.next()
}

速率限制:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
})

export async function middleware(request: NextRequest) {
  const ip = request.ip ?? '127.0.0.1'
  const { success } = await ratelimit.limit(ip)

  if (!success) {
    return new Response('Too Many Requests', { status: 429 })
  }

  return NextResponse.next()
}

export const config = {
  matcher: '/api/:path*',
}
元数据和 SEO

Next.js 提供 Metadata API,用于定义页面元数据。

静态元数据:

// app/about/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'About Us',
  description: 'Learn more about our company',
  openGraph: {
    title: 'About Us',
    description: 'Learn more about our company',
    images: ['/og-image.jpg'],
  },
  twitter: {
    card: 'summary_large_image',
  },
}

export default function AboutPage() {
  return <h1>About Us</h1>
}

动态元数据:

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

type 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: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
      type: 'article',
      publishedTime: post.publishedAt,
    },
  }
}

export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug)
  return <article>{post.content}</article>
}

带图标的元数据:

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

export const metadata: Metadata = {
  title: {
    default: 'My App',
    template: '%s | My App',
  },
  description: 'My application description',
  icons: {
    icon: '/favicon.ico',
    apple: '/apple-icon.png',
  },
  manifest: '/manifest.json',
}
图片优化

Next.js 使用 Image 组件自动优化图片。

基本图片:

import Image from 'next/image'

export default function Page() {
  return (
    <Image
      src="/profile.jpg"
      alt="Profile picture"
      width={500}
      height={500}
    />
  )
}

远程图片:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
    ],
  },
}

// Component
import Image from 'next/image'

export default function Page() {
  return (
    <Image
      src="https://images.unsplash.com/photo-1234567890"
      alt="Photo"
      width={800}
      height={600}
      priority // Load image with high priority
    />
  )
}

填充容器:

import Image from 'next/image'

export default function Page() {
  return (
    <div style={{ position: 'relative', width: '100%', height: '400px' }}>
      <Image
        src="/background.jpg"
        alt="Background"
        fill
        style={{ objectFit: 'cover' }}
      />
    </div>
  )
}

响应式图片:

import Image from 'next/image'

export default function Page() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1920}
      height={1080}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    />
  )
}
字体优化

Next.js 使用 next/font 自动优化字体。

Google Fonts:

// 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 }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={`${inter.className} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}

本地字体:

// app/layout.tsx
import localFont from 'next/font/local'

const myFont = localFont({
  src: './fonts/my-font.woff2',
  display: 'swap',
  variable: '--font-my-font',
})

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

工作流模式

创建新页面
  1. 在 app 目录中创建新文件夹
  2. 添加 page.tsx 文件
  3. 导出一个默认组件
  4. 可选择添加布局、加载和错误文件
// app/products/page.tsx
export default function ProductsPage() {
  return <h1>Products</h1>
}

// app/products/layout.tsx
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <nav>Products Navigation</nav>
      {children}
    </div>
  )
}

// app/products/loading.tsx
export default function Loading() {
  return <div>Loading products...</div>
}
服务端操作

服务端操作允许你直接从客户端组件运行服务端代码。

使用服务端操作的表单:

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

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

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

  revalidatePath('/posts')
}

// app/new-post/page.tsx
import { createPost } from '../actions'

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit">Create Post</button>
    </form>
  )
}

使用 useTransition 的服务端操作:

// app/actions.ts
'use server'

export async function updateUser(userId: string, data: UserData) {
  await db.user.update({
    where: { id: userId },
    data,
  })
  return { success: true }
}

// app/profile/page.tsx
'use client'

import { useTransition } from 'react'
import { updateUser } from '../actions'

export default function ProfilePage() {
  const [isPending, startTransition] = useTransition()

  const handleSubmit = (formData: FormData) => {
    startTransition(async () => {
      await updateUser('user-id', {
        name: formData.get('name') as string,
      })
    })
  }

  return (
    <form action={handleSubmit}>
      <input name="name" />
      <button disabled={isPending}>
        {isPending ? 'Saving...' : 'Save'}
      </button>
    </form>
  )
}
身份验证模式

使用中间件进行基本身份验证:

// lib/auth.ts
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

export async function getSession() {
  const session = cookies().get('session')?.value
  if (!session) return null
  return await verifySession(session)
}

export async function requireAuth() {
  const session = await getSession()
  if (!session) {
    redirect('/login')
  }
  return session
}

// app/dashboard/page.tsx
import { requireAuth } from '@/lib/auth'

export default async function DashboardPage() {
  const session = await requireAuth()

  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
    </div>
  )
}

// app/api/login/route.ts
import { cookies } from 'next/headers'

export async function POST(request: Request) {
  const { email, password } = await request.json()

  const user = await validateCredentials(email, password)

  if (!user) {
    return Response.json({ error: 'Invalid credentials' }, { status: 401 })
  }

  const session = await createSession(user)
  cookies().set('session', session, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // 1 week
  })

  return Response.json({ success: true })
}
数据库模式

在服务端组件中使用 Prisma:

// lib/db.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = global as unknown as { prisma: PrismaClient }

export const prisma = globalForPrisma.prisma || new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

// app/posts/page.tsx
import { prisma } from '@/lib/db'

async function getPosts() {
  return await prisma.post.findMany({
    orderBy: { createdAt: 'desc' },
    include: { author: true },
  })
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <div>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>By {post.author.name}</p>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  )
}
缓存策略

按需重新验证:

// app/actions.ts
'use server'

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

export async function createPost(data: PostData) {
  await db.post.create({ data })

  // Revalidate specific path
  revalidatePath('/posts')

  // Or revalidate by tag
  revalidateTag('posts')
}

// Fetch with cache tags
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] }
  })
  return res.json()
}

基于时间的重新验证:

async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 } // Revalidate every hour
  })
  return res.json()
}

路由段配置:

// app/blog/page.tsx
export const revalidate = 3600 // Revalidate every hour
export const dynamic = 'force-static' // Force static generation
export const fetchCache = 'force-cache' // Force cache for all fetch requests

export default async function BlogPage() {
  const posts = await getPosts()
  return <div>{/* ... */}</div>
}

最佳实践

1. 默认使用服务端组件

服务端组件是默认选项,能提供更好的性能。仅在需要时使用客户端组件。

// ✅ Good - 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>
}

// ✅ Good - Client Component only when needed
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}
2. 在需要的位置获取数据

不要通过 props 层层传递数据。在需要数据的组件中获取数据。

// ✅ Good - Fetch where needed
async function Header() {
  const user = await getUser()
  return <div>Welcome, {user.name}</div>
}

async function Posts() {
  const posts = await getPosts()
  return <div>{/* render posts */}</div>
}

export default function Page() {
  return (
    <>
      <Header />
      <Posts />
    </>
  )
}
3. 并行获取数据

尽可能并行获取数据,以缩短加载时间。

// ✅ Good - Parallel fetching
export default async function Page() {
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts()
  ])

  return <div>{/* ... */}</div>
}
4. 优化图片

始终使用 Image 组件进行自动优化。

// ✅ Good
import Image from 'next/image'

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  priority
/>

// ❌ Bad
<img src="/hero.jpg" alt="Hero" />
5. 使用 Metadata API 优化 SEO

为每个页面定义元数据。

// ✅ Good
export const metadata = {
  title: 'My Page',
  description: 'Page description',
}

export default function Page() {
  return <div>Content</div>
}
6. 实现错误边界

添加 error.tsx 文件以处理错误。

// app/error.tsx
'use client'

export default function Error({ error, reset }: { error: Error, reset: () => void }) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  )
}
7. 使用加载状态

添加 loading.tsx 文件以显示加载界面。

// app/loading.tsx
export default function Loading() {
  return <div>Loading...</div>
}
8. 使用路由处理程序实现 API

使用路由处理程序,而不是 pages 目录中的 API 路由。

// ✅ Good - app/api/users/route.ts
export async function GET(request: Request) {
  const users = await db.user.findMany()
  return Response.json(users)
}
9. 使用中间件处理通用逻辑

在中间件中实现身份验证、重定向和重写。

// middleware.ts
export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}
10. 优化字体

使用 next/font 自动优化字体。

// ✅ Good
import { Inter } from 'next/font/google'

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

export default function RootLayout({ children }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

常见模式

博客模式
// app/blog/page.tsx
import Link from 'next/link'

async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 }
  })
  return res.json()
}

export default async function BlogPage() {
  const posts = await getPosts()

  return (
    <div>
      <h1>Blog</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>
            <Link href={`/blog/${post.slug}`}>{post.title}</Link>
          </h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  if (!res.ok) return null
  return res.json()
}

export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  return posts.map((post) => ({ slug: post.slug }))
}

export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)
  if (!post) return {}

  return {
    title: post.title,
    description: post.excerpt,
  }
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound()
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}
电子商务模式
// app/products/page.tsx
import { Suspense } from 'react'
import ProductGrid from './ProductGrid'
import Filters from './Filters'

export default function ProductsPage({
  searchParams,
}: {
  searchParams: { category?: string; sort?: string }
}) {
  return (
    <div>
      <h1>Products</h1>
      <Filters />
      <Suspense fallback={<div>Loading products...</div>}>
        <ProductGrid searchParams={searchParams} />
      </Suspense>
    </div>
  )
}

// app/products/ProductGrid.tsx
async function getProducts(category?: string, sort?: string) {
  const params = new URLSearchParams()
  if (category) params.set('category', category)
  if (sort) params.set('sort', sort)

  const res = await fetch(`https://api.example.com/products?${params}`, {
    next: { revalidate: 300 }
  })
  return res.json()
}

export default async function ProductGrid({
  searchParams,
}: {
  searchParams: { category?: string; sort?: string }
}) {
  const products = await getProducts(searchParams.category, searchParams.sort)

  return (
    <div className="grid">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

// app/products/[id]/page.tsx
async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`)
  return res.json()
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id)

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>${product.price}</p>
      <AddToCartButton productId={product.id} />
    </div>
  )
}
仪表盘模式
// app/dashboard/layout.tsx
import { requireAuth } from '@/lib/auth'
import Sidebar from './Sidebar'

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const session = await requireAuth()

  return (
    <div className="dashboard">
      <Sidebar user={session.user} />
      <main>{children}</main>
    </div>
  )
}

// app/dashboard/page.tsx
import { Suspense } from 'react'
import Stats from './Stats'
import RecentActivity from './RecentActivity'
import Chart from './Chart'

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>

      <Suspense fallback={<div>Loading stats...</div>}>
        <Stats />
      </Suspense>

      <div className="grid">
        <Suspense fallback={<div>Loading chart...</div>}>
          <Chart />
        </Suspense>

        <Suspense fallback={<div>Loading activity...</div>}>
          <RecentActivity />
        </Suspense>
      </div>
    </div>
  )
}

总结

此 Next.js 开发技能涵盖:

  1. App Router:基于文件系统的现代路由
  2. 服务端组件:默认在服务端渲染,以获得更好的性能
  3. 数据获取:扩展的 fetch API,支持缓存和重新验证
  4. 路由:动态路由、全匹配路由、路由组、并行路由
  5. 布局:嵌套布局、模板和布局组合
  6. 加载状态:使用 Suspense 自动显示加载界面
  7. 错误处理:错误边界和未找到页面
  8. API 路由:使用 Web API 的路由处理程序
  9. 中间件:拦截和修改请求
  10. 元数据:使用 Metadata API 优化 SEO
  11. 图片优化:使用 Image 组件自动优化图片
  12. 字体优化:使用 next/font 自动加载字体
  13. 服务端操作:从客户端组件执行服务端变更
  14. 身份验证:会话管理和受保护路由
  15. 最佳实践:性能优化、缓存策略和常见模式

所有模式均基于 Next.js 官方文档(信任评分:10),代表了现代 Next.js 13+ App Router 开发实践。

评论

支持 Markdown