Skip to content

Next.js 全栈框架

Next.js 是 React 的生产级框架,提供 SSR、SSG、ISR、App Router 等特性,是全栈开发的首选。

App Router vs Pages Router

Next.js 13+ 引入 App Router(推荐),基于 React Server Components

app/
├── layout.tsx          ← 根布局(持久化,不重新渲染)
├── page.tsx            ← 首页 /
├── loading.tsx         ← 加载 UI(Suspense 边界)
├── error.tsx           ← 错误边界
├── not-found.tsx       ← 404 页面
├── (marketing)/        ← 路由组(不影响 URL)
│   ├── about/page.tsx  ← /about
│   └── blog/page.tsx   ← /blog
├── dashboard/
│   ├── layout.tsx      ← 嵌套布局
│   └── page.tsx        ← /dashboard
└── api/
    └── users/route.ts  ← API 路由

Server Components vs Client Components

tsx
// Server Component(默认):在服务器渲染,可直接访问数据库
// app/users/page.tsx
async function UsersPage() {
  // 直接在组件中 fetch,无需 useEffect
  const users = await db.user.findMany()

  return (
    <div>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  )
}

// Client Component:需要交互、状态、浏览器 API
// 'use client' 指令标记
'use client'

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

// 组合模式:Server Component 包含 Client Component
// app/dashboard/page.tsx(Server)
async function DashboardPage() {
  const data = await fetchDashboardData()  // 服务端数据获取

  return (
    <div>
      <h1>Dashboard</h1>
      <StaticChart data={data} />     {/* Server Component */}
      <InteractiveFilter />           {/* Client Component */}
    </div>
  )
}

数据获取

tsx
// fetch 自动去重和缓存
async function getData() {
  // 默认缓存(等同于 SSG)
  const res = await fetch('https://api.example.com/data')

  // 不缓存(等同于 SSR,每次请求都重新获取)
  const res = await fetch('https://api.example.com/data', {
    cache: 'no-store'
  })

  // 定时重新验证(ISR)
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 }  // 每小时重新验证
  })

  if (!res.ok) throw new Error('Failed to fetch')
  return res.json()
}

// 并行数据获取
async function Page() {
  // 同时发起,不串行等待
  const [user, posts] = await Promise.all([
    fetchUser(),
    fetchPosts()
  ])
  return <div>...</div>
}

// 流式渲染(Streaming)
import { Suspense } from 'react'

async function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* 快速内容立即显示 */}
      <QuickStats />
      {/* 慢速内容流式加载 */}
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}

路由与导航

tsx
// 动态路由:app/posts/[id]/page.tsx
interface Props {
  params: { id: string }
  searchParams: { page?: string }
}

async function PostPage({ params, searchParams }: Props) {
  const post = await fetchPost(params.id)
  const page = Number(searchParams.page ?? 1)
  return <PostDetail post={post} page={page} />
}

// 生成静态路径(SSG)
export async function generateStaticParams() {
  const posts = await fetchAllPosts()
  return posts.map(post => ({ id: String(post.id) }))
}

// 元数据
export async function generateMetadata({ params }: Props) {
  const post = await fetchPost(params.id)
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { images: [post.coverImage] }
  }
}

// 客户端导航
'use client'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'

function NavButton() {
  const router = useRouter()
  const pathname = usePathname()

  return (
    <button onClick={() => router.push('/dashboard')}>
      Go to Dashboard
    </button>
  )
}

API 路由(Route Handlers)

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

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const page = Number(searchParams.get('page') ?? 1)

  const users = await db.user.findMany({
    skip: (page - 1) * 10,
    take: 10
  })

  return NextResponse.json({ users, page })
}

export async function POST(request: NextRequest) {
  const body = await request.json()

  // 验证
  const result = userSchema.safeParse(body)
  if (!result.success) {
    return NextResponse.json({ error: result.error }, { status: 400 })
  }

  const user = await db.user.create({ data: result.data })
  return NextResponse.json(user, { status: 201 })
}

// app/api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await db.user.findUnique({ where: { id: params.id } })
  if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 })
  return NextResponse.json(user)
}

Server Actions

tsx
// 直接在组件中定义服务端操作(无需 API 路由)
'use server'

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')  // 重新验证缓存
  redirect('/posts')
}

// 在表单中使用
function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">发布</button>
    </form>
  )
}

// 配合 useFormState 处理错误
'use client'
import { useFormState, useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending } = useFormStatus()
  return <button disabled={pending}>{pending ? '提交中...' : '提交'}</button>
}

中间件

ts
// 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

  // 保护路由
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  // 添加请求头
  const response = NextResponse.next()
  response.headers.set('x-pathname', request.nextUrl.pathname)
  return response
}

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

总结

  • App Router 默认是 Server Components,需要交互才加 'use client'
  • 数据获取直接在 async 组件中 await,无需 useEffect
  • Server Actions 让表单提交无需手写 API 路由
  • 用 Suspense + loading.tsx 实现流式渲染,提升感知性能
  • 中间件处理认证、重定向等横切关注点

系统学习 Web 前端生态,深入底层架构