Next.js App Router 实践从页面路由到服务端组件现代 Web 应用的架构演进一、Pages Router 的架构瓶颈当全栈需求超越静态生成Next.js 的 Pages Router 基于文件系统的路由映射每个 pages/ 目录下的文件对应一个路由。这种设计简单直观但随着应用复杂度增长暴露出几个结构性问题每个页面都是独立的 React 组件无法在路由级别共享布局状态如导航栏、侧栏getServerSideProps 和 getStaticProps 是页面级别的数据获取无法在组件级别按需获取API Routes 与页面路由混在同一目录缺乏清晰的关注点分离。App RouterNext.js 13通过引入 React Server Components 和嵌套布局来解决这些问题。但 App Router 不是 Pages Router 的简单升级而是一次架构范式的转变——从客户端渲染 服务端数据获取到服务端组件 客户端交互的混合架构。理解这个范式转变才能正确使用 App Router。二、App Router 的核心架构与渲染模型App Router 的核心变化是引入了三种渲染模式Server Components默认、Client Componentsuse client、Shared Components两者皆可。flowchart TD A[App Router 架构] -- B[Server Components] A -- C[Client Components] A -- D[布局与模板] B -- B1[默认模式: 无需标记] B -- B2[服务端渲染: 无 JS 发送到客户端] B -- B3[直接访问后端: DB/文件系统/API] B -- B4[限制: 无状态/无事件/无 Effect] C -- C1[显式标记: use client] C -- C2[客户端渲染: 发送 JS Bundle] C -- C3[完整 React 功能: useState/useEffect] C -- C4[限制: 无法直接访问后端] D -- D1[layout.tsx: 嵌套布局, 跨路由持久化] D -- D2[template.tsx: 嵌套模板, 路由切换重建] D -- D3[loading.tsx: Suspense 加载状态] D -- D4[error.tsx: 错误边界] B -- E[数据获取] C -- E E -- E1[async/await: 组件内直接 await] E -- E2[Suspense: 流式渲染] E -- E3[ISR: 增量静态再生] E -- E4[Parallel: 并行数据获取] style B fill:#e8f5e9 style C fill:#e1f5fe style E fill:#fff3e02.1 Server Components 与数据获取// app/products/page.tsx — Server Component 数据获取 // 设计意图在服务端组件中直接访问数据库 // 无需 API 层中转减少请求瀑布流 import { Suspense } from react; import { ProductList } from /components/ProductList; import { ProductFilters } from /components/ProductFilters; import { db } from /lib/db; // Server Component: 默认模式无需 use client // 此组件在服务端渲染不发送 JS 到客户端 async function ProductsPage({ searchParams, }: { searchParams: { category?: string; sort?: string }; }) { // 直接在组件中 await 数据获取 // Next.js 会在服务端执行此函数将结果序列化为 HTML const products await db.product.findMany({ where: searchParams.category ? { category: searchParams.category } : undefined, orderBy: searchParams.sort price ? { price: asc } : { createdAt: desc }, include: { category: true }, }); const categories await db.category.findMany(); return ( div classNameproducts-page {/* 客户端组件需要交互筛选、排序 */} ProductFilters categories{categories} / {/* Suspense 边界ProductList 内部数据加载时显示骨架屏 */} Suspense fallback{ProductListSkeleton /} ProductList products{products} / /Suspense /div ); } // 骨架屏组件 function ProductListSkeleton() { return ( div classNamegrid grid-cols-3 gap-4 {Array.from({ length: 6 }).map((_, i) ( div key{i} classNameanimate-pulse div classNameh-48 bg-gray-200 rounded / div classNameh-4 bg-gray-200 rounded mt-2 w-3/4 / div classNameh-4 bg-gray-200 rounded mt-1 w-1/2 / /div ))} /div ); } export default ProductsPage;2.2 嵌套布局与状态持久化// app/layout.tsx — 根布局 // 设计意图根布局在所有路由间共享不会在路由切换时重新渲染 // 保持全局状态如主题、用户信息的持久化 import ./globals.css; import { ThemeProvider } from /components/ThemeProvider; import { Navigation } from /components/Navigation; import type { Metadata } from next; export const metadata: Metadata { title: My App, description: Built with Next.js App Router, }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( html langzh-CN suppressHydrationWarning body ThemeProvider Navigation / main{children}/main /ThemeProvider /body /html ); } // app/dashboard/layout.tsx — 仪表盘嵌套布局 // 设计意图仪表盘的侧栏布局在子路由切换时保持不变 // 只有内容区域重新渲染 import { DashboardSidebar } from /components/DashboardSidebar; export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return ( div classNamedashboard-layout flex {/* 侧栏在子路由切换时不会重新渲染 */} DashboardSidebar / div classNamedashboard-content flex-1 {children} /div /div ); }三、Server Actions 与表单处理3.1 Server Actions 实现// app/actions/products.ts — Server Actions // 设计意图在服务端定义表单处理逻辑客户端直接调用 // 无需手动创建 API Route减少样板代码 use server; import { revalidatePath } from next/cache; import { redirect } from next/navigation; import { db } from /lib/db; import { productSchema } from /lib/validations; // Server Action: 在服务端执行客户端通过 RPC 调用 export async function createProduct(formData: FormData) { // 表单数据验证 const rawData { name: formData.get(name) as string, price: parseFloat(formData.get(price) as string), category: formData.get(category) as string, description: formData.get(description) as string, }; const validated productSchema.safeParse(rawData); if (!validated.success) { return { error: validated.error.flatten().fieldErrors }; } try { await db.product.create({ data: validated.data }); } catch (error) { return { error: { _form: [创建失败请重试] } }; } // 重新验证缓存确保列表页显示最新数据 revalidatePath(/products); redirect(/products); } export async function deleteProduct(productId: string) { try { await db.product.delete({ where: { id: productId } }); revalidatePath(/products); return { success: true }; } catch { return { error: 删除失败 }; } }3.2 客户端交互组件// components/ProductFilters.tsx — 客户端交互组件 // 设计意图需要用户交互筛选、排序的组件标记为 Client Component // 通过 URL searchParams 实现状态持久化 use client; import { useRouter, useSearchParams } from next/navigation; import { useCallback } from react; interface ProductFiltersProps { categories: Array{ id: string; name: string }; } export function ProductFilters({ categories }: ProductFiltersProps) { const router useRouter(); const searchParams useSearchParams(); const currentCategory searchParams.get(category) ?? ; const currentSort searchParams.get(sort) ?? ; const updateParams useCallback( (updates: Recordstring, string) { const params new URLSearchParams(searchParams.toString()); for (const [key, value] of Object.entries(updates)) { if (value) { params.set(key, value); } else { params.delete(key); } } router.push(/products?${params.toString()}); }, [router, searchParams], ); return ( div classNameproduct-filters flex gap-4 mb-6 select value{currentCategory} onChange{(e) updateParams({ category: e.target.value })} classNameborder rounded px-3 py-2 option value全部分类/option {categories.map((cat) ( option key{cat.id} value{cat.name} {cat.name} /option ))} /select select value{currentSort} onChange{(e) updateParams({ sort: e.target.value })} classNameborder rounded px-3 py-2 option value最新/option option valueprice价格升序/option /select /div ); }四、边界分析与架构权衡Server/Client 组件的边界划分组件树中 Server Component 和 Client Component 的边界划分直接影响性能。过多的 Client Component 会增加 JS Bundle 大小过多的 Server Component 会减少交互能力。原则是尽可能使用 Server Component只在需要交互useState、useEffect、事件处理时才转为 Client Component。数据获取的瀑布流问题Server Component 中的顺序 await 会导致请求瀑布流——第一个请求完成后才开始第二个。解决方案是并行发起请求Promise.all 或 Suspense 边界但需要理解 Next.js 的流式渲染机制。缓存策略的复杂性App Router 的缓存行为比 Pages Router 更复杂。fetch 请求默认被缓存revalidatePath 和 revalidateTag 控制缓存失效。理解缓存层级请求缓存、路由缓存、全路由缓存是正确使用 App Router 的前提。迁移的渐进性App Router 和 Pages Router 可以共存但两者之间的导航行为不同。Pages Router 的页面切换是完整的页面加载App Router 是客户端导航。混合使用时需要注意导航体验的一致性。五、总结Next.js App Router 通过 Server Components、嵌套布局和 Server Actions 重新定义了全栈 Web 应用的架构范式。Server Components 减少客户端 JS 体积嵌套布局实现跨路由的状态持久化Server Actions 简化表单处理逻辑。落地建议优先使用 Server Component只在需要交互时转为 Client Component利用 Suspense 实现流式渲染避免页面级加载阻塞通过 URL searchParams 管理筛选状态实现状态持久化和可分享理解 App Router 的缓存机制合理使用 revalidatePath 控制数据新鲜度。