14
章节14
提高可访问性
在上一章中,我们探讨了如何捕获错误(包括 404 错误)并向用户显示备用内容。但是,我们还需要讨论另一个难题:表单验证。让我们看看如何使用 Server Actions 实现服务器端验证,以及如何使用 React 的 useActionState 钩子来显示表单错误 - 同时考虑到可访问性!
在本章中...
我们将涵盖以下主题:
如何将 eslint-plugin-jsx-a11y 与 Next.js 结合使用以实现可访问性最佳实践。
如何实现服务器端表单验证。
如何使用 React useActionState 钩子来处理表单错误,并将其显示给用户。
什么是可访问性?
可访问性是指设计和实现所有人(包括残障人士)都能使用的 Web 应用程序。这是一个涵盖了许多领域的广泛主题,例如键盘导航、语义 HTML、图像、颜色、视频等。
虽然我们不会在本课程中深入探讨可访问性,但我们会讨论 Next.js 中提供的可访问性功能以及一些使您的应用程序更具可访问性的常见实践。
在 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://:3000/dashboard/invoices/create,然后提交一个空表单。会发生什么?
您收到一个错误!这是因为您正在将空表单值发送到您的 Server Action。您可以通过在客户端或服务器上验证表单来防止这种情况。
客户端验证
您可以通过几种方式在客户端验证表单。最简单的方法是依赖浏览器提供的表单验证,方法是在表单中的 <input> 和 <select> 元素中添加 required 属性。例如
<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 钩子。由于 useActionState 是一个钩子,您需要使用 "use client" 指令将您的表单转换为客户端组件
'use client';
// ...
import { useActionState } from 'react';在您的 Form 组件内部,useActionState 钩子
- 接受两个参数:
(action, initialState)。 - 返回两个值:
[state, formAction]- 表单状态,以及在表单提交时要调用的函数。
将您的 createInvoice action 作为 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 action 以接受两个参数 - 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钩子传递的状态。在此示例中您不会在 action 中使用它,但它是一个必需的属性。
然后,将 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组件中。 - 编辑
updateInvoiceaction 以处理 Zod 的验证错误。 - 在您的组件中显示错误,并添加 aria 标签以提高可访问性。
准备就绪后,展开下面的代码片段以查看解决方案
您已完成本章14
太棒了,您已经学会了如何使用 React 表单状态和服务器端验证来改善表单的可访问性。
这有帮助吗?
