Next.js 15 App Router服务端组件实战与数据获取模式

Next.js App Router的架构变革

Next.js 15的App Router引入了React Server Components(RSC)作为默认渲染模式,改变了前端开发的数据获取和组件设计范式。在传统Pages Router中,getServerSideProps和getStaticProps在页面级别获取数据;App Router将数据获取下沉到组件级别,Server Components可以直接访问数据库和文件系统,无需通过API层中转。这种模式对Web性能优化和前端工程化都有深远影响。

Server Components与Client Components区分

App Router中所有组件默认是Server Component,只有在文件顶部添加"use client"指令才变为Client Component。两者的核心区别:

// app/product/page.tsx - Server Component(默认)
// 可以直接访问数据库、读取文件系统、使用服务端API密钥
import { db } from '@/lib/db'

export default async function ProductPage() {
  // 直接在服务端查询数据库
  const products = await db.product.findMany({
    take: 20,
    orderBy: { createdAt: 'desc' }
  })
  
  return (
    <div>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  )
}

// app/components/ProductCard.tsx
// 不带 use client,仍然是 Server Component
export function ProductCard({ product }: { product: Product }) {
  return (
    <article>
      <h3>{product.name}</h3>
      <p>{product.description}</p>
    </article>
  )
}
// app/components/AddToCartButton.tsx - Client Component
"use client"

import { useState } from 'react'

export function AddToCartButton({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false)
  
  const handleClick = async () => {
    setLoading(true)
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId })
    })
    setLoading(false)
  }
  
  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? '添加中...' : '加入购物车'}
    </button>
  )
}

Server Component的代码不会打包到客户端JS bundle中,只有Client Component的代码会发送到浏览器。对于TypeScript实战场景,这意味着大型依赖库(如markdown渲染器、日期处理库)可以只在服务端运行,不增加客户端包体积。

数据获取模式:fetch缓存策略

Next.js 15扩展了原生fetch API,增加了缓存和重新验证配置:

// 1. 静态生成 + ISR(增量静态再生)
// 默认缓存,每小时重新生成
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }  // 3600秒后重新验证
  })
  return res.json()
}

// 2. 动态渲染(每次请求都获取最新数据)
async function getUserProfile(userId: string) {
  const res = await fetch(`https://api.example.com/users/${userId}`, {
    cache: 'no-store'  // 不缓存,每次请求都获取
  })
  return res.json()
}

// 3. 标签化缓存(按需重新验证)
async function getArticle(id: string) {
  const res = await fetch(`https://api.example.com/articles/${id}`, {
    next: { tags: ['article', `article-${id}`] }
  })
  return res.json()
}

// 在Server Action或Route Handler中按需刷新
import { revalidateTag } from 'next/cache'
async function updateArticle(id: string, data: any) {
  await db.article.update({ where: { id }, data })
  revalidateTag(`article-${id}`)  // 精准刷新该文章缓存
}

动态路由与布局嵌套

App Router使用文件系统定义路由,目录结构即路由层级:

app/
├── layout.tsx          # 根布局(所有页面共享)
├── page.tsx            # 首页
├── blog/
│   ├── layout.tsx      # 博客模块布局
│   ├── page.tsx        # 博客列表页
│   └── [slug]/
│       ├── page.tsx    # 博客详情页
│       └── loading.tsx # 加载UI

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

// params和searchParams在Server Component中可直接获取
export default async function BlogPost({
  params,
  searchParams
}: {
  params: { slug: string }
  searchParams: { preview?: string }
}) {
  const post = await getPost(params.slug)
  
  if (!post) notFound()
  
  const isPreview = searchParams.preview === 'true'
  
  return (
    <article>
      <h1>{post.title}</h1>
      {isPreview && <PreviewBanner />}
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

// 生成静态参数(构建时预渲染)
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}

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

Server Actions表单处理

Next.js 15的Server Actions允许在Server Component中直接定义表单提交处理逻辑,无需创建API路由:

// app/contact/page.tsx
import { revalidatePath } from 'next/cache'

// Server Action定义
async function submitContact(formData: FormData) {
  'use server'
  
  const name = formData.get('name')
  const email = formData.get('email')
  const message = formData.get('message')
  
  // 服务端验证
  if (!name || !email) {
    return { error: '姓名和邮箱不能为空' }
  }
  
  // 写入数据库
  await db.contact.create({
    data: { name, email, message: message as string }
  })
  
  // 刷新页面缓存
  revalidatePath('/contact')
  return { success: true }
}

export default function ContactPage() {
  return (
    <form action={submitContact}>
      <input type="text" name="name" placeholder="姓名" />
      <input type="email" name="email" placeholder="邮箱" />
      <textarea name="message" placeholder="留言"></textarea>
      <button type="submit">提交</button>
    </form>
  )
}

Server Action自动处理CSRF防护,表单提交后页面会自动重新渲染。这种模式在响应式布局和跨端小程序开发场景下也很适用,因为表单逻辑完全在服务端执行,客户端JS包不含业务逻辑。

Streaming与Suspense异步渲染

App Router支持React Streaming,通过Suspense边界实现渐进式页面加载:

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

export default function DashboardPage() {
  return (
    <div>
      <h1>仪表盘</h1>
      
      {/* 快速加载的部分立即渲染 */}
      <Suspense fallback={<Skeleton />}>
        <Stats />
      </Suspense>
      
      {/* 慢查询部分异步加载,不阻塞首屏 */}
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>
      
      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  )
}

// 每个Suspense内的组件独立获取数据
async function RevenueChart() {
  const data = await getRevenueData()  // 可能需要2秒
  return <Chart data={data} />
}

Streaming模式下,HTML流式发送到浏览器,已就绪的部分先渲染,慢的部分用fallback占位。首屏FCP(First Contentful Paint)大幅降低,LCP(Largest Contentful Paint)取决于最慢的Suspense边界。

中间件与路由守卫

Next.js 15的Middleware运行在Edge Runtime上,用于鉴权和路由重写:

// middleware.ts(项目根目录)
import { NextRequest, NextResponse } from 'next/server'
import { verifyToken } from '@/lib/auth'

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value
  
  // 公开路由直接放行
  const publicPaths = ['/login', '/register', '/api/auth']
  if (publicPaths.some(p => request.nextUrl.pathname.startsWith(p))) {
    return NextResponse.next()
  }
  
  // 验证Token
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  const user = await verifyToken(token)
  if (!user) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  // 注入用户信息到请求头
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-user-id', user.id)
  requestHeaders.set('x-user-role', user.role)
  
  return NextResponse.next({
    request: { headers: requestHeaders }
  })
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}

性能优化策略

App Router的包体积分析与优化:

// next.config.ts
import { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    optimizePackageImports: ['lucide-react', 'date-fns']
  },
  images: {
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [{ protocol: 'https', hostname: '**' }]
  },
  // 服务端组件中的大型依赖不会进入客户端bundle
  // 但Client Component中的依赖需要关注
}

分析客户端bundle组成:

# 安装分析工具
npm install @next/bundle-analyzer

# package.json
"scripts": {
  "analyze": "ANALYZE=true next build"
}

# 运行后打开 8888/8889 端口查看

常见优化手段:将日期处理库从moment.js切换到date-fns的tree-shakeable导入,图标库使用optimizePackageImports按需加载,图片使用next/image自动生成AVIF/WebP格式和响应式尺寸。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/nextjs15approuter-fu-wu-duan-zu-jian-shi-zhan-yu-shu-ju-huo/

(0)
小编小编
上一篇 20小时前
下一篇 20小时前

相关推荐