什么是 Server Actions?它们是如何简化表单和数据变更操作的?

50 阅读4分钟

什么是 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

性能优化

  • 更小的客户端包
  • 服务器端处理
  • 自动缓存管理

开发体验

  • 类型安全
  • 更好的错误处理
  • 渐进式增强

最佳实践

  • 使用数据验证
  • 实现错误处理
  • 管理缓存
  • 保护敏感操作