mcpField
N
Next.js

Next.js

github.com/oriolrius开发2437 次安装6 天前更新
代码仓库

关于 Next.js

Next.js 是面向 React Web 应用开发者的框架技能,侧重 App Router,也覆盖 Pages Router、服务端渲染、静态生成和 API 路由。内容包括路由、数据获取、错误处理与部署模式。

主要功能

  1. 01App Router 文件路由
  2. 02服务端与客户端组件指导
  3. 03数据获取和缓存模式
  4. 04加载、错误与未找到状态
  5. 05API 路由和中间件示例
  6. 06元数据与图像优化指导

适用场景

  1. 01规划 Next.js 应用的路由结构
  2. 02在服务端组件中获取并缓存数据
  3. 03实现 API 端点与访问检查
  4. 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 - 路由段的共享 UI
  • page.tsx - 路由的独有 UI
  • loading.tsx - 加载 UI
  • error.tsx - 错误 UI
  • not-found.tsx - 404 UI
  • route.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