Skip to content

Server Actions服务端操作

概述

Server Actions是Next.js 13.4引入的强大特性,允许在服务器上执行异步代码。它们可以在服务端组件和客户端组件中调用,无需创建API端点,简化了表单提交和数据变更操作。本文将全面介绍Server Actions的使用方法和最佳实践。

Server Actions基础

定义Server Action

typescript
// app/actions.ts
'use server';

export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;
  
  // 数据库操作
  await db.user.create({
    data: { name, email },
  });
  
  // 重定向
  redirect('/users');
}

// 或在组件中定义
// app/page.tsx
export default function Page() {
  async function create(formData: FormData) {
    'use server';
    
    const name = formData.get('name') as string;
    await db.user.create({ data: { name } });
  }
  
  return (
    <form action={create}>
      <input name="name" />
      <button type="submit">Create</button>
    </form>
  );
}

在表单中使用

typescript
// app/users/new/page.tsx
import { createUser } from '@/app/actions';

export default function NewUserPage() {
  return (
    <form action={createUser}>
      <input name="name" placeholder="Name" required />
      <input name="email" placeholder="Email" type="email" required />
      <button type="submit">Create User</button>
    </form>
  );
}

在客户端组件中使用

typescript
// components/CreateUserForm.tsx
'use client';

import { createUser } from '@/app/actions';
import { useFormState, useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Creating...' : 'Create User'}
    </button>
  );
}

export default function CreateUserForm() {
  const [state, formAction] = useFormState(createUser, null);
  
  return (
    <form action={formAction}>
      <input name="name" placeholder="Name" />
      <input name="email" placeholder="Email" />
      <SubmitButton />
      {state?.error && <p className="error">{state.error}</p>}
    </form>
  );
}

数据变更

创建数据

typescript
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email'),
  age: z.number().min(0).max(150),
});

export async function createUser(formData: FormData) {
  // 验证数据
  const validatedFields = CreateUserSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    age: Number(formData.get('age')),
  });
  
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }
  
  try {
    // 创建用户
    await db.user.create({
      data: validatedFields.data,
    });
    
    // 重新验证缓存
    revalidatePath('/users');
    
    return { success: true };
  } catch (error) {
    return { error: 'Failed to create user' };
  }
}

更新数据

typescript
// app/actions.ts
'use server';

export async function updateUser(id: string, formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;
  
  try {
    await db.user.update({
      where: { id },
      data: { name, email },
    });
    
    revalidatePath(`/users/${id}`);
    revalidatePath('/users');
    
    return { success: true };
  } catch (error) {
    return { error: 'Failed to update user' };
  }
}

// components/EditUserForm.tsx
'use client';

import { updateUser } from '@/app/actions';

export default function EditUserForm({ user }: { user: User }) {
  const updateUserWithId = updateUser.bind(null, user.id);
  
  return (
    <form action={updateUserWithId}>
      <input name="name" defaultValue={user.name} />
      <input name="email" defaultValue={user.email} />
      <button type="submit">Update</button>
    </form>
  );
}

删除数据

typescript
// app/actions.ts
'use server';

export async function deleteUser(id: string) {
  try {
    await db.user.delete({
      where: { id },
    });
    
    revalidatePath('/users');
    
    return { success: true };
  } catch (error) {
    return { error: 'Failed to delete user' };
  }
}

// components/DeleteUserButton.tsx
'use client';

import { deleteUser } from '@/app/actions';

export default function DeleteUserButton({ userId }: { userId: string }) {
  const handleDelete = async () => {
    if (confirm('Are you sure?')) {
      const result = await deleteUser(userId);
      if (result.error) {
        alert(result.error);
      }
    }
  };
  
  return (
    <button onClick={handleDelete}>
      Delete
    </button>
  );
}

表单状态管理

useFormState Hook

typescript
// app/actions.ts
'use server';

export async function submitForm(prevState: any, formData: FormData) {
  const name = formData.get('name') as string;
  
  if (!name) {
    return { error: 'Name is required' };
  }
  
  await db.user.create({ data: { name } });
  
  return { success: true, message: 'User created successfully' };
}

// components/Form.tsx
'use client';

import { useFormState } from 'react-dom';
import { submitForm } from '@/app/actions';

export default function Form() {
  const [state, formAction] = useFormState(submitForm, { error: null });
  
  return (
    <form action={formAction}>
      <input name="name" />
      
      {state?.error && (
        <p className="error">{state.error}</p>
      )}
      
      {state?.success && (
        <p className="success">{state.message}</p>
      )}
      
      <button type="submit">Submit</button>
    </form>
  );
}

useFormStatus Hook

typescript
// components/SubmitButton.tsx
'use client';

import { useFormStatus } from 'react-dom';

export default function SubmitButton() {
  const { pending, data, method, action } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

// components/Form.tsx
'use client';

import { createUser } from '@/app/actions';
import SubmitButton from './SubmitButton';

export default function Form() {
  return (
    <form action={createUser}>
      <input name="name" />
      <SubmitButton />
    </form>
  );
}

useOptimistic Hook

typescript
// components/TodoList.tsx
'use client';

import { useOptimistic } from 'react';
import { addTodo } from '@/app/actions';

type Todo = {
  id: string;
  text: string;
  completed: boolean;
};

export default function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo: string) => [
      ...state,
      { id: 'temp-' + Date.now(), text: newTodo, completed: false },
    ]
  );
  
  const formAction = async (formData: FormData) => {
    const text = formData.get('text') as string;
    addOptimisticTodo(text);
    await addTodo(text);
  };
  
  return (
    <div>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.id.startsWith('temp-') ? 0.5 : 1 }}>
            {todo.text}
          </li>
        ))}
      </ul>
      
      <form action={formAction}>
        <input name="text" />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
}

数据验证

使用Zod

typescript
// app/actions.ts
'use server';

import { z } from 'zod';

const UserSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  age: z.number().min(18, 'Must be at least 18 years old'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

export async function createUser(formData: FormData) {
  const rawData = {
    name: formData.get('name'),
    email: formData.get('email'),
    age: Number(formData.get('age')),
    password: formData.get('password'),
  };
  
  const validatedFields = UserSchema.safeParse(rawData);
  
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: 'Validation failed',
    };
  }
  
  try {
    await db.user.create({
      data: validatedFields.data,
    });
    
    revalidatePath('/users');
    return { success: true };
  } catch (error) {
    return { error: 'Database error' };
  }
}

字段级验证

typescript
// app/actions.ts
'use server';

export async function validateEmail(email: string) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  
  if (!emailRegex.test(email)) {
    return { error: 'Invalid email format' };
  }
  
  const existingUser = await db.user.findUnique({
    where: { email },
  });
  
  if (existingUser) {
    return { error: 'Email already exists' };
  }
  
  return { valid: true };
}

// components/EmailInput.tsx
'use client';

import { useState } from 'react';
import { validateEmail } from '@/app/actions';

export default function EmailInput() {
  const [error, setError] = useState('');
  
  const handleBlur = async (e: React.FocusEvent<HTMLInputElement>) => {
    const result = await validateEmail(e.target.value);
    if (result.error) {
      setError(result.error);
    } else {
      setError('');
    }
  };
  
  return (
    <div>
      <input
        name="email"
        type="email"
        onBlur={handleBlur}
      />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

认证和授权

检查用户权限

typescript
// app/actions.ts
'use server';

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

export async function deletePost(postId: string) {
  const session = await getServerSession(authOptions);
  
  if (!session) {
    return { error: 'Unauthorized' };
  }
  
  const post = await db.post.findUnique({
    where: { id: postId },
  });
  
  if (post.authorId !== session.user.id) {
    return { error: 'Forbidden' };
  }
  
  await db.post.delete({
    where: { id: postId },
  });
  
  revalidatePath('/posts');
  return { success: true };
}

管理员操作

typescript
// app/actions.ts
'use server';

import { getServerSession } from 'next-auth';

export async function deleteUser(userId: string) {
  const session = await getServerSession(authOptions);
  
  if (!session || session.user.role !== 'admin') {
    return { error: 'Admin access required' };
  }
  
  await db.user.delete({
    where: { id: userId },
  });
  
  revalidatePath('/admin/users');
  return { success: true };
}

文件上传

处理文件上传

typescript
// app/actions.ts
'use server';

import { writeFile } from 'fs/promises';
import { join } from 'path';

export async function uploadFile(formData: FormData) {
  const file = formData.get('file') as File;
  
  if (!file) {
    return { error: 'No file provided' };
  }
  
  // 验证文件类型
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
  if (!allowedTypes.includes(file.type)) {
    return { error: 'Invalid file type' };
  }
  
  // 验证文件大小 (5MB)
  if (file.size > 5 * 1024 * 1024) {
    return { error: 'File too large' };
  }
  
  try {
    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, 
      url: `/uploads/${fileName}` 
    };
  } catch (error) {
    return { error: 'Upload failed' };
  }
}

// components/FileUpload.tsx
'use client';

import { uploadFile } from '@/app/actions';
import { useState } from 'react';

export default function FileUpload() {
  const [uploading, setUploading] = useState(false);
  const [url, setUrl] = useState('');
  
  const handleSubmit = async (formData: FormData) => {
    setUploading(true);
    const result = await uploadFile(formData);
    setUploading(false);
    
    if (result.url) {
      setUrl(result.url);
    }
  };
  
  return (
    <form action={handleSubmit}>
      <input type="file" name="file" accept="image/*" />
      <button type="submit" disabled={uploading}>
        {uploading ? 'Uploading...' : 'Upload'}
      </button>
      {url && <img src={url} alt="Uploaded" />}
    </form>
  );
}

缓存重新验证

revalidatePath

typescript
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  
  await db.post.create({
    data: { title },
  });
  
  // 重新验证特定路径
  revalidatePath('/posts');
  
  // 重新验证特定路径和类型
  revalidatePath('/posts', 'page'); // 只重新验证页面
  revalidatePath('/posts', 'layout'); // 重新验证布局及以下所有页面
  
  return { success: true };
}

revalidateTag

typescript
// app/posts/page.tsx
export default async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] },
  }).then(r => r.json());
  
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

// app/actions.ts
'use server';

import { revalidateTag } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  
  await db.post.create({
    data: { title },
  });
  
  // 重新验证带有'posts'标签的所有数据
  revalidateTag('posts');
  
  return { success: true };
}

重定向

基本重定向

typescript
// app/actions.ts
'use server';

import { redirect } from 'next/navigation';

export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  
  const user = await db.user.create({
    data: { name },
  });
  
  // 重定向到新创建的用户页面
  redirect(`/users/${user.id}`);
}

条件重定向

typescript
// app/actions.ts
'use server';

import { redirect } from 'next/navigation';

export async function updateProfile(formData: FormData) {
  const session = await getServerSession();
  
  if (!session) {
    redirect('/login');
  }
  
  const name = formData.get('name') as string;
  
  await db.user.update({
    where: { id: session.user.id },
    data: { name },
  });
  
  redirect('/profile');
}

错误处理

Try-Catch

typescript
// app/actions.ts
'use server';

export async function createUser(formData: FormData) {
  try {
    const name = formData.get('name') as string;
    
    await db.user.create({
      data: { name },
    });
    
    revalidatePath('/users');
    return { success: true };
  } catch (error) {
    console.error('Failed to create user:', error);
    return { error: 'Failed to create user. Please try again.' };
  }
}

自定义错误类型

typescript
// lib/errors.ts
export class ValidationError extends Error {
  constructor(public errors: Record<string, string[]>) {
    super('Validation failed');
    this.name = 'ValidationError';
  }
}

export class UnauthorizedError extends Error {
  constructor() {
    super('Unauthorized');
    this.name = 'UnauthorizedError';
  }
}

// app/actions.ts
'use server';

import { ValidationError, UnauthorizedError } from '@/lib/errors';

export async function createPost(formData: FormData) {
  const session = await getServerSession();
  
  if (!session) {
    throw new UnauthorizedError();
  }
  
  const title = formData.get('title') as string;
  
  if (!title || title.length < 3) {
    throw new ValidationError({
      title: ['Title must be at least 3 characters'],
    });
  }
  
  await db.post.create({
    data: {
      title,
      authorId: session.user.id,
    },
  });
  
  revalidatePath('/posts');
  return { success: true };
}

批量操作

批量创建

typescript
// app/actions.ts
'use server';

export async function bulkCreateUsers(users: Array<{ name: string; email: string }>) {
  try {
    await db.user.createMany({
      data: users,
    });
    
    revalidatePath('/users');
    return { success: true, count: users.length };
  } catch (error) {
    return { error: 'Bulk create failed' };
  }
}

// components/BulkUpload.tsx
'use client';

import { bulkCreateUsers } from '@/app/actions';

export default function BulkUpload() {
  const handleSubmit = async (formData: FormData) => {
    const file = formData.get('file') as File;
    const text = await file.text();
    const users = JSON.parse(text);
    
    const result = await bulkCreateUsers(users);
    if (result.success) {
      alert(`Created ${result.count} users`);
    }
  };
  
  return (
    <form action={handleSubmit}>
      <input type="file" name="file" accept=".json" />
      <button type="submit">Upload</button>
    </form>
  );
}

批量更新

typescript
// app/actions.ts
'use server';

export async function bulkUpdateUsers(updates: Array<{ id: string; data: any }>) {
  try {
    await db.$transaction(
      updates.map(({ id, data }) =>
        db.user.update({
          where: { id },
          data,
        })
      )
    );
    
    revalidatePath('/users');
    return { success: true };
  } catch (error) {
    return { error: 'Bulk update failed' };
  }
}

后台任务

异步处理

typescript
// app/actions.ts
'use server';

export async function processLargeFile(formData: FormData) {
  const file = formData.get('file') as File;
  
  // 立即返回,不等待处理完成
  processFileInBackground(file).catch(console.error);
  
  return { 
    success: true, 
    message: 'File is being processed in the background' 
  };
}

async function processFileInBackground(file: File) {
  // 长时间运行的任务
  const data = await file.text();
  const processed = await heavyProcessing(data);
  await db.result.create({ data: processed });
}

队列系统集成

typescript
// app/actions.ts
'use server';

import { Queue } from 'bullmq';

const emailQueue = new Queue('email', {
  connection: {
    host: 'localhost',
    port: 6379,
  },
});

export async function sendWelcomeEmail(userId: string) {
  await emailQueue.add('welcome', {
    userId,
    type: 'welcome',
  });
  
  return { success: true, message: 'Email queued' };
}

最佳实践

1. 验证输入

typescript
// ✅ 好 - 始终验证输入
'use server';

import { z } from 'zod';

const schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

export async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
  });
  
  if (!validatedFields.success) {
    return { errors: validatedFields.error.flatten().fieldErrors };
  }
  
  // 继续处理...
}

// ❌ 不好 - 直接使用未验证的数据
export async function createUser(formData: FormData) {
  const name = formData.get('name');
  await db.user.create({ data: { name } });
}

2. 检查权限

typescript
// ✅ 好 - 检查用户权限
'use server';

export async function deletePost(postId: string) {
  const session = await getServerSession();
  
  if (!session) {
    return { error: 'Unauthorized' };
  }
  
  const post = await db.post.findUnique({ where: { id: postId } });
  
  if (post.authorId !== session.user.id) {
    return { error: 'Forbidden' };
  }
  
  await db.post.delete({ where: { id: postId } });
  return { success: true };
}

3. 错误处理

typescript
// ✅ 好 - 适当的错误处理
'use server';

export async function createUser(formData: FormData) {
  try {
    const name = formData.get('name') as string;
    await db.user.create({ data: { name } });
    revalidatePath('/users');
    return { success: true };
  } catch (error) {
    console.error('Create user error:', error);
    return { error: 'Failed to create user' };
  }
}

4. 重新验证缓存

typescript
// ✅ 好 - 数据变更后重新验证
'use server';

export async function updatePost(id: string, formData: FormData) {
  await db.post.update({
    where: { id },
    data: { title: formData.get('title') as string },
  });
  
  revalidatePath(`/posts/${id}`);
  revalidatePath('/posts');
  
  return { success: true };
}

Server Actions提供了一种简洁的方式来处理服务器端逻辑,无需创建API端点,大大简化了全栈开发流程。