什么是 Server Actions?它们是如何简化表单和数据变更操作的?
Server Actions 概述
Server Actions 是 Next.js 15 中的一项功能,允许在服务器端执行函数,简化了表单处理和数据变更操作。它们消除了对传统 API 路由的需求,提供了更直接的数据操作方式。
基本概念
// app/actions/user-actions.js
'use server'
import { db } from '@/lib/database'
import { revalidatePath } from 'next/cache'
export async function createUser(formData) {
const name = formData.get('name')
const email = formData.get('email')
if (!name || !email) {
throw new Error('Name and email are required')
}
const user = await db.users.create({
data: { name, email },
})
// 重新验证缓存
revalidatePath('/users')
return { success: true, user }
}
表单处理简化
传统方式 vs Server Actions
// 传统方式:需要 API 路由 + 客户端处理
// pages/api/users.js
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
const { name, email } = req.body
const user = await db.users.create({ data: { name, email } })
res.status(201).json({ user })
}
// 客户端组件
'use client'
import { useState } from 'react'
export default function CreateUserForm() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setError(null)
try {
const formData = new FormData(e.target)
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({
name: formData.get('name'),
email: formData.get('email')
}),
headers: { 'Content-Type': 'application/json' }
})
if (!response.ok) {
throw new Error('Failed to create user')
}
const result = await response.json()
// 处理成功响应
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <p>{error}</p>}
</form>
)
}
// Server Actions 方式:更简洁
// app/actions/user-actions.js
'use server'
export async function createUser(formData) {
const name = formData.get('name')
const email = formData.get('email')
if (!name || !email) {
throw new Error('Name and email are required')
}
const user = await db.users.create({ data: { name, email } })
revalidatePath('/users')
return { success: true, user }
}
// 客户端组件
;('use client')
import { createUser } from '@/app/actions/user-actions'
export default function CreateUserForm() {
return (
<form action={createUser}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Create User</button>
</form>
)
}
实际应用示例
1. 用户注册表单
// app/actions/auth-actions.js
'use server'
import { db } from '@/lib/database'
import bcrypt from 'bcryptjs'
import { redirect } from 'next/navigation'
export async function registerUser(formData) {
const name = formData.get('name')
const email = formData.get('email')
const password = formData.get('password')
// 验证输入
if (!name || !email || !password) {
throw new Error('All fields are required')
}
if (password.length < 6) {
throw new Error('Password must be at least 6 characters')
}
// 检查用户是否已存在
const existingUser = await db.users.findUnique({
where: { email },
})
if (existingUser) {
throw new Error('User already exists')
}
// 创建用户
const hashedPassword = await bcrypt.hash(password, 12)
const user = await db.users.create({
data: {
name,
email,
password: hashedPassword,
},
})
// 重定向到登录页面
redirect('/login?message=Registration successful')
}
// app/register/page.js
import { registerUser } from '@/app/actions/auth-actions'
export default function RegisterPage() {
return (
<div>
<h1>Register</h1>
<form action={registerUser}>
<div>
<label htmlFor="name">Name:</label>
<input id="name" name="name" type="text" required />
</div>
<div>
<label htmlFor="email">Email:</label>
<input id="email" name="email" type="email" required />
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
name="password"
type="password"
required
minLength={6}
/>
</div>
<button type="submit">Register</button>
</form>
</div>
)
}
2. 博客文章创建
// app/actions/post-actions.js
'use server'
import { db } from '@/lib/database'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData) {
const title = formData.get('title')
const content = formData.get('content')
const categoryId = formData.get('categoryId')
if (!title || !content || !categoryId) {
throw new Error('All fields are required')
}
const post = await db.posts.create({
data: {
title,
content,
categoryId: parseInt(categoryId),
published: true,
},
})
revalidatePath('/blog')
redirect(`/blog/${post.id}`)
}
export async function updatePost(postId, formData) {
const title = formData.get('title')
const content = formData.get('content')
if (!title || !content) {
throw new Error('Title and content are required')
}
const post = await db.posts.update({
where: { id: parseInt(postId) },
data: { title, content },
})
revalidatePath('/blog')
revalidatePath(`/blog/${postId}`)
return { success: true, post }
}
// app/blog/create/page.js
import { createPost } from '@/app/actions/post-actions'
import { db } from '@/lib/database'
export default async function CreatePostPage() {
const categories = await db.categories.findMany()
return (
<div>
<h1>Create New Post</h1>
<form action={createPost}>
<div>
<label htmlFor="title">Title:</label>
<input id="title" name="title" type="text" required />
</div>
<div>
<label htmlFor="categoryId">Category:</label>
<select id="categoryId" name="categoryId" required>
<option value="">Select a category</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
</div>
<div>
<label htmlFor="content">Content:</label>
<textarea id="content" name="content" rows={10} required />
</div>
<button type="submit">Create Post</button>
</form>
</div>
)
}
3. 购物车操作
// app/actions/cart-actions.js
'use server'
import { db } from '@/lib/database'
import { revalidatePath } from 'next/cache'
export async function addToCart(productId, quantity = 1) {
const product = await db.products.findUnique({
where: { id: parseInt(productId) },
})
if (!product) {
throw new Error('Product not found')
}
if (product.stock < quantity) {
throw new Error('Insufficient stock')
}
// 这里简化处理,实际应用中需要处理用户会话
const cartItem = await db.cartItems.create({
data: {
productId: parseInt(productId),
quantity,
price: product.price,
},
})
revalidatePath('/cart')
return { success: true, cartItem }
}
export async function updateCartItem(cartItemId, quantity) {
if (quantity <= 0) {
await db.cartItems.delete({
where: { id: parseInt(cartItemId) },
})
} else {
await db.cartItems.update({
where: { id: parseInt(cartItemId) },
data: { quantity },
})
}
revalidatePath('/cart')
return { success: true }
}
export async function removeFromCart(cartItemId) {
await db.cartItems.delete({
where: { id: parseInt(cartItemId) },
})
revalidatePath('/cart')
return { success: true }
}
// app/products/[id]/page.js
import { addToCart } from '@/app/actions/cart-actions'
import { db } from '@/lib/database'
export default async function ProductPage({ params }) {
const product = await db.products.findUnique({
where: { id: parseInt(params.id) },
})
if (!product) {
return <div>Product not found</div>
}
return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p>Stock: {product.stock}</p>
<form action={addToCart}>
<input type="hidden" name="productId" value={product.id} />
<input
type="number"
name="quantity"
min="1"
max={product.stock}
defaultValue="1"
/>
<button type="submit">Add to Cart</button>
</form>
</div>
)
}
高级特性
1. 渐进式增强
// app/actions/form-actions.js
'use server'
export async function submitForm(formData) {
const name = formData.get('name')
const email = formData.get('email')
// 处理表单数据
const result = await processFormData({ name, email })
return { success: true, result }
}
// app/forms/progressive-form.js
'use client'
import { submitForm } from '@/app/actions/form-actions'
import { useState } from 'react'
export default function ProgressiveForm() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [message, setMessage] = useState('')
const handleSubmit = async (formData) => {
setIsSubmitting(true)
setMessage('')
try {
const result = await submitForm(formData)
setMessage('Form submitted successfully!')
} catch (error) {
setMessage('Error: ' + error.message)
} finally {
setIsSubmitting(false)
}
}
return (
<form action={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
{message && <p>{message}</p>}
</form>
)
}
2. 错误处理
// app/actions/error-handling-actions.js
'use server'
export async function handleFormWithErrors(formData) {
try {
const name = formData.get('name')
const email = formData.get('email')
// 验证数据
if (!name) {
throw new Error('Name is required')
}
if (!email) {
throw new Error('Email is required')
}
if (!email.includes('@')) {
throw new Error('Invalid email format')
}
// 处理数据
const result = await processData({ name, email })
return { success: true, result }
} catch (error) {
return { success: false, error: error.message }
}
}
// app/forms/error-form.js
'use client'
import { handleFormWithErrors } from '@/app/actions/error-handling-actions'
import { useState } from 'react'
export default function ErrorForm() {
const [result, setResult] = useState(null)
const handleSubmit = async (formData) => {
const response = await handleFormWithErrors(formData)
setResult(response)
}
return (
<div>
<form action={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Submit</button>
</form>
{result && (
<div>
{result.success ? (
<p style={{ color: 'green' }}>Success: {result.result}</p>
) : (
<p style={{ color: 'red' }}>Error: {result.error}</p>
)}
</div>
)}
</div>
)
}
3. 文件上传
// app/actions/file-actions.js
'use server'
import { writeFile } from 'fs/promises'
import { join } from 'path'
export async function uploadFile(formData) {
const file = formData.get('file')
if (!file) {
throw new Error('No file uploaded')
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const filename = `${Date.now()}-${file.name}`
const path = join(process.cwd(), 'public/uploads', filename)
await writeFile(path, buffer)
return { success: true, filename, url: `/uploads/${filename}` }
}
// app/upload/page.js
import { uploadFile } from '@/app/actions/file-actions'
export default function UploadPage() {
return (
<div>
<h1>Upload File</h1>
<form action={uploadFile}>
<input type="file" name="file" accept="image/*" required />
<button type="submit">Upload</button>
</form>
</div>
)
}
最佳实践
1. 数据验证
// lib/validation.js
import { z } from 'zod'
export const userSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email format'),
age: z.number().min(18, 'Must be at least 18 years old'),
})
export function validateFormData(formData, schema) {
const data = Object.fromEntries(formData.entries())
return schema.parse(data)
}
// app/actions/validated-actions.js
'use server'
import { validateFormData, userSchema } from '@/lib/validation'
export async function createValidatedUser(formData) {
try {
const validatedData = validateFormData(formData, userSchema)
const user = await db.users.create({
data: validatedData,
})
return { success: true, user }
} catch (error) {
if (error instanceof z.ZodError) {
return { success: false, errors: error.errors }
}
return { success: false, error: error.message }
}
}
2. 缓存管理
// app/actions/cache-actions.js
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function updateUser(userId, formData) {
const name = formData.get('name')
const email = formData.get('email')
const user = await db.users.update({
where: { id: parseInt(userId) },
data: { name, email },
})
// 重新验证特定路径
revalidatePath('/users')
revalidatePath(`/users/${userId}`)
// 重新验证特定标签
revalidateTag('users')
return { success: true, user }
}
总结
Server Actions 的主要优势:
简化开发:
- 无需创建 API 路由
- 直接处理表单数据
- 减少客户端 JavaScript
性能优化:
- 更小的客户端包
- 服务器端处理
- 自动缓存管理
开发体验:
- 类型安全
- 更好的错误处理
- 渐进式增强
最佳实践:
- 使用数据验证
- 实现错误处理
- 管理缓存
- 保护敏感操作