Next.js 14的App Router引入了基于React Server Components的路由系统,配合Server Actions实现了前后端一体化的数据交互模式。Server Actions允许在服务端组件中直接定义处理函数,前端调用时自动通过POST请求传输,无需手写API路由。这种模式简化了全栈开发流程,同时保持了类型安全和SEO优势。
App Router目录结构与路由约定
App Router使用文件系统路由,app目录下的文件夹结构直接映射为URL路径:
app/
├── layout.tsx # 根布局,所有页面共享
├── page.tsx # 首页 /
├── loading.tsx # 全局加载UI
├── error.tsx # 全局错误边界
├── not-found.tsx # 404页面
├── globals.css # 全局样式
├── (marketing)/ # 路由组,不影响URL
│ ├── about/
│ │ └── page.tsx # /about
│ └── pricing/
│ └── page.tsx # /pricing
├── dashboard/
│ ├── layout.tsx # dashboard专属布局
│ ├── page.tsx # /dashboard
│ ├── settings/
│ │ └── page.tsx # /dashboard/settings
│ └── users/
│ ├── page.tsx # /dashboard/users
│ └── [id]/ # 动态路由
│ └── page.tsx # /dashboard/users/123
├── api/
│ └── webhook/
│ └── route.ts # API路由(传统模式)
└── sitemap.ts # 动态站点地图
关键约定:每个路由文件夹内的page.tsx是路由的UI入口,layout.tsx定义该层级的共享布局。路由组用()包裹,路径不显示在URL中,用于组织代码结构。动态路由用[]包裹参数名。
Server Component与Client Component划分策略
App Router默认所有组件都是Server Component,仅在需要交互时才添加use client指令。合理划分两者边界直接影响性能:
// app/dashboard/users/page.tsx
// Server Component - 默认在服务端渲染
import { db } from '@/lib/database'
import { UserTable } from './UserTable'
import { Pagination } from './Pagination'
interface PageProps {
searchParams: { page?: string; q?: string }
}
// 服务端获取数据,直接访问数据库
async function getUsers(page: number, query: string) {
const pageSize = 20
const offset = (page - 1) * pageSize
const users = await db.user.findMany({
where: query ? {
OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ email: { contains: query, mode: 'insensitive' } }
]
} : undefined,
select: { id: true, name: true, email: true, role: true, createdAt: true },
skip: offset,
take: pageSize,
orderBy: { createdAt: 'desc' }
})
const total = await db.user.count({
where: query ? {
OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ email: { contains: query, mode: 'insensitive' } }
]
} : undefined
})
return { users, total, totalPages: Math.ceil(total / pageSize) }
}
export default async function UsersPage({ searchParams }: PageProps) {
const page = parseInt(searchParams.page || '1')
const query = searchParams.q || ''
const { users, totalPages } = await getUsers(page, query)
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">用户管理</h1>
<UserTable users={users} />
<Pagination currentPage={page} totalPages={totalPages} />
</div>
)
}
// app/dashboard/users/UserTable.tsx
'use client' // 标记为Client Component
import { useState } from 'react'
import { deleteUserAction } from './actions'
export function UserTable({ users }: { users: User[] }) {
const [selected, setSelected] = useState<string[]>([])
const [deleting, setDeleting] = useState(false)
const handleDelete = async () => {
setDeleting(true)
// 调用Server Action,无需手写API
await deleteUserAction(selected)
setDeleting(false)
setSelected([])
window.location.reload()
}
return (
<div>
<table>
<thead>
<tr>
<th><input type="checkbox" /></th>
<th>姓名</th>
<th>邮箱</th>
<th>角色</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>
<input
type="checkbox"
checked={selected.includes(user.id)}
onChange={(e) => {
if (e.target.checked) {
setSelected([...selected, user.id])
} else {
setSelected(selected.filter(id => id !== user.id))
}
}}
/>
</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
</tr>
))}
</tbody>
</table>
{selected.length > 0 && (
<button onClick={handleDelete} disabled={deleting}>
{deleting ? '删除中...' : `删除选中(${selected.length})`}
</button>
)}
</div>
)
}
Server Actions表单处理与数据验证
Server Actions用use server指令标记,可以在服务端组件中内联定义或单独文件导出:
// app/dashboard/users/actions.ts
'use server'
import { db } from '@/lib/database'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
// Zod schema验证
const UserSchema = z.object({
name: z.string().min(2, '姓名至少2个字符').max(50),
email: z.string().email('邮箱格式不正确'),
role: z.enum(['admin', 'editor', 'viewer']),
})
export async function createUserAction(formData: FormData) {
const raw = {
name: formData.get('name'),
email: formData.get('email'),
role: formData.get('role'),
}
const result = UserSchema.safeParse(raw)
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors
}
}
try {
const user = await db.user.create({
data: result.data
})
revalidatePath('/dashboard/users')
return { success: true, user }
} catch (error) {
return { success: false, errors: { _form: ['创建用户失败'] } }
}
}
export async function deleteUserAction(ids: string[]) {
if (ids.length === 0) return
try {
await db.user.deleteMany({
where: { id: { in: ids } }
})
revalidatePath('/dashboard/users')
return { success: true }
} catch (error) {
return { success: false, error: '批量删除失败' }
}
}
// 使用revalidateTag实现精细化的缓存控制
export async function updateUserProfileAction(formData: FormData) {
const userId = formData.get('userId') as string
await db.user.update({
where: { id: userId },
data: {
name: formData.get('name'),
bio: formData.get('bio'),
}
})
revalidateTag(`user-${userId}`)
revalidatePath('/dashboard/users')
}
Server Actions与progressive enhancement表单
Server Actions支持progressive enhancement,即使JavaScript未加载,表单也能通过传统POST提交工作:
// app/dashboard/users/create/page.tsx
import { createUserAction } from '../actions'
import { useFormState, useFormStatus } from 'react-dom'
export default function CreateUserPage() {
const [state, formAction] = useFormState(createUserAction, {
success: false,
errors: {}
})
return (
<form action={formAction}>
<div>
<label>姓名</label>
<input type="text" name="name" required />
{state.errors?.name && (
<span className="error">{state.errors.name[0]}</span>
)}
</div>
<div>
<label>邮箱</label>
<input type="email" name="email" required />
{state.errors?.email && (
<span className="error">{state.errors.email[0]}</span>
)}
</div>
<div>
<label>角色</label>
<select name="role">
<option value="viewer">查看者</option>
<option value="editor">编辑者</option>
<option value="admin">管理员</option>
</select>
</div>
<SubmitButton />
{state.success && (
<p className="success">用户创建成功</p>
)}
</form>
)
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '创建用户'}
</button>
)
}
关键点:useFormState替代了传统的useState+onSubmit模式,表单action直接绑定Server Action。useFormStatus提供提交状态,用于禁用按钮和显示loading。revalidatePath在Action执行后自动刷新对应路由的Server Component缓存数据。
安全注意事项:Server Actions本质是HTTP POST请求,任何能访问到该URL的客户端都可以调用。需要在Action内部做权限校验,不能依赖前端的按钮禁用来阻止未授权操作。使用Next.js中间件或Action内的Session检查确保访问控制。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/nextjs14approuter-lu-you-jia-gou-yu-serveractions-fu-wu/