在上一章中,您使用 URL 搜索参数和 Next.js API实现了搜索和分页。让我们继续处理发票页面,添加创建、更新和删除发票的功能!
在本章中,我们将要讨论几个主题:
React Server Actions 是什么以及如何使用它们来改变数据。
如何使用表单和服务器组件。
使用本机formData对象的最佳实践,包括类型验证。
如何使用
revalidatePath
API 重新验证客户端缓存。如何创建具有特定 ID 的动态路线段。
什么是服务器操作?
React Server Actions 允许您直接在服务器上运行异步代码。它们消除了创建API端点来改变数据的需要。相反,您可以编写在服务器上执行并可从客户端或服务器组件调用的异步函数。
安全性是 Web 应用程序的首要任务,因为它们可能容易受到各种威胁。这就是服务器操作的作用所在。它们提供有效的安全解决方案,可防止不同类型的攻击,保护您的数据并确保授权访问。服务器操作通过 POST 请求、加密闭包、严格输入检查、错误消息哈希和主机限制等技术实现这一点,所有这些技术共同作用,可显著增强应用程序的安全性。
将表单与服务器操作结合使用
在 React 中,你可以使用元素action中的属性<form>
来调用操作。操作将自动接收原生的FormData对象,包含捕获的数据。
例如:
// Server Component
export default function Page() {
// Action
async function create(formData: FormData) {
'use server';
// Logic to mutate data...
}
// Invoke the action using the "action" attribute
return <form action={create}>...</form>;
}
在服务器组件内调用服务器操作的一个优点是逐步增强 - 即使在客户端禁用 JavaScript,表单仍然有效。
Next.js 与服务器操作
Server Actions 也与 Next.js缓存深度集成。当通过服务器操作提交表单时,您不仅可以使用该操作来改变数据,还可以使用revalidatePath
和等API重新验证相关缓存revalidateTag
,接下来让我们看看它们是如何协同工作的!
创建发票
以下是创建新发票需要采取的步骤:
创建一个表单来捕获用户的输入。
创建一个服务器操作并从表单调用它。
在服务器操作中,从对象中提取数据formData。
验证并准备要插入数据库的数据。
插入数据并处理任何错误。
重新验证缓存并将用户重定向回发票页面。
创建新路线和表单
首先,在 /invoices
文件夹内,创建一个新的路径 /app/dashboard/invoices/create/page.tsx
文件。
在page.tsx上面写入代码
import Form from '@/app/ui/invoices/create-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
export default async function Page() {
const customers = await fetchCustomers();
return (
<main>
<Breadcrumbs
breadcrumbs={[
{ label: 'Invoices', href: '/dashboard/invoices' },
{
label: 'Create Invoice',
href: '/dashboard/invoices/create',
active: true,
},
]}
/>
<Form customers={customers} />
</main>
);
}
这个组件的主要目的是提供一个创建发票的界面,包括导航和表单。它从服务器获取客户数据,然后将这些数据传递给表单组件,以便用户可以选择客户并创建新的发票。
这时候我们看到编辑器可能会显示import Form from '@/app/ui/invoices/create-form';
,import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
显示红色,因为我们还没有实现这块组件的逻辑。
编辑/app/ui/invoices/create-form
实现from表单提交
import Link from 'next/link';
import {
CheckIcon,
ClockIcon,
CurrencyDollarIcon,
UserCircleIcon,
} from '@heroicons/react/24/outline';
import { Button } from '@/app/ui/button';
export type CustomerField = {
id: string;
name: string;
};
export default function Form({ customers }: { customers: CustomerField[] }) {
return (
<form>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer Name */}
<div className="mb-4">
<label htmlFor="customer" className="mb-2 block text-sm font-medium">
Choose customer
</label>
<div className="relative">
<select
id="customer"
name="customerId"
className="peer block w-full cursor-pointer rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
defaultValue=""
>
<option value="" disabled>
Select a customer
</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
</div>
</div>
{/* Invoice Amount */}
<div className="mb-4">
<label htmlFor="amount" className="mb-2 block text-sm font-medium">
Choose an amount
</label>
<div className="relative mt-2 rounded-md">
<div className="relative">
<input
id="amount"
name="amount"
type="number"
step="0.01"
placeholder="Enter USD amount"
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
/>
<CurrencyDollarIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
</div>
</div>
{/* Invoice Status */}
<fieldset>
<legend className="mb-2 block text-sm font-medium">
Set the invoice status
</legend>
<div className="rounded-md border border-gray-200 bg-white px-[14px] py-3">
<div className="flex gap-4">
<div className="flex items-center">
<input
id="pending"
name="status"
type="radio"
value="pending"
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
/>
<label
htmlFor="pending"
className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600"
>
Pending <ClockIcon className="h-4 w-4" />
</label>
</div>
<div className="flex items-center">
<input
id="paid"
name="status"
type="radio"
value="paid"
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
/>
<label
htmlFor="paid"
className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white"
>
Paid <CheckIcon className="h-4 w-4" />
</label>
</div>
</div>
</div>
</fieldset>
</div>
<div className="mt-6 flex justify-end gap-4">
<Link
href="/dashboard/invoices"
className="flex h-10 items-center rounded-lg bg-gray-100 px-4 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-200"
>
Cancel
</Link>
<Button type="submit">Create Invoice</Button>
</div>
</form>
);
}
From表单上面的Button实现方式
import clsx from 'clsx';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
}
export function Button({ children, className, ...rest }: ButtonProps) {
return (
<button
{...rest}
className={clsx(
'flex h-10 items-center rounded-lg bg-blue-500 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 active:bg-blue-600 aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
className,
)}
>
{children}
</button>
);
}
编辑import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'
组件,这个组件的主要目的是创建一个灵活的面包屑导航,可以根据传入的数据动态生成导航链接,并突出显示当前活动项。它提供了清晰的页面层次结构,帮助用户了解他们在网站中的位置。
import { clsx } from 'clsx';
import Link from 'next/link';
import { lusitana } from '@/app/ui/fonts';
interface Breadcrumb {
label: string;
href: string;
active?: boolean;
}
export default function Breadcrumbs({
breadcrumbs,
}: {
breadcrumbs: Breadcrumb[];
}) {
return (
<nav aria-label="Breadcrumb" className="mb-6 block">
<ol className={clsx(lusitana.className, 'flex text-xl md:text-2xl')}>
{breadcrumbs.map((breadcrumb, index) => (
<li
key={breadcrumb.href}
aria-current={breadcrumb.active}
className={clsx(
breadcrumb.active ? 'text-gray-900' : 'text-gray-500',
)}
>
<Link href={breadcrumb.href}>{breadcrumb.label}</Link>
{index < breadcrumbs.length - 1 ? (
<span className="mx-3 inline-block">/</span>
) : null}
</li>
))}
</ol>
</nav>
);
}
当您点击提交按钮时,生成的链接http://172.16.100.104/dashboard/invoices/create?customerId=cc27c14a-0acf-4f4a-a6c9-d45682c144b9&amount=111&status=paid
表单提交:
在 create-form.tsx 文件中,表单没有指定action属性。这意味着表单会默认提交到当前页面的 URL。
表单数据:
customerId: 从下拉菜单中选择的客户 ID
amount: 在输入框中输入的金额
status: 从单选按钮中选择的状态(pending 或 paid)
默认表单行为:
当表单提交时,浏览器会将表单数据作为查询参数附加到当前 URL 上。
URL 构成:
查询参数: ?customerId=cc27c14a-0acf-4f4a-a6c9-d45682c144b9&amount=111&status=paid
参数解释:
customerId=cc27c14a-0acf-4f4a-a6c9-d45682c144b9: 选择的客户 ID
amount=111: 输入的金额
status=paid: 选择的发票状态
需要注意的是,这种 URL生成方式是浏览器的默认行为,而不是由React或Next.js直接控制的。在实际应用中,通常会使用 JavaScript 来处理表单提交,防止页面刷新并通过 API 发送数据。
以上内容为分页的详细技术实现步骤,如果你想看更多内容或者能够看到技术更新的内容,请百度搜索:曲速引擎 warp drive csdn
在首页找到我的地址访问即可,一线更新内容将会在我的个人博客上面更新,谢谢大家。
更详细内容查看
独立博客 https://www.dataeast.cn/
CSDN博客 https://blog.csdn.net/siberiaWarpDrive
B站视频空间 https://space.bilibili.com/25871614?spm_id_from=333.1007.0.0
关注 “曲速引擎 Warp Drive” 微信公众号