第一章:Next.js App Router——现代 Web 开发的范式
第一章:Next.js App Router——现代 Web 开发的范式
App Router 是 Next.js 13+ 引入的全新架构,它重新定义了"什么在服务端运行,什么在客户端运行"。理解这个模型,是写出高性能、可维护 SaaS 的基础。
一、Server Components vs Client Components
React 历史:
v1-v17(2013-2021):所有组件都在客户端渲染(CSR)
Next.js Pages Router:提供 SSR/SSG,但模型复杂
React 18 + Next.js 13:Server Components——服务端运行的 React 组件
核心区别:
Server Component(默认):在服务器上渲染,HTML 直接发到客户端
Client Component(加 "use client"):在浏览器中运行,有状态和交互
// app/dashboard/page.tsx
// 默认就是 Server Component(不需要声明)
import { createClient } from '@/lib/supabase/server'
import { UserStats } from './user-stats' // Client Component(有图表交互)
import { RecentActivity } from './activity' // Server Component(静态列表)
// 这个函数在服务器上执行!直接查询数据库,无需 API 请求
export default async function DashboardPage() {
const supabase = createClient()
// 直接在组件中查询 DB(服务端!)
const { data: stats } = await supabase
.from('usage_stats')
.select('*')
.single()
return (
<div className="p-6">
<h1 className="text-2xl font-bold">Dashboard</h1>
{/* Client Component:传入数据,客户端处理交互 */}
<UserStats initialData={stats} />
{/* Server Component:纯展示,不需要在客户端渲染 */}
<RecentActivity />
</div>
)
}
// app/dashboard/user-stats.tsx
"use client" // 声明为 Client Component
import { useState } from 'react'
import { Line } from 'recharts' // 图表库(只在客户端有意义)
interface UserStatsProps {
initialData: any
}
export function UserStats({ initialData }: UserStatsProps) {
const [period, setPeriod] = useState<'7d' | '30d'>('7d')
// 可以使用 useState, useEffect, onClick 等
return (
<div>
<button onClick={() => setPeriod('7d')}>7 天</button>
<button onClick={() => setPeriod('30d')}>30 天</button>
<Line data={initialData} />
</div>
)
}
判断规则(记住这个 checklist):
需要 "use client" 的情况:
✅ 使用 useState / useReducer
✅ 使用 useEffect
✅ 使用 onClick, onChange 等事件处理器
✅ 使用浏览器 API(window, localStorage)
✅ 使用第三方需要浏览器环境的库(charts, maps)
不需要 "use client"(保持 Server Component):
✅ 只展示数据(无交互)
✅ 直接访问数据库 / 后端 API
✅ 访问文件系统、环境变量(服务端私密)
✅ SEO 重要的页面(服务端渲染,爬虫可以看到内容)
二、App Router 文件系统路由
app/
layout.tsx → 根布局(所有页面共享的 shell)
page.tsx → 首页 /
loading.tsx → 加载状态(自动 Suspense)
error.tsx → 错误边界
not-found.tsx → 404 页面
dashboard/
layout.tsx → Dashboard 布局(侧边栏等)
page.tsx → /dashboard
loading.tsx → /dashboard 加载状态
settings/
page.tsx → /dashboard/settings
projects/
page.tsx → /dashboard/projects
[id]/ → 动态路由
page.tsx → /dashboard/projects/[id]
api/
stripe/
webhook/
route.ts → POST /api/stripe/webhook(API Route)
(auth)/ → 路由组(括号不出现在 URL 中)
login/
page.tsx → /login
register/
page.tsx → /register
layout.tsx → 认证页面专用布局(不带侧边栏)
// app/layout.tsx - 根布局
import { Inter } from 'next/font/google'
import { Providers } from './providers' // 所有 Context Provider
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: {
template: '%s | YourSaaS', // 子页面会替换 %s
default: 'YourSaaS - 描述',
},
description: '你的产品描述(SEO 重要)',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh">
<body className={inter.className}>
<Providers> {/* QueryClient, ThemeProvider 等 */}
{children}
</Providers>
</body>
</html>
)
}
三、Server Actions——表单提交的新范式
Server Actions 让你不需要写 API Route 就能从客户端调用服务端函数:
// app/actions/create-project.ts
"use server"
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'
const CreateProjectSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
})
export async function createProject(formData: FormData) {
// 这个函数在服务器上执行,可以直接访问数据库
const supabase = createClient()
// 验证输入(服务端验证!)
const result = CreateProjectSchema.safeParse({
name: formData.get('name'),
description: formData.get('description'),
})
if (!result.success) {
return { error: result.error.flatten().fieldErrors }
}
// 获取当前用户
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login')
// 插入数据库
const { data, error } = await supabase
.from('projects')
.insert({ ...result.data, user_id: user.id })
.select()
.single()
if (error) return { error: { form: error.message } }
// 刷新缓存,让页面重新获取数据
revalidatePath('/dashboard/projects')
redirect(`/dashboard/projects/${data.id}`)
}
// app/dashboard/projects/new/page.tsx
import { createProject } from '@/app/actions/create-project'
export default function NewProjectPage() {
return (
<form action={createProject}> {/* 直接传入 Server Action */}
<input name="name" placeholder="项目名称" required />
<textarea name="description" placeholder="描述(可选)" />
<button type="submit">创建项目</button>
</form>
)
}
四、Streaming 与 Suspense——渐进式加载 UI
// app/dashboard/page.tsx
import { Suspense } from 'react'
// 骨架屏组件
function StatsSkeleton() {
return <div className="animate-pulse bg-gray-200 h-32 rounded-lg" />
}
// 慢加载组件(可能需要查询多个表)
async function DashboardStats() {
// 模拟慢查询(实际是复杂的统计查询)
const [revenue, users, churn] = await Promise.all([
fetchRevenue(), // 需要 1s
fetchUserCount(), // 需要 0.5s
fetchChurnRate(), // 需要 1.5s
])
return (
<div className="grid grid-cols-3 gap-4">
<StatCard title="MRR" value={`$${revenue}`} />
<StatCard title="活跃用户" value={users} />
<StatCard title="流失率" value={`${churn}%`} />
</div>
)
}
export default function DashboardPage() {
return (
<div>
{/* Suspense:DashboardStats 加载时显示骨架屏 */}
<Suspense fallback={<StatsSkeleton />}>
<DashboardStats /> {/* 慢加载不阻塞整个页面! */}
</Suspense>
{/* 其他内容立即渲染,不等待上面的组件 */}
<QuickActions />
<RecentActivity />
</div>
)
}
五、完整项目结构(SaaS 最佳实践)
your-saas/
├── app/
│ ├── (auth)/ → 认证相关页面(无侧边栏)
│ │ ├── login/
│ │ ├── register/
│ │ └── layout.tsx
│ ├── (dashboard)/ → 需要登录的页面
│ │ ├── dashboard/
│ │ ├── settings/
│ │ └── layout.tsx → 带侧边栏的布局
│ ├── (marketing)/ → 公开页面(Landing Page, 定价页)
│ │ ├── page.tsx → Landing Page
│ │ ├── pricing/
│ │ └── blog/
│ ├── api/ → API Routes
│ │ └── stripe/
│ │ └── webhook/
│ │ └── route.ts
│ ├── actions/ → Server Actions
│ └── layout.tsx → 根布局
├── components/
│ ├── ui/ → shadcn/ui 基础组件
│ ├── auth/ → 认证相关组件
│ └── dashboard/ → Dashboard 专用组件
├── lib/
│ ├── supabase/
│ │ ├── client.ts → 浏览器端 Supabase 客户端
│ │ └── server.ts → 服务器端 Supabase 客户端
│ ├── stripe.ts
│ └── utils.ts
├── types/
│ └── database.ts → Supabase 生成的类型
└── middleware.ts → 认证中间件(保护路由)
// middleware.ts - 保护需要登录的路由
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
// 刷新 session(保持登录状态)
const { data: { session } } = await supabase.auth.getSession()
// 未登录时重定向到 /login
const isAuthPage = req.nextUrl.pathname.startsWith('/login') ||
req.nextUrl.pathname.startsWith('/register')
const isProtectedPage = req.nextUrl.pathname.startsWith('/dashboard')
if (isProtectedPage && !session) {
return NextResponse.redirect(new URL('/login', req.url))
}
if (isAuthPage && session) {
return NextResponse.redirect(new URL('/dashboard', req.url))
}
return res
}
export const config = {
matcher: ['/dashboard/:path*', '/login', '/register']
}
关键认知
App Router 的三个核心思维转变:
-
"组件在哪运行"变成了第一性问题:写组件之前先问:这个组件需要在浏览器跑(有交互?有状态?)还是在服务器跑就够了?大多数展示型组件不需要 “use client”。
-
Server Actions 消灭了"多余的 API Route":CRUD 操作不再需要写
app/api/projects/route.ts,直接写 Server Action,让代码更内聚。 -
Suspense 让页面加载体验质的提升:不用为了快,把所有数据放进一个慢查询。用 Suspense 包裹慢组件,让快的部分立即渲染,用户体验更好。
“App Router 的出现,让一个 Next.js 开发者可以做全栈开发的 80%,而不需要维护单独的后端项目。这是 2024 年独立开发者效率最大的杠杆之一。”