时间:2025-12-27 21:46
人气:
作者:admin
点赞 + 收藏 === 学会????????????
我们知道,Next.js 最核心的特性便是 支持静态生成(SSG)和服务端渲染(SSG),这也就意味着我们可以以部署 Node 服务的方式,将其部署在服务器上,用请求后端接口类似的形式来请求页面文件。换句话说,我们其实可以直接把 Next.js 看成一个特殊的 Node 后端服务。
既然是在服务端进行运行,那么它在数据库的查询方式上自然和一般的 SPA 客户端进行查询有所区别。
我们先简单分析一下一般 React SPA 项目的前后端交互:
这样是一套完整的前后端分离接口交互的编写。很显然,这样的交互、从数据库中拿数据的行为是以页面为单位的,当用户重新在这个页面上发生相同的交互行为时,对应的 HTTP 请求会 100%完整地复刻一遍。
而在 Next.js 中,基于其自身的特殊性,提供了一个与数据库直接进行交互的特殊方式:Server Actions。
Server Actions 直译为 服务端行为,是 Next.js 中的一个特殊概念,用于在服务端直接进行数据库查询等操作,而不是在客户端进行异步请求。
下面贴一下官方的一些解释:
React Server Actions allow you to run asynchronous code directly on the server. They eliminate the need to create API endpoints to mutate your data. Instead, you write asynchronous functions that execute on the server and can be invoked from your Client or Server Components.
React Server Actions 允许您直接在服务器上运行异步代码。您无需创建 API 端点来更改数据。相反,您可以编写在服务器上执行的异步函数,并可从客户端或服务器组件中调用。
Security is a top priority for web applications, as they can be vulnerable to various threats. This is where Server Actions come in. They offer an effective security solution, protecting against different types of attacks, securing your data, and ensuring authorized access. Server Actions achieve this through techniques like POST requests, encrypted closures, strict input checks, error message hashing, and host restrictions, all working together to significantly enhance your app's safety.
安全性是网络应用程序的重中之重,因为它们很容易受到各种威胁。这就是服务器操作的用武之地。它们提供了一种有效的安全解决方案,可抵御各种类型的攻击、保护数据安全并确保授权访问。Server Actions 通过 POST 请求、加密关闭、严格输入检查、错误信息散列和主机限制等技术来实现这一目标,所有这些技术共同作用,大大提高了应用程序的安全性。
因此简单来说,所谓的 Server Actions 实际上就是一个在 Next.js 中的一个 普通异步函数,只不过这个异步函数可以直接操作数据库而已。这个函数与 React Server Components 深度集成,可以看成是组件本身的一部分。
这个异步函数的执行单纯在服务端进行,而 不以接口请求的形式获取数据,因此 无法在网络调试中看到接口的请求,可以理解为这个异步函数是在服务端直接执行的。
也正是因此,不将请求的数据暴露出来,能够最大程度上保证数据的安全性。
Server Actions 还与 Next.js 缓存深度集成。通过其提交表单时,不仅可以使用该动作更改数据,还可以使用 revalidatePath 和 revalidateTag 等 Next API 刷新相关页面的缓存,以确保在每次数据更新之后重新访问该页面时能够获取最新的数据。
我们可以试着总结一下 Server Action 和传统 API Endpoints 的优缺点:
优点:
简化代码结构
onClick 中传递 Server Action 异步函数即可,无需调用 API Endpoint。直接与组件交互
减少网络请求
自动处理错误
缺点:
难以复用
测试难度大
可扩展性
优点:
松散耦合
易于测试
灵活性
可扩展性
缺点:
额外的网络开销
复杂性
代码分散
适用场景
Server Actions
API Endpoints
上面我们简单讨论了 Server Actions 的定义以及其与一般 API Endpoint 的区别,接下来我们看一个实际使用 Server Action 与数据库进行交互的例子。
此处的例子来源于Next 官方教程,需要的同学可以去官网进行查阅。此处需要实现的目标是对数据库 Invoices(发票)数据的 增、改、删 ,也就是直接修改数据库数据。
我们可以看一下具体的代码,并对它进行解析:
"use server"; // Server Actions 只在服务端运行
import { z } from "zod";
import { sql } from "@vercel/postgres";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
// zod 是一个用于验证数据的库,它可以帮助我们定义数据的结构,并验证数据是否符合这个结构。
const FormSchema = z.object({
id: z.string(),
customerId: z.string(),
amount: z.coerce.number(), // coerce.number() 会将字符串转换为数字
status: z.enum(["pending", "paid"]),
date: z.string(),
});
const CreateInvoice = FormSchema.omit({ id: true, date: true });
export async function createInvoice(formData: FormData) {
// CreateInvoice.parse 方法用于验证 formData 中的数据是否符合 CreateInvoice 的结构
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get("customerId"),
amount: formData.get("amount"),
status: formData.get("status"),
});
const amountInCents = amount * 100;
const date = new Date().toISOString().split("T")[0]; // 获取当前日期:YYYY-MM-DD
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
revalidatePath("/dashboard/invoices"); // revalidatePath 的作用是重新生成指定路由的页面,以便在下次访问时显示最新数据
redirect("/dashboard/invoices"); // 重定向
}
const UpdateInvoice = FormSchema.omit({ id: true, date: true });
export const updateInvoice = async (id: string, formData: FormData) => {
const { customerId, amount, status } = UpdateInvoice.parse({
customerId: formData.get("customerId"),
amount: formData.get("amount"),
status: formData.get("status"),
});
const amountInCents = amount * 100; // 将金额转换为分
await sql`
UPDATE invoices
SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
WHERE id = ${id}
`;
revalidatePath("/dashboard/invoices"); // 重新生成指定路由的页面
redirect("/dashboard/invoices"); // 重定向
};
export const deleteInvoice = async (id: string) => {
await sql`DELETE FROM invoices WHERE id = ${id}`;
revalidatePath("/dashboard/invoices");
};
这段代码是一个完整的 actions.ts 文件。对相同对象的数据库操作可以用单独的文件进行存放。
首先我们看一下导入。这里使用了 zod 库用于 TS 数据验证,@vercel/postgres 用于数据库操作,next/cache 和 next/navigation 用于缓存和导航控制。定义了一个 FormSchema 来验证发票表单数据的结构。
const CreateInvoice = FormSchema.omit({ id: true, date: true });
CreateInvoice 是一个从 FormSchema 中去除 id 和 date 字段的验证器,用于创建发票时的数据验证。export async function createInvoice(formData: FormData) {
// CreateInvoice.parse 方法用于验证 formData 中的数据是否符合 CreateInvoice 的结构
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get("customerId"),
amount: formData.get("amount"),
status: formData.get("status"),
});
const amountInCents = amount * 100;
const date = new Date().toISOString().split("T")[0]; // 获取当前日期:YYYY-MM-DD
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
revalidatePath("/dashboard/invoices"); // 重新生成指定路由的页面
redirect("/dashboard/invoices"); // 重定向
}
createInvoice 函数处理发票创建:
formData 获取并验证数据。revalidatePath 重新生成 /dashboard/invoices 路由的页面,以便显示最新数据。redirect 重定向到 /dashboard/invoices。其余两函数对 Invoice 的操作大同小异,三个函数分别实现了增、改、删的操作。
那么,对应具体该怎么在组件中使用 Server Actions 呢?
我们对应的来看一下提交表单的内容:
import { CustomerField } from "@/app/lib/definitions";
import Link from "next/link";
import {
CheckIcon,
ClockIcon,
CurrencyDollarIcon,
UserCircleIcon,
} from "@heroicons/react/24/outline";
import { Button } from "@/app/ui/button";
import { createInvoice } from "@/app/lib/actions";
export default function Form({ customers }: { customers: CustomerField[] }) {
return (
<form action={createInvoice}>
{/* ~此处省略表单的具体内容~ *