北鸟南游的博客 北鸟南游的博客
首页
  • 前端文章

    • JavaScript
    • Nextjs
  • 界面

    • html
    • css
  • 计算机基础
  • 后端语言
  • linux
  • mysql
  • 工具类
  • 面试相关
  • 图形学入门
  • 入门算法
  • 极客专栏
  • 慕课专栏
  • 电影资源
  • 儿童动漫
  • 英文
关于我
归档
GitHub (opens new window)
首页
  • 前端文章

    • JavaScript
    • Nextjs
  • 界面

    • html
    • css
  • 计算机基础
  • 后端语言
  • linux
  • mysql
  • 工具类
  • 面试相关
  • 图形学入门
  • 入门算法
  • 极客专栏
  • 慕课专栏
  • 电影资源
  • 儿童动漫
  • 英文
关于我
归档
GitHub (opens new window)
  • JavaScript

    • 原生js
    • vue
    • react
    • node
    • nextjs
      • 01-nextjs起步-[翻译官网案例]
      • 02-样式设置【翻译官网案例】
      • 03-优化字体和图像
      • 04-创建layouts布局和pages页面
      • 05-在页面之间导航
      • 06-设置数据库
      • 07-fetch获取数据
      • 08-静态和动态渲染
      • 09-Streaming数据模式
      • 10-部分预渲染
      • 11-添加搜索和分页
      • 12-提交数据
        • 本章目标
        • 什么是Server Actions
          • 使用forms的server actions
          • next.js的server actions
        • 创建Invoices数据
          • 新加一个路由和form
          • 创建server action
          • 从formData中提取数据
          • 验证和准备数据
          • 将数据插入数据库
          • 重新验证和重定向
        • 编辑更新Invoices数据
          • 使用 invoices id创建一个新的动态路由
          • 从页面参数获取 invoice id
          • 获取特定的invoice
          • 将id传递给服务器操作
        • 删除Invoices数据
      • 13-处理错误
      • 14-提高交互性
      • 15-添加认证授权
      • 16-添加Metadata头数据
    • 其它框架
  • 界面

  • front
  • javascript
  • nextjs
北鸟南游
2024-06-24
目录

12-提交数据

原文链接:https://nextjs.org/learn/dashboard-app/mutating-data (opens new window)

在上一章中,您使用URL search Params和Next.js API实现了搜索和分页。让我们添加创建、更新和删除Invoices 页面的功能,继续使用Invoices 页面!

# 本章目标

  • 什么是React Server Actions,怎么使用React Server Actions进行提交数据
  • 如何使用表单和服务器组件。
  • 使用原生的 formData 对象的最佳实践,包括类型验证。
  • 如何使用revalidatePath API重新验证客户端缓存。
  • 如何创建具有特定ID的动态路由。

# 什么是Server Actions

React Server Actions允许您直接在服务器上运行异步代码。该功能取消了通过创建API才能提交数据的流程。现在,您可以编写在服务器上执行的异步函数,这些函数可以从客户端或服务器组件调用。 安全是web应用程序的首要任务,因为它们可能容易受到各种威胁。这就是服务器操作的作用所在。它们提供了一个有效的安全解决方案,可以抵御不同类型的攻击,保护您的数据安全,并确保授权访问。Server Actions 通过POST请求等技术实现这一点,加密闭包、严格的输入检查、错误消息哈希和主机限制等所有方案共同努力,显著提高应用程序的安全性。

# 使用forms的server actions

在react中,你可以使用react中form的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>;
}
1
2
3
4
5
6
7
8
9
10
11
12

在服务器组件中调用服务器操作的一个优点是渐进式增强——即使客户端禁用了JavaScript,表单也能工作。

# next.js的server actions

server actions是深度集成到next.js框架中。通过服务器操作提交表单时,您不仅可以使用该操作来变异数据,您还可以使用诸如revalidatePath和revalidateTag之类的API来重新验证关联的缓存。

# 创建Invoices数据

以下是创建新发票的步骤

  1. 创建一个表单来捕获用户的输入。
  2. 创建server action 并从表单中调用它。
  3. 在 server action 中,从formData中提取数据
  4. 验证并准备要插入数据库的数据。
  5. 插入数据并处理任何错误。
  6. 重新验证缓存并将用户重定向回invoice页面。

# 新加一个路由和form

首先,在/invoices文件夹中,使用page.tsx文件添加名为/create的路由: image.png

您将使用此路路由创建新invoie数据。在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>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

您的页面是一个服务器组件,用于获取客户并将其传递给<Form>组件。为了节省时间,我们已经为您创建了<Form>组件。 导航到<Form>组件,您会看到该表单:

  • 有一个<select>的下拉组件,内容是包含了customers的列表。
  • 包含一个类型为type="number"的input组件,设置amount数据
  • 包含一个类型为type="radio"的input组件,用来设置status数据。
  • 有一个类型为 type="submit"的提交按钮

打开http://localhost:3000/dashboard/invoices/create (opens new window),您应该会看到以下UI: image.png

# 创建server action

很好,现在让我们创建一个服务器操作,该操作将在提交表单时调用。 导航到lib目录并创建一个名为actions.ts的新文件。在该文件的顶部,添加React use server (opens new window)指令:

'use server';
1

通过添加'use server',将文件中所有导出的函数标记为“服务器操作”。然后可以导入这些服务器功能,并在客户端和服务器组件中使用这些功能。 您也可以通过在操作中添加'use server',直接在服务器组件中编写服务器操作。但对于本课程,我们将把它们全部组织在一个单独的文件中。 在actions.ts文件中,创建一个接受formData的新异步函数:

'use server';
 
export async function createInvoice(formData: FormData) {}
1
2
3

然后,在<Form>组件中,从actions.ts文件导入createInvoice。向<form>元素添加一个action属性,并调用createInvoice操作。

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}>
      // ...
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 从formData中提取数据

回到 actions.ts 文件中,您需要提取formData的值。有以下几种方法 (opens new window)可以使用,对于这个例子,让我们使用.get(name)方法。

'use server';
 
export async function createInvoice(formData: FormData) {
  const rawFormData = {
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  };
  // Test it out:
  console.log(rawFormData);
}
1
2
3
4
5
6
7
8
9
10
11

提示:如果您使用的表单有很多字段,您可能需要考虑将entries()方法与JavaScript的Object.fromEntries()一起使用。例如 const rawFormData = Object.fromEntries(formData.entries());

要检查所有连接是否正确,请继续尝试该表单。提交后,您应该会在终端中看到刚刚输入表单的数据。 现在您的数据是一个对象,它将更容易使用。

# 验证和准备数据

在将表单数据发送到数据库之前,您希望确保它的格式和类型正确。如果您还记得本课程前面的内容,您的invoices表需要以下格式的数据:

export type Invoice = {
  id: string; // Will be created on the database
  customer_id: string;
  amount: number; // Stored in cents
  status: 'pending' | 'paid';
  date: string;
};
1
2
3
4
5
6
7

到目前为止,从form中您只有customer_id, amount, and status 数据。

# 类型验证和是否必填

重要的是要验证表单中的数据是否与数据库中的预期类型一致。例如,如果在操作中添加console.log:

console.log(typeof rawFormData.amount);
1

你将注意到amount是string类型,而不是number类型。但是input类型为type=“number”的输入元素实际上返回的是字符串,而不是数字! 要处理类型验证,您有几个选项。虽然您可以手动验证类型,但使用类型验证库可以节省时间和精力。对于您的示例,我们将使用Zod (opens new window),这是一个TypeScript优先验证库,可以为您简化此任务。 在actions.ts文件中,导入Zod并定义一个与表单对象匹配的结构。此结构将在将formData保存到数据库之前对其进行验证。

'use server';
 
import { z } from 'zod';
 
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum(['pending', 'paid']),
  date: z.string(),
});
 
const CreateInvoice = FormSchema.omit({ id: true, date: true });
 
export async function createInvoice(formData: FormData) {
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

zod插件将amount字段,设置为在验证其类型的同时将字符串强制(更改)为数字。 然后,您可以将rawFormData传递给CreateInvoice以验证类型:

// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
}
1
2
3
4
5
6
7
8

# 以分为单位处理值

通常,在数据库中存储以分为单位的货币值是一种很好的做法,以消除JavaScript浮点错误并确保更高的准确性。 让我们把金额换算成分:

// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
}
1
2
3
4
5
6
7
8
9

# 创建日期数据

让我们为invoice的创建日期创建一个格式为“YYYY-MM-DD”的新日期:

// ...
export async function createInvoice(formData: FormData) {
  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];
}
1
2
3
4
5
6
7
8
9
10

# 将数据插入数据库

现在您已经拥有了数据库所需的所有值,可以创建一个SQL查询,将新发票插入数据库并传入变量:

import { z } from 'zod';
import { sql } from '@vercel/postgres';
 
// ...
 
export async function createInvoice(formData: FormData) {
  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];
 
  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

现在,我们没有处理任何错误。我们将在下一章中做这件事。

# 重新验证和重定向

Next.js有一个客户端路由器缓存,它将路由存储在用户的浏览器中一段时间。与预获取 (opens new window)一起,该缓存确保用户可以在路由之间快速导航,同时减少对服务器的请求数量。 由于您正在更新invoices路由中显示的数据,因此需要清除此缓存并触发对服务器的新请求。您可以使用Next.js中的revalidatePath (opens new window)函数来执行此操作:

'use server';
 
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
 
// ...
 
export async function createInvoice(formData: FormData) {
  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];
 
  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
 
  revalidatePath('/dashboard/invoices');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

一旦数据库已经被更新,将重新验证/dashboard/invoits路径,并从服务器获取新数据。此时,您还需要将用户重定向回/dashboard/invoices页面。您可以使用Next.js中的重定向函数来完成此操作:

'use server';
 
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
 
// ...
 
export async function createInvoice(formData: FormData) {
  // ...
 
  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

祝贺您刚刚实现了第一个服务器操作。如果一切正常,请添加新 invoices 数据进行测试:

  • 提交时应该重定向到 /dashboard/invoices路由下
  • 可以在表格的顶部看到新提交的数据

# 编辑更新Invoices数据

更新invoices表单类似于创建发票表单,更新需要传递发票id来更新数据库中的记录。让我们看看如何获取和传递invoices id。 您将采取以下步骤更新invoices

  • 使用 invoices id创建一个新的动态路由。
  • 从页面参数中读取 invoices id。
  • 从数据库中获取特定 invoices 数据。
  • 使用 invoices 数据预先填充表单。
  • 更新数据库中的 invoices 数据。

# 使用 invoices id创建一个新的动态路由

Next.js允许您在不知道确切的路由名称并希望根据props创建路由时创建动态路由 (opens new window)。这可能是博客文章标题、产品页面等。可以通过将文件夹的名称括在方括号[]中来创建动态路由。例如[id], [post] or [slug] 在你的/invoices文件夹中,创建一个名为[id]的新动态路由,然后使用page.tsx文件创建一个名为edit的新路由。您的文件结构应该如下所示: image.png

在<Table>组件中,注意到<UpdateInvoice />组件它从表记录中接收 invoices 的id。

export default async function InvoicesTable({
  query,
  currentPage,
}: {
  query: string;
  currentPage: number;
}) {
  return (
    // ...
    <td className="flex justify-end gap-2 whitespace-nowrap px-6 py-4 text-sm">
      <UpdateInvoice id={invoice.id} />
      <DeleteInvoice id={invoice.id} />
    </td>
    // ...
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

导航到<UpdateInvoice/>组件,并更新链接的href以接受id prop。可以使用模板文字链接到动态路由:

import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
 
// ...
 
export function UpdateInvoice({ id }: { id: string }) {
  return (
    <Link
      href={`/dashboard/invoices/${id}/edit`}
      className="rounded-md border p-2 hover:bg-gray-100"
    >
      <PencilIcon className="w-5" />
    </Link>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 从页面参数获取 invoice id

回到<Page>组件,粘贴以下代码:

import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
 
export default async function Page() {
  return (
    <main>
      <Breadcrumbs
        breadcrumbs={[
          { label: 'Invoices', href: '/dashboard/invoices' },
          {
            label: 'Edit Invoice',
            href: `/dashboard/invoices/${id}/edit`,
            active: true,
          },
        ]}
      />
      <Form invoice={invoice} customers={customers} />
    </main>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

现在你应该可以看到和/create页面有相似之处,除了导入了一个不同的表单(从edit-form.tsx文件);此表单预先填充表单的defaultValue 默认值,customer's name, invoice amount, and status;要预填充表单字段,您需要使用id获取特定invoice数据。 除了searchParams,页面组件还接受一个名为params的props,您可以使用它来访问id。更新您的<page>组件以接收props:

import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
 
export default async function Page({ params }: { params: { id: string } }) {
  const id = params.id;
  // ...
}
1
2
3
4
5
6
7
8

# 获取特定的invoice

  • 导入一个名为fetchInvoiceById的新函数,并将该id作为参数传递。
  • 导入fetchCustomers可获取下拉列表中的客户名称。

您可以使用Promise.all并行获取invoice和customers数据

import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
 
export default async function Page({ params }: { params: { id: string } }) {
  const id = params.id;
  const [invoice, customers] = await Promise.all([
    fetchInvoiceById(id),
    fetchCustomers(),
  ]);
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12

您将在终端中看到invoice prop 的临时TS错误,因为invoice可能undefined。现在不要担心,您将在下一章添加错误处理时解决它。 太棒了现在,测试所有东西是否正确连接。查看http://localhost:3000/dashboard/invoices (opens new window)然后单击铅笔图标编辑invoice。导航后,您应该会看到一个预先填充了invoice详细信息的表单: image.png

URL还应更新为如下id:: [http://localhost:3000/dashboard/invoice/uuid/edit](http://localhost:3000/dashboard/invoice/uuid/edit)

# 将id传递给服务器操作

最后,您希望将id传递给服务器操作,以便更新数据库中的正确记录。您不能像这样将id作为参数传递:

// Passing an id as argument won't work
<form action={updateInvoice(id)}>
1
2

相反,您可以使用JS绑定将id传递给服务器操作。这将确保传递给服务器操作的任何值都经过编码。

// ...
import { updateInvoice } from '@/app/lib/actions';
 
export default function EditInvoiceForm({
  invoice,
  customers,
}: {
  invoice: InvoiceForm;
  customers: CustomerField[];
}) {
  const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
 
  return (
    <form action={updateInvoiceWithId}>
      <input type="hidden" name="id" value={invoice.id} />
    </form>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

注意:在表单中使用隐藏的输入字段也可以(例如<input type="hidden" name="id" value={invoice.id}/>)。但是,这些值将在HTML源中显示为全文,这对于ID等敏感数据来说并不理想。

然后,在actions.ts文件中,创建一个新的操作 updateInvoice:

// Use Zod to update the expected types
const UpdateInvoice = FormSchema.omit({ id: true, date: true });
 
// ...
 
export async function updateInvoice(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');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

对比createInvoice操作,更新有如下操作

  • 从formData中提取数据。
  • 使用Zod验证类型。
  • 将amount转为分
  • 将变量传递给SQL查询
  • 调用revalidatePath以清除客户端缓存并发出新的服务器请求。
  • 调用redirect将用户重定向到invoice的页面。

通过编辑invoice进行测试。提交表单后,应将您重定向到invoice页面,并更新invoice。

# 删除Invoices数据

要使用服务器操作删除invoice,请将删除按钮包装在<form>元素中,并使用bind将id传递给服务器操作:

import { deleteInvoice } from '@/app/lib/actions';
 
// ...
 
export function DeleteInvoice({ id }: { id: string }) {
  const deleteInvoiceWithId = deleteInvoice.bind(null, id);
 
  return (
    <form action={deleteInvoiceWithId}>
      <button type="submit" className="rounded-md border p-2 hover:bg-gray-100">
        <span className="sr-only">Delete</span>
        <TrashIcon className="w-4" />
      </button>
    </form>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在actions.ts文件中,创建一个名为deleteInvoice的新操作。

export async function deleteInvoice(id: string) {
  await sql`DELETE FROM invoices WHERE id = ${id}`;
  revalidatePath('/dashboard/invoices');
}
1
2
3
4

由于此操作是在/dashboard/invoices路径中调用的,因此不需要调用redirect。调用revalidatePath将触发一个新的服务器请求并重新呈现表。

您还可以阅读有关服务器操作安全性 (opens new window)的更多信息,以获得更多学习

编辑 (opens new window)
上次更新: 2025/04/19, 14:22:11
11-添加搜索和分页
13-处理错误

← 11-添加搜索和分页 13-处理错误→

最近更新
01
色戒2007
04-19
02
真实real
04-19
03
Home
更多文章>
Theme by Vdoing | Copyright © 2018-2025 北鸟南游
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式