在现代 Web 开发中,安全性、类型检查和代码效率至关重要。

Next.js 的 Server Actions 为我们提供了在服务器端执行操作的强大工具,而 next-safe-action 库则进一步增强了这一功能。

本文将深入探讨如何利用 next-safe-action 来创建更安全、更高效的 Server Actions。

Server Actions 基础

首先,让我们回顾一下 Next.js 中 Server Actions 的基本概念。

Server Actions 是在 Next.js 13 中引入的功能,允许开发者直接在组件中定义和调用服务器端函数。这些函数使用 “use server” 指令标记,可以在服务器组件和客户端组件中使用。

基本示例

  1. "use server"
  2. async function handleSubmit(formData: FormData) {
  3. const name = formData.get('name')
  4. // 服务器端逻辑
  5. console.log(`Server received: ${name}`)
  6. }

这个简单的例子展示了 Server Action 的基本用法。然而,在实际应用中,我们通常需要处理更复杂的场景,包括数据验证、错误处理和类型安全。

next-safe-action 介绍

next-safe-action 是一个专为 Next.js Server Actions 设计的库,旨在解决以下问题:

  1. 类型安全:确保输入和输出都有正确的类型定义。
  2. 数据验证:集成 Zod 等验证库,轻松进行数据校验。
  3. 错误处理:提供统一的错误处理机制。
  4. 代码简化:减少样板代码,提高开发效率。

深入对比:传统 Server Action vs next-safe-action

让我们通过一个更复杂的例子来深入比较这两种方法。

场景:用户注册功能

假设我们正在开发一个用户注册功能,需要验证用户输入,处理可能的错误,并返回适当的响应。

传统 Server Action 实现

  1. // src/app/actions/register.ts
  2. "use server"
  3. import { z } from 'zod'
  4. const UserSchema = z.object({
  5. username: z.string().min(3).max(20),
  6. email: z.string().email(),
  7. password: z.string().min(8),
  8. })
  9. type User = z.infer<typeof UserSchema>
  10. type ReturnType = {
  11. success: boolean
  12. message: string
  13. errors?: Record<string, string[]>
  14. }
  15. export async function registerUser(userData: User): Promise<ReturnType> {
  16. const result = UserSchema.safeParse(userData)
  17. if (!result.success) {
  18. return {
  19. success: false,
  20. message: "Validation failed",
  21. errors: result.error.flatten().fieldErrors
  22. }
  23. }
  24. try {
  25. // 模拟用户注册逻辑
  26. await new Promise(resolve => setTimeout(resolve, 1000))
  27. // 检查邮箱是否已被使用
  28. if (userData.email === "test@example.com") {
  29. throw new Error("Email already in use")
  30. }
  31. return {
  32. success: true,
  33. message: "User registered successfully"
  34. }
  35. } catch (error) {
  36. return {
  37. success: false,
  38. message: error instanceof Error ? error.message : "An unknown error occurred"
  39. }
  40. }
  41. }

使用 next-safe-action 的实现

  1. // src/app/actions/register.ts
  2. "use server"
  3. import { z } from 'zod'
  4. import { actionClient } from "@/lib/safe-action"
  5. const UserSchema = z.object({
  6. username: z.string().min(3).max(20),
  7. email: z.string().email(),
  8. password: z.string().min(8),
  9. })
  10. export const registerUserAction = actionClient
  11. .schema(UserSchema)
  12. .action(async ({ parsedInput }) => {
  13. try {
  14. // 模拟用户注册逻辑
  15. await new Promise(resolve => setTimeout(resolve, 1000))
  16. // 检查邮箱是否已被使用
  17. if (parsedInput.email === "test@example.com") {
  18. throw new Error("Email already in use")
  19. }
  20. return {
  21. success: true,
  22. message: "User registered successfully"
  23. }
  24. } catch (error) {
  25. throw new Error(error instanceof Error ? error.message : "An unknown error occurred")
  26. }
  27. })

主要区别分析

  1. 代码结构:next-safe-action 版本的代码结构更清晰,将验证逻辑和业务逻辑明确分开。
  2. 错误处理:传统方法需要手动构建错误对象,而 next-safe-action 允许直接抛出错误,简化了错误处理流程。
  3. 类型安全:next-safe-action 自动推断输入和输出类型,减少了手动类型定义的需要。
  4. 验证集成:在 next-safe-action 中,验证逻辑直接集成到 action 定义中,无需额外的验证步骤。
  5. 返回值处理:传统方法需要手动构建返回对象,而 next-safe-action 允许直接返回结果,库会自动处理响应格式。

客户端实现对比

现在,让我们看看如何在客户端组件中使用这些 Server Actions。

传统方法

  1. "use client"
  2. import { useState } from 'react'
  3. import { registerUser } from '@/app/actions/register'
  4. export default function RegisterForm() {
  5. const [message, setMessage] = useState('')
  6. const [errors, setErrors] = useState<Record<string, string[]>>({})
  7. const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
  8. event.preventDefault()
  9. const formData = new FormData(event.currentTarget)
  10. const userData = Object.fromEntries(formData)
  11. const result = await registerUser(userData)
  12. if (result.success) {
  13. setMessage(result.message)
  14. setErrors({})
  15. } else {
  16. setMessage(result.message)
  17. setErrors(result.errors || {})
  18. }
  19. }
  20. return (
  21. <form onSubmit={handleSubmit}>
  22. {/* 表单字段 */}
  23. <button type="submit">Register</button>
  24. {message && <p>{message}</p>}
  25. {Object.entries(errors).map(([field, messages]) => (
  26. <p key={field}>{field}: {messages.join(', ')}</p>
  27. ))}
  28. </form>
  29. )
  30. }

使用 next-safe-action

  1. "use client"
  2. import { useAction } from "next-safe-action/hooks"
  3. import { registerUserAction } from '@/app/actions/register'
  4. export default function RegisterForm() {
  5. const { execute, result, isExecuting } = useAction(registerUserAction)
  6. const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
  7. event.preventDefault()
  8. const formData = new FormData(event.currentTarget)
  9. const userData = Object.fromEntries(formData)
  10. execute(userData)
  11. }
  12. return (
  13. <form onSubmit={handleSubmit}>
  14. {/* 表单字段 */}
  15. <button type="submit" disabled={isExecuting}>
  16. {isExecuting ? 'Registering...' : 'Register'}
  17. </button>
  18. {result?.data && <p>{result.data.message}</p>}
  19. {result?.validationErrors && (
  20. <ul>
  21. {Object.entries(result.validationErrors).map(([field, errors]) => (
  22. <li key={field}>{field}: {errors?.join(', ')}</li>
  23. ))}
  24. </ul>
  25. )}
  26. {result?.serverError && <p>Server Error: {result.serverError}</p>}
  27. </form>
  28. )
  29. }

客户端实现的主要区别

  1. 状态管理:next-safe-action 版本使用 useAction hook 管理状态,无需手动设置 state。
  2. 类型安全useAction hook 提供类型安全的结果对象,包括数据、验证错误和服务器错误。
  3. 加载状态:next-safe-action 提供 isExecuting 状态,便于处理加载状态。
  4. 错误处理:验证错误和服务器错误的处理更加直观和统一。
  5. 代码简洁性:整体代码结构更加简洁,减少了样板代码。

next-safe-action 的高级特性

除了基本用法,next-safe-action 还提供了一些高级特性:

1. 中间件支持

你可以添加中间件来处理认证、日志记录等横切关注点:

  1. import { actionClient } from "@/lib/safe-action"
  2. const authenticatedAction = actionClient.middleware(async () => {
  3. const user = await getUser()
  4. if (!user) throw new Error("Unauthorized")
  5. return { user }
  6. })
  7. export const protectedAction = authenticatedAction
  8. .schema(SomeSchema)
  9. .action(async ({ parsedInput, user }) => {
  10. // 使用 user 和 parsedInput
  11. })

2. 自定义错误处理

你可以自定义错误的处理方式:

  1. import { actionClient } from "@/lib/safe-action"
  2. const customErrorAction = actionClient
  3. .schema(SomeSchema)
  4. .action(async ({ parsedInput }) => {
  5. // ...
  6. })
  7. .error((error) => {
  8. // 自定义错误处理逻辑
  9. return { customError: error.message }
  10. })

3. 重用验证逻辑

你可以在多个 actions 中重用相同的验证逻辑:

  1. const baseAction = actionClient.schema(BaseSchema)
  2. export const action1 = baseAction.action(async ({ parsedInput }) => {
  3. // 实现 action1
  4. })
  5. export const action2 = baseAction.action(async ({ parsedInput }) => {
  6. // 实现 action2
  7. })

性能考虑

使用 next-safe-action 可能会带来一些性能优势:

  1. 减少网络请求:由于验证在服务器端进行,可以减少无效请求。
  2. 优化构建:next-safe-action 支持树摇(tree shaking),有助于减小构建大小。
  3. 缓存友好:可以更容易地实现结果缓存,提高响应速度。

最佳实践

  1. 始终使用 Zod 或类似库进行数据验证:这不仅提供了类型安全,还确保了数据的完整性。
  2. 利用中间件进行认证和授权:这可以集中管理安全性,减少重复代码。
  3. 合理使用错误处理:自定义错误处理可以提供更好的用户体验。
  4. 考虑使用 React Query 或 SWR:这些库可以与 next-safe-action 很好地配合,提供更强大的数据管理能力。
  5. 定期更新依赖:确保你使用的是 next-safe-action 的最新版本,以获得最新的功能和安全修复。

结论

next-safe-action 为 Next.js 的 Server Actions 带来了显著的改进。它不仅简化了代码,还提供了更强的类型安全和错误处理能力。通过使用 next-safe-action,开发者可以更专注于业务逻辑,减少处理样板代码和错误情况的时间。

对于追求代码质量、开发效率和应用安全性的团队来说,next-safe-action 是一个值得考虑的工具。它使得 Server Actions 的使用更加直观和安全,是 Next.js 开发中的一个强大补充。

无论你是正在构建新项目还是优化现有代码库,next-safe-action 都值得一试。它可能会成为你 Next.js 工具箱中不可或缺的一部分,帮助你构建更加健壮和高效的 Web 应用。

通过本文的深入探讨和实际例子,我们看到了 next-safe-action 如何改变我们处理 Server Actions 的方式。它不仅提高了代码的可读性和可维护性,还为开发者提供了更多的信心来处理复杂的服务器端逻辑。在未来的 Next.js 项目中,考虑将 next-safe-action 纳入你的技术栈,你可能会发现它是提升开发体验和应用质量的关键工具。