在探索 Next.js 的过程中,我们经常需要在客户端和服务端之间传递数据。传统上,这意味着要创建复杂的 API 路由来处理表单提交或数据库更新。但现在,随着 Server Actions(服务端动作) 的引入,这一过程变得前所未有的简洁和强大。
在这篇文章中,我们将深入探讨 Server Actions 的核心概念、配置方法以及如何在实际项目中高效地使用它们。我们将一起学习如何摆脱繁琐的手动 API 创建,直接在 UI 中调用服务端逻辑,从而构建更快、更安全的 Web 应用。
目录
什么是 Next.js 中的 Server Actions?
简单来说,Server Actions 是一个异步服务端函数,它可以在服务端运行,但可以直接从客户端或服务端组件中调用。这使得我们能够非常直观地处理 数据变更,如表单提交、数据更新、身份验证以及后台任务(如发送邮件等)。
核心优势
- 简化代码结构:不再需要为每一个表单创建单独的 API Route (
/api/submit)。我们可以将逻辑直接写在需要它的组件旁边,或者放在单独的文件中引入。 - 增强安全性:Server Actions 通过自动生成防 CSRF 令牌和仅暴露公共方法来保护应用。敏感代码(如数据库密钥)永远不会被发送到客户端。
- 渐进式增强:如果 JavaScript 尚未加载或被禁用,表单仍然可以通过传统的 POST 请求提交并正常工作。
核心指令:"use server"
为了定义一个 Server Action,我们需要使用 "use server" 指令。这告诉 Next.js 将此函数的代码打包并在服务端执行,而不是包含在客户端的 JavaScript bundle 中。
- 在独立文件中:将
"use server"放在文件的最顶部。该文件导出的所有函数都将被视为 Server Actions。 - 在组件内部定义:如果是在服务端组件内部定义 Action,将
"use server"放在函数体的顶部。
Server Actions 的两种实现方式
1. 独立文件中的 Server Actions(推荐用于客户端组件)
当你需要在 客户端组件("use client")中触发服务端逻辑时,这是最常用的方式。你需要创建一个单独的文件来存放服务端逻辑,然后将其导入到客户端组件中。
文件结构:
/src
/app
/actions
formSubmit.js <-- Server Action 文件
page.js <-- 客户端/服务端组件
代码示例 (actions/formSubmit.js):
// actions/formSubmit.js
// 添加 ‘use server‘ 指令,标记该文件导出的函数均为 Server Actions
‘use server‘
export async function createUser(formData) {
// 模拟数据库操作
const name = formData.get(‘name‘);
const email = formData.get(‘email‘);
console.log(`正在创建用户: ${name}, ${email}`);
// 在这里进行数据库插入或 API 调用
// await db.users.create({ data: { name, email } })
return { success: true, message: "用户创建成功!" };
}
如何在客户端组件中使用:
// app/page.js
‘use client‘
import { createUser } from ‘./actions/formSubmit‘;
export default function Page() {
return (
);
}
2. 组件内联 Server Actions(仅限服务端组件)
如果你正在编写一个 服务端组件,你可以直接在组件内部定义 Action。这种方式适合逻辑简单且与 UI 紧密结合的场景。
代码示例:
// app/page.js
// 这是一个服务端组件,默认就是服务端组件
export default function Page() {
// 定义内联 Server Action
async function inlineAction(formData) {
‘use server‘ // 必须在函数体顶部添加指令
console.log(‘内联 Action 被调用‘);
// 处理逻辑...
}
return (
);
}
实战演练:构建一个用户管理系统
理论讲完了,让我们通过一个完整的实战例子来巩固这些知识。我们将构建一个简单的应用,允许用户输入信息,并将数据保存到本地的 data.json 文件中。这个过程涵盖了表单处理、文件系统操作以及动态 UI 更新。
第一步:项目初始化
首先,我们需要搭建开发环境。请打开你的终端并执行以下命令。
npx create-next-app@latest my-server-actions
创建过程中,你会遇到一系列配置问题。为了确保与本文教程的一致性,建议按照以下选项进行配置(当然,根据你的偏好选择 TypeScript 也是完全可以的):
√ Would you like to use TypeScript? ... No # 或 Yes,取决于你的偏好
√ Would you like to use ESLint? ... Yes
√ Would you like to use Tailwind CSS? ... No
√ Would you like to use `src/` directory? ... Yes
√ Would you like to use App Router? ... Yes # 必须是 Yes,Server Actions 依赖 App Router
√ Would you like to customize the default import alias (@/*)? ... No
配置完成后,进入项目目录:
cd my-server-actions
第二步:准备数据存储
为了模拟数据库,我们将在项目根目录下的 INLINECODE9c3508a1 文件夹中创建一个 INLINECODE6d55c967 文件。请确保在创建项目后,手动添加该文件并填入一些初始数据。
文件路径:src/data.json
[
{"name": "张三", "age": "25", "city": "北京"},
{"name": "李四", "age": "30", "city": "上海"}
]
第三步:创建 Server Action
现在,让我们编写核心逻辑。我们将创建一个 Server Action 来读取表单数据,并将其追加到我们的 JSON 文件中。
文件路径:src/app/actions.js
‘use server‘
// 引入 Node.js 文件系统模块
import fs from ‘fs‘;
import path from ‘path‘;
// 定义异步函数处理表单提交
export async function formSubmit(formData) {
// 1. 从 formData 中获取输入字段
// formData 是一个原生的 FormData 对象,可以使用 .get() 方法
const name = formData.get(‘name‘);
const age = formData.get(‘age‘);
const city = formData.get(‘city‘);
// 简单的验证
if (!name || !age || !city) {
return { success: false, message: "所有字段都是必填的" };
}
// 2. 构建新对象
const newItem = { name, age, city };
// 3. 读取现有数据(为了演示简单起见,这里使用同步操作,生产环境建议使用异步库)
// 注意:在实际生产环境中,你应该使用数据库(如 Prisma, MongoDB)而不是直接操作文件
const filePath = path.join(process.cwd(), ‘src‘, ‘data.json‘);
// 读取并解析 JSON
let currentData = [];
try {
const fileContent = fs.readFileSync(filePath, ‘utf8‘);
currentData = JSON.parse(fileContent);
} catch (error) {
console.error("读取文件失败,可能是首次运行", error);
}
// 4. 更新数据
currentData.push(newItem);
// 5. 写回文件
fs.writeFileSync(filePath, JSON.stringify(currentData, null, 2));
// 6. 返回响应(可选,用于处理 Toast 或重定向)
console.log("数据已成功保存到 data.json");
}
第四步:构建用户界面
接下来,我们在主页面中引入这个 Action,并构建一个包含表单和数据展示表格的 UI。
文件路径:src/app/page.js
‘use client‘;
// 引入我们的 Server Action
import { formSubmit } from ‘./actions‘;
// 引入初始数据(注意:在真实应用中,通常通过服务端组件 props 传递数据,而不是直接 import JSON)
import initialData from ‘../../data.json‘;
import { useState } from ‘react‘;
export default function Page() {
// 使用 state 来管理页面显示的数据,以便在提交后实时刷新
const [data, setData] = useState(initialData);
// 注意:这里的 formSubmit 直接作为 action 传递给 form 元素
// Next.js 会自动处理请求,并在完成后重新渲染组件
// 为了演示简化,我们在 action 里更新文件,这里手动刷新页面来查看最新数据
// 或者我们可以使用 revalidatePath API (下文会提到)
return (
用户信息录入系统
{/* 表单区域 */}
现有数据列表
{/* 数据展示区域 */}
姓名
年龄
城市
{data.map((item, key) => (
{item.name}
{item.age}
{item.city}
))}
);
}
第五步:运行与测试
- 保存所有文件。
- 在终端运行
npm run dev启动开发服务器。 - 打开浏览器访问
http://localhost:3000。 - 填写表单并点击提交。
- 观察结果:虽然页面可能不会自动刷新(因为这是一个简单的客户端 state 示例),但你可以检查项目根目录下的
src/data.json文件,你会发现新数据已经被成功追加进去了!
> 实用见解: 如果你希望提交后页面自动刷新显示新数据,你可以在 Server Action 中使用 revalidatePath 函数。这将在下一节的高级技巧中详细解释。
进阶技巧与最佳实践
仅仅让代码跑通是不够的,作为专业的开发者,我们需要关注性能、用户体验和代码的可维护性。以下是几个进阶话题。
1. 处理加载状态与错误
在表单提交时,用户需要知道系统正在处理。我们可以利用 useFormStatus 钩子来实现这一功能,而无需手动管理 state。
示例:
‘use client‘
import { useFormStatus } from ‘react-dom‘
export function SubmitButton() {
const { pending } = useFormStatus()
return (
)
}
你可以在上面的 INLINECODE06c2fbd4 中将 INLINECODE794094c6 替换为这个组件。当 Server Action 运行时,按钮会自动禁用并显示“提交中…”。
2. 数据重新验证
在 Next.js 中,为了提高性能,默认会缓存获取的数据(Fetch 缓存或 Router 缓存)。当你通过 Server Action 更新了数据后,你需要告诉 Next.js 重新验证某些路径,以清除缓存并显示最新数据。
你只需要在 Server Action 中导入并使用 INLINECODE101611da 或 INLINECODE5531f524。
‘use server‘
import { revalidatePath } from ‘next/cache‘
export async function updateUser() {
// ... 更新数据库逻辑 ...
// 告诉 Next.js 重新验证 ‘/‘ 路径的缓存
// 这会导致访问首页时重新获取最新数据
revalidatePath(‘/‘)
}
3. 重定向
在完成操作(如登录或创建文章)后,我们通常希望将用户引导到另一个页面。使用 redirect 函数非常简单。
‘use server‘
import { redirect } from ‘next/navigation‘
export async function createPost(formData) {
// ... 创建逻辑 ...
// 成功后重定向到帖子列表页
redirect(‘/posts‘)
}
4. 错误处理与 Server Errors
Server Actions 允许你返回普通对象,也可以抛出错误。如果 Action 抛出错误,最近的 error.js Error Boundary 将捕获它。
‘use server‘
export async function dangerousAction() {
if (Math.random() > 0.5) {
throw new Error(‘随机错误发生了!‘);
}
return { success: true };
}
在客户端组件中,你可以通过 try-catch 配合 startTransition 来处理,或者依赖 Next.js 的全局错误处理机制。
5. 安全性注意事项
虽然 Server Actions 会自动进行 CSRF 保护,但作为开发者,你仍需注意:
- 输入验证:永远不要信任来自客户端的数据。在 Server Action 中必须再次严格验证所有输入(使用 Zod 或 Yup 等库)。
- 权限检查:确保用户有权限执行该操作(例如,只有管理员才能删除文章)。
总结与后续步骤
通过这篇文章,我们深入学习了 Next.js Server Actions 的核心机制。我们一起探索了如何从零开始构建一个处理表单提交的应用,并掌握了独立文件 Action 和内联 Action 的区别。
关键要点回顾:
- Server Actions 允许我们在服务端直接运行代码,无需手动创建 API 端点。
- 使用
"use server"指令来标记服务端函数。 - 它们非常适合处理 数据变更,并与 Next.js 的缓存系统(通过
revalidatePath)无缝集成。 - 结合
useFormStatus可以轻松打造出色的用户体验。
你可以尝试的下一步:
- 尝试重构现有的 API 路由,将其转换为 Server Actions,感受代码量的减少。
- 结合数据库(如 Prisma + SQLite)来替换示例中的 JSON 文件存储,体验更真实的开发流程。
- 探索在 Server Actions 中使用 Zod 进行强大的数据验证。
希望这篇文章能帮助你更好地理解 Next.js Server Actions。编码愉快!