Nextjs第七课 - 数据变更

4 阅读4分钟

上节我们学习了数据获取,本节来聊聊数据变更(修改数据)。在实际应用中,我们不仅需要读取数据,还需要创建、更新、删除数据。Next.js 提供了几种方式来处理数据变更,每种都有自己的适用场景。

数据变更方式

在 Next.js 中,主要有三种方式来变更数据:

  1. Server Actions - Next.js 推荐的新方式,可以直接从组件调用服务器函数
  2. API 路由 - 传统的 REST API 端点
  3. 客户端 fetch - 从客户端发送请求到外部 API

大多数情况下,Server Actions 是最简洁的选择。

Server Actions

Server Actions 是 Next.js 13+ 引入的新特性,它允许你在服务器上执行的异步函数,可以直接从客户端或服务端组件调用。这比传统的 API 路由 + fetch 方式要简洁很多。

基本 Server Action

创建 Server Action 非常简单,只需要在文件顶部加上 'use server' 指令:

// app/actions.ts
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  // 验证数据
  if (!title || !content) {
    return { error: '标题和内容不能为空' }
  }

  // 保存到数据库
  await db.post.create({
    data: { title, content },
  })

  return { success: true }
}

在表单中使用

Server Action 最常见的用途就是处理表单提交:

// app/posts/page.tsx
import { createPost } from '@/app/actions'

export default function PostsPage() {
  return (
    <div>
      <h1>创建文章</h1>
      <form action={createPost}>
        <input name="title" placeholder="标题" />
        <textarea name="content" placeholder="内容"></textarea>
        <button type="submit">提交</button>
      </form>
    </div>
  )
}

带重定向的 Server Action

很多情况下,表单提交后需要跳转到其他页面:

// app/actions.ts
'use server'

import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'

export async function updatePost(id: string, formData: FormData) {
  const title = formData.get('title') as string

  await db.post.update({
    where: { id },
    data: { title },
  })

  // 重新验证相关页面的缓存
  revalidatePath('/posts')
  revalidatePath(`/posts/${id}`)

  // 重定向
  redirect(`/posts/${id}`)
}

处理错误

数据验证和错误处理是必不可少的:

// app/actions.ts
'use server'

import { z } from 'zod'

const PostSchema = z.object({
  title: z.string().min(1, '标题不能为空').max(100, '标题太长'),
  content: z.string().min(10, '内容至少10个字符'),
})

export async function createPost(formData: FormData) {
  // 验证数据
  const validatedFields = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  })

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: '验证失败',
    }
  }

  const { title, content } = validatedFields.data

  try {
    await db.post.create({ data: { title, content } })
    return { message: '文章创建成功' }
  } catch (error) {
    return { message: '数据库错误: 无法创建文章' }
  }
}

API 路由

虽然 Server Actions 是推荐方式,但有些场景下 API 路由仍然很有用,比如需要给第三方调用,或者需要标准的 REST API。

基本 API 路由

// app/api/posts/route.ts
import { NextResponse } from 'next/server'

export async function GET() {
  const posts = await db.post.findMany()
  return NextResponse.json(posts)
}

export async function POST(request: Request) {
  const body = await request.json()
  const post = await db.post.create({
    data: body,
  })

  return NextResponse.json(post, { status: 201 })
}

动态 API 路由

处理特定资源的 CRUD 操作:

// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'

export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const post = await db.post.findUnique({
    where: { id: params.id },
  })

  if (!post) {
    return NextResponse.json(
      { error: '文章不存在' },
      { status: 404 }
    )
  }

  return NextResponse.json(post)
}

export async function DELETE(
  request: Request,
  { params }: { params: { id: string } }
) {
  await db.post.delete({
    where: { id: params.id },
  })

  return new NextResponse(null, { status: 204 })
}

乐观更新

乐观更新是一种让 UI 响应更快的技巧,它在等待服务器响应时先更新 UI,如果失败了再回滚:

'use client'

import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'

export default function PostWithLike({ post }: { post: Post }) {
  const [optimisticPost, addOptimisticPost] = useOptimistic(
    post,
    (state, newPost: Post) => {
      return {
        ...state,
        ...newPost,
      }
    }
  )

  async function handleLike() {
    addOptimisticPost({ ...post, likes: post.likes + 1 })
    await likePost(post.id)
  }

  return (
    <div>
      <h2>{post.title}</h2>
      <p>{optimisticPost.likes} 点赞</p>
      <button onClick={handleLike}>点赞</button>
    </div>
  )
}

重新验证

当数据变更后,需要重新验证相关页面的缓存:

'use server'

import { revalidatePath } from 'next/cache'

export async function updatePost(id: string, data: FormData) {
  // 更新文章
  await db.post.update({
    where: { id },
    data: { title: data.get('title') },
  })

  // 重新验证相关页面
  revalidatePath('/posts')
  revalidatePath(`/posts/${id}`)
}

使用标签进行更精确的控制:

'use server'

import { revalidateTag } from 'next/cache'

export async function createComment() {
  await db.comment.create(/* ... */)

  // 重新验证多个缓存
  revalidateTag('comments')
  revalidateTag('posts') // 更新文章评论数
}

实用建议

这里分享几个在处理数据变更时特别实用的技巧。

使用 Server Actions 简化代码

实际开发中,我发现 Server Actions 通常是更好的选择,代码更简洁:

// 推荐这样做 - 使用 Server Actions
<form action={createPost}>
  <input name="title" />
  <button>提交</button>
</form>

// 传统方式也可以,但代码会复杂一些
<form onSubmit={handleSubmit}>
  <input name="title" />
  <button>提交</button>
</form>

始终验证输入

这个技巧特别重要——永远不要相信用户输入,一定要验证:

'use server'

import { z } from 'zod'

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export async function register(formData: FormData) {
  const validatedData = schema.parse({
    email: formData.get('email'),
    password: formData.get('password'),
  })

  // 然后可以安全地使用验证后的数据
}

处理错误状态

给用户明确的错误反馈,这个小细节能大大提升用户体验:

'use client'

import { useFormState } from 'react-dom'

export default function Form() {
  const [state, formAction] = useFormState(action, initialState)

  return (
    <form action={formAction}>
      {/* 表单字段 */}
      {state.errors && <div>{state.errors}</div>}
    </form>
  )
}

总结

本节我们学习了 Next.js 中的数据变更方式,包括 Server Actions、API 路由、乐观更新等。Server Actions 是 Next.js 推荐的新方式,能让数据变更的代码更简洁。合理使用这些技术,能让你的应用交互更加流畅。

如果你对本节内容有任何疑问,欢迎在评论区提出来,我们一起学习讨论。

原文地址:https://blog.uuhb.cn/archives/Next-js-07.html