提升可访问性
在上一章节中,我们探讨了如何捕获错误(包括 404 错误)并向用户展示回退内容。然而,我们还需要讨论另一个关键部分:表单验证。让我们看看如何通过服务端操作 (Server Actions) 实现服务端验证,以及如何使用 React 的 useActionState
钩子展示表单错误——同时兼顾可访问性!
什么是可访问性?
可访问性 (accessibility) 是指设计和实现所有人都能使用的网页应用,包括残障人士。这是一个涵盖多个领域的广泛主题,例如键盘导航、语义化 HTML、图片、颜色、视频等。
虽然本课程不会深入探讨可访问性,但我们会讨论 Next.js 提供的可访问性功能以及一些使应用更具可访问性的常见实践。
如果你想了解更多关于可访问性的内容,我们推荐 web.dev 的 Learn Accessibility 课程。
在 Next.js 中使用 ESLint 可访问性插件
Next.js 在其 ESLint 配置中内置了 eslint-plugin-jsx-a11y
插件,帮助及早发现可访问性问题。例如,该插件会在以下情况发出警告:图片缺少 alt
文本、错误使用 aria-*
和 role
属性等。
如果你想尝试此功能,可以在 package.json
文件中添加 next lint
脚本:
然后在终端运行 pnpm lint
:
这将引导你安装并配置项目的 ESLint。如果现在运行 pnpm lint
,你应该会看到以下输出:
但如果你的图片缺少 alt
文本会怎样呢?让我们试试看!
转到 /app/ui/invoices/table.tsx
并移除图片的 alt
属性。你可以使用编辑器的搜索功能快速找到 <Image>
:
再次运行 pnpm lint
,你应该会看到以下警告:
虽然添加和配置 linter 不是必需步骤,但它有助于在开发过程中发现可访问性问题。
提升表单可访问性
我们已经在表单中做了三件事来提升可访问性:
- 语义化 HTML:使用语义化元素(如
<input>
、<option>
)而非<div>
。这使辅助技术 (AT) 能够聚焦输入元素并向用户提供适当的上下文信息,使表单更易于导航和理解。 - 标签化:包含
<label>
和htmlFor
属性确保每个表单字段都有描述性文本标签。这通过提供上下文增强了 AT 支持,并通过允许用户点击标签聚焦到对应输入字段提升了可用性。 - 焦点轮廓:字段在聚焦时正确显示轮廓样式。这对可访问性至关重要,因为它视觉上指示了页面上的活动元素,帮助键盘和屏幕阅读器用户理解他们在表单中的位置。你可以按
tab
键验证这一点。
这些实践为让表单对更多用户具有可访问性奠定了良好基础。但它们并未解决表单验证和错误处理问题。
表单验证
访问 http://localhost:3000/dashboard/invoices/create 并提交空表单。会发生什么?
你会得到一个错误!这是因为你向服务端操作 (Server Action) 发送了空表单值。你可以通过在客户端或服务端验证表单来防止这种情况。
客户端验证
有几种方法可以在客户端验证表单。最简单的方式是利用浏览器提供的表单验证,即在表单的 <input>
和 <select>
元素上添加 required
属性。例如:
再次提交表单。如果你尝试提交包含空值的表单,浏览器会显示警告。
这种方式通常可行,因为部分 AT 支持浏览器验证。
客户端验证的替代方案是服务端验证。我们将在下一节探讨如何实现它。现在,如果你添加了 required
属性,请先删除它们。
服务端验证
通过在服务端验证表单,您可以:
- 确保数据在发送到数据库前符合预期格式
- 降低恶意用户绕过客户端验证的风险
- 为"有效数据"建立单一可信来源
在您的 create-form.tsx
组件中,从 react
导入 useActionState
钩子。由于 useActionState
是钩子函数,您需要使用 "use client"
指令将表单转为客户端组件:
在表单组件内部,useActionState
钩子:
- 接收两个参数:
(action, initialState)
- 返回两个值:
[state, formAction]
—— 表单状态和表单提交时调用的函数
将 createInvoice
操作作为参数传递给 useActionState
,并在 <form action={}>
属性中调用 formAction
。
initialState
可以是您定义的任何内容,本例中创建一个包含两个空键的对象:message
和 errors
,并从 actions.ts
文件导入 State
类型(State
目前还不存在,我们将在下一步创建):
这看起来可能有些复杂,但当您更新服务端操作后就会更清晰。现在让我们开始更新。
在 action.ts
文件中,可以使用 Zod 验证表单数据。按如下方式更新 FormSchema
:
customerId
- Zod 已会在客户字段为空时抛出错误(因为它期望字符串类型),但我们添加了更友好的提示信息amount
- 由于您将金额类型从字符串强制转换为数字,空字符串会默认为零。我们使用.gt()
函数告诉 Zod 金额必须大于 0status
- Zod 已会在状态字段为空时抛出错误(因为它期望 "pending" 或 "paid"),我们也添加了更友好的提示信息
接下来,更新 createInvoice
操作以接收两个参数 —— prevState
和 formData
:
formData
- 与之前相同prevState
- 包含从useActionState
钩子传递的状态。本例中不会在操作中使用它,但这是必需属性
然后将 Zod 的 parse()
函数改为 safeParse()
:
safeParse()
会返回包含 success
或 error
字段的对象,这样无需将逻辑放在 try/catch
块中就能更优雅地处理验证。
在将信息发送到数据库前,用条件语句检查表单字段是否验证成功:
如果 validatedFields
不成功,我们将提前返回函数并携带 Zod 的错误信息。
提示: 可以 console.log
validatedFields
并提交空表单查看其结构。
最后,由于您在 try/catch 块外单独处理表单验证,可以为任何数据库错误返回特定消息。最终代码应如下所示:
很好,现在让我们在表单组件中显示错误。回到 create-form.tsx
组件,您可以通过表单 state
访问错误。
添加一个三元运算符来检查每个特定错误。例如,在客户字段后可以添加:
提示: 您可以在组件内部 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
组件添加表单验证。
您需要:
- 在
edit-form.tsx
组件中添加useActionState
- 编辑
updateInvoice
操作以处理来自 Zod 的验证错误 - 在组件中显示错误,并添加 ARIA 标签提升可访问性
完成后,展开下方代码片段查看解决方案: