14
章节14
提升可访问性
在上一章节中,我们学习了如何捕获错误(包括 404 错误)并向用户显示回退。但是,我们仍然需要讨论拼图的另一部分:表单验证。让我们看看如何使用服务器操作实现服务器端验证,以及如何使用 React 的 useActionState
hook - 同时考虑到可访问性!
在本章节中...
以下是我们将会涵盖的主题
如何在 Next.js 中使用 eslint-plugin-jsx-a11y
来实施可访问性最佳实践。
如何实现服务器端表单验证。
如何使用 React 的 useActionState
hook 来处理表单错误,并将其显示给用户。
什么是可访问性?
可访问性指的是设计和实现每个人都可以使用的 Web 应用程序,包括残疾人士。这是一个涵盖许多领域的广泛主题,例如键盘导航、语义 HTML、图像、颜色、视频等等。
虽然我们不会在本课程中深入探讨可访问性,但我们将讨论 Next.js 中可用的可访问性功能以及一些使您的应用程序更易于访问的常用做法。
如果您想了解更多关于可访问性的信息,我们推荐由 web.dev 提供的 Learn Accessibility 课程。
在 Next.js 中使用 ESLint 可访问性插件
Next.js 在其 ESLint 配置中包含了 eslint-plugin-jsx-a11y
插件,以帮助尽早发现可访问性问题。例如,如果您有缺少 alt
文本的图像、错误地使用了 aria-*
和 role
属性等,此插件会发出警告。
可选地,如果您想尝试一下,请在您的 package.json
文件中添加 next lint
脚本
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start",
"lint": "next lint"
},
然后在您的终端中运行 pnpm lint
pnpm lint
这将指导您完成为项目安装和配置 ESLint 的过程。如果您现在运行 pnpm lint
,您应该会看到以下输出
✔ No ESLint warnings or errors
但是,如果您有一个缺少 alt
文本的图像会发生什么?让我们来看看!
转到 /app/ui/invoices/table.tsx
并从图像中删除 alt
属性。您可以使用编辑器的搜索功能快速找到 <Image>
<Image
src={invoice.image_url}
className="rounded-full"
width={28}
height={28}
alt={`${invoice.name}'s profile picture`} // Delete this line
/>
现在再次运行 pnpm lint
,您应该会看到以下警告
./app/ui/invoices/table.tsx
45:25 Warning: Image elements must have an alt prop,
either with meaningful text, or an empty string for decorative images. jsx-a11y/alt-text
虽然添加和配置 linter 不是必需的步骤,但它有助于在您的开发过程中捕获可访问性问题。
提升表单可访问性
我们已经在做三件事来提高表单的可访问性
- 语义 HTML:使用语义元素(
<input>
、<option>
等)而不是<div>
。这允许辅助技术 (AT) 专注于输入元素,并向用户提供适当的上下文信息,使表单更易于导航和理解。 - 标签:包含
<label>
和htmlFor
属性确保每个表单字段都有描述性文本标签。这通过提供上下文来改进 AT 支持,并通过允许用户单击标签来聚焦相应的输入字段,从而增强可用性。 - 焦点轮廓:字段被正确地样式化,以便在获得焦点时显示轮廓。这对于可访问性至关重要,因为它在视觉上指示页面上的活动元素,帮助键盘用户和屏幕阅读器用户理解他们在表单上的位置。您可以通过按
tab
键来验证这一点。
这些实践为使您的表单更易于被许多用户访问奠定了良好的基础。但是,它们没有解决**表单验证**和**错误**。
表单验证
转到 https://127.0.0.1:3000/dashboard/invoices/create,并提交一个空表单。会发生什么?
您会收到一个错误!这是因为您正在向服务器操作发送空的表单值。您可以通过在客户端或服务器上验证表单来防止这种情况。
客户端验证
您可以通过几种方法在客户端验证表单。最简单的方法是依赖浏览器提供的表单验证,方法是将 required
属性添加到表单中的 <input>
和 <select>
元素。例如
<input
id="amount"
name="amount"
type="number"
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"
required
/>
再次提交表单。如果您尝试提交具有空值的表单,浏览器将显示警告。
这种方法通常还可以,因为一些 AT 支持浏览器验证。
客户端验证的替代方案是服务器端验证。让我们在下一节中看看如何在服务器端实现它。现在,如果您添加了 required
属性,请删除它们。
服务器端验证
通过在服务器上验证表单,您可以
- 确保您的数据在发送到数据库之前是预期的格式。
- 降低恶意用户绕过客户端验证的风险。
- 对于什么被认为是*有效*数据,有一个单一的事实来源。
在您的 create-form.tsx
组件中,从 react
导入 useActionState
hook。由于 useActionState
是一个 hook,您需要使用 "use client"
指令将您的表单转换为客户端组件
'use client';
// ...
import { useActionState } from 'react';
在您的表单组件内部,useActionState
hook
- 接受两个参数:
(action, initialState)
。 - 返回两个值:
[state, formAction]
- 表单状态,以及在表单提交时调用的函数。
将您的 createInvoice
操作作为 useActionState
的参数传递,并在您的 <form action={}>
属性中,调用 formAction
。
// ...
import { useActionState } from 'react';
export default function Form({ customers }: { customers: CustomerField[] }) {
const [state, formAction] = useActionState(createInvoice, initialState);
return <form action={formAction}>...</form>;
}
initialState
可以是您定义的任何内容,在本例中,创建一个具有两个空键的对象:message
和 errors
,并从您的 actions.ts
文件中导入 State
类型。State
尚不存在,但我们将在下一步创建它
// ...
import { createInvoice, State } from '@/app/lib/actions';
import { useActionState } from 'react';
export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState: State = { message: null, errors: {} };
const [state, formAction] = useActionState(createInvoice, initialState);
return <form action={formAction}>...</form>;
}
这最初可能看起来令人困惑,但一旦您更新服务器操作,它就会更有意义。让我们现在就做。
在您的 action.ts
文件中,您可以使用 Zod 来验证表单数据。按如下方式更新您的 FormSchema
const FormSchema = z.object({
id: z.string(),
customerId: z.string({
invalid_type_error: 'Please select a customer.',
}),
amount: z.coerce
.number()
.gt(0, { message: 'Please enter an amount greater than $0.' }),
status: z.enum(['pending', 'paid'], {
invalid_type_error: 'Please select an invoice status.',
}),
date: z.string(),
});
customerId
- 如果客户字段为空,Zod 已经抛出错误,因为它期望类型为string
。但是,如果用户未选择客户,让我们添加一条友好的消息。amount
- 由于您正在将金额类型从string
强制转换为number
,如果字符串为空,它将默认为零。让我们告诉 Zod 我们始终希望金额大于 0,使用.gt()
函数。status
- 如果状态字段为空,Zod 已经抛出错误,因为它期望 “pending” 或 “paid”。如果用户未选择状态,让我们也添加一条友好的消息。
接下来,更新您的 createInvoice
操作以接受两个参数 - prevState
和 formData
export type State = {
errors?: {
customerId?: string[];
amount?: string[];
status?: string[];
};
message?: string | null;
};
export async function createInvoice(prevState: State, formData: FormData) {
// ...
}
formData
- 与以前相同。prevState
- 包含从useActionState
hook 传递的状态。在本示例的操作中您不会使用它,但它是一个必需的 prop。
然后,将 Zod 的 parse()
函数更改为 safeParse()
export async function createInvoice(prevState: State, formData: FormData) {
// Validate form fields using Zod
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// ...
}
safeParse()
将返回一个对象,该对象包含 success
或 error
字段。这将有助于更优雅地处理验证,而无需将此逻辑放在 try/catch
块内。
在将信息发送到数据库之前,请检查表单字段是否已通过条件正确验证
export async function createInvoice(prevState: State, formData: FormData) {
// Validate form fields using Zod
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// If form validation fails, return errors early. Otherwise, continue.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
// ...
}
如果 validatedFields
不成功,我们将提前返回该函数,并返回来自 Zod 的错误消息。
提示:
console.log
validatedFields
并提交一个空表单以查看其形状。
最后,由于您正在单独处理表单验证,在您的 try/catch 块之外,您可以为任何数据库错误返回特定消息,您的最终代码应如下所示
export async function createInvoice(prevState: State, formData: FormData) {
// Validate form using Zod
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// If form validation fails, return errors early. Otherwise, continue.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
// Prepare data for insertion into the database
const { customerId, amount, status } = validatedFields.data;
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];
// Insert data into the database
try {
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
} catch (error) {
// If a database error occurs, return a more specific error.
return {
message: 'Database Error: Failed to Create Invoice.',
};
}
// Revalidate the cache for the invoices page and redirect the user.
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
太棒了,现在让我们在您的表单组件中显示错误。回到 create-form.tsx
组件,您可以使用表单 state
访问错误。
添加一个**三元运算符**,用于检查每个特定错误。例如,在客户字段之后,您可以添加
<form action={formAction}>
<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 rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
defaultValue=""
aria-describedby="customer-error"
>
<option value="" disabled>
Select a customer
</option>
{customers.map((name) => (
<option key={name.id} value={name.id}>
{name.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 id="customer-error" aria-live="polite" aria-atomic="true">
{state.errors?.customerId &&
state.errors.customerId.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>
// ...
</div>
</form>
提示: 您可以在组件内部
console.log
state
,并检查是否一切都已正确连接。检查开发工具中的控制台,因为您的表单现在是一个客户端组件。
在上面的代码中,您还添加了以下 aria 标签
aria-describedby="customer-error"
:这在select
元素和错误消息容器之间建立关系。它指示id="customer-error"
的容器描述了select
元素。当用户与select
框交互以通知他们错误时,屏幕阅读器将读取此描述。id="customer-error"
:此id
属性唯一标识了保存select
输入的错误消息的 HTML 元素。这是aria-describedby
建立关系所必需的。aria-live="polite"
:当div
内的错误更新时,屏幕阅读器应礼貌地通知用户。当内容更改时(例如,当用户纠正错误时),屏幕阅读器将宣布这些更改,但仅当用户空闲时,以免打断他们。
练习:添加 aria 标签
使用上面的示例,将错误添加到其余的表单字段。如果缺少任何字段,您还应该在表单底部显示一条消息。您的 UI 应该如下所示

准备就绪后,运行 pnpm lint
以检查您是否正确使用了 aria 标签。
如果您想挑战自己,请运用您在本章中学到的知识,并将表单验证添加到 edit-form.tsx
组件。
您需要
- 将
useActionState
添加到您的edit-form.tsx
组件。 - 编辑
updateInvoice
操作以处理来自 Zod 的验证错误。 - 在您的组件中显示错误,并添加 aria 标签以提高可访问性。
准备就绪后,展开下面的代码片段以查看解决方案
您已完成章节14
太棒了,您已经学习了如何使用 React Form Status 和服务器端验证来提高表单的可访问性。
这有帮助吗?