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/