执行契约
- 适用场景
- 字段是否有效依赖服务端事实,例如唯一名称或邀请码,并且需要在最终提交前提供提前反馈。
- 不要使用
- 格式、长度和必填等本地规则应同步验证;不要为了“统一”把所有规则都变成网络请求。
- 前置检查
- 先定义同步前置校验、异步触发时机、debounce、规范化规则、请求失败文案,以及提交端的同规则重新校验。
- 验证结果
- 用假计时器和可控响应测试快速输入、失焦、重复值、网络失败与提交;无效输入不发请求,最终界面只对应当前值。
- 失败模式
- 每次按键都请求通常是 debounce 配置在错误层级;旧值错误覆盖新值时,应检查自定义请求层是否保留了过期响应结果。
- 安全边界
- 提前校验端点可能泄露账号或资源是否存在;对响应做最小化、限速并评估枚举风险,最终提交仍需完整认证、授权与输入校验。
只有服务端事实才需要异步
必填、长度和格式应使用同步 onChange 或 onBlur。名称占用、邀请码有效性等依赖远程事实的规则才进入 onChangeAsync 或 onBlurAsync。同步校验默认先运行,只有通过后才运行异步校验,避免把明显无效输入送到网络。
在验证器层配置 debounce
onChangeAsyncDebounceMs 只延迟对应异步验证器,不会推迟本地值更新。界面继续即时受控,同时避免每次按键都访问校验端点。错误返回值可以是字符串或自定义结构,但同一表单应保持稳定形状。
<form.Field name="title" validators={{
onChange: ({ value }) =>
value.trim().length >= 3 ? undefined : 'Use at least 3 characters',
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) =>
await isIssueTitleTaken(value.trim()) ? 'Title is already in use' : undefined,
}}>
{(field) => <label>Title<input name={field.name} value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)} />
{field.state.meta.isTouched && !field.state.meta.isValid
? <span role="alert">{field.state.meta.errors.join(', ')}</span> : null}
</label>}
</form.Field>提交仍是最终验证点
canSubmit 与 isSubmitting 用于界面反馈和防重复动作,但不构成信任边界。onSubmit 必须调用经过服务端解析、认证和授权的写入接口。唯一性可能在提前校验和提交之间发生变化,因此服务端冲突必须返回并映射为当前表单可理解的错误。
提前校验也有枚举风险
“邮箱已注册”或“资源存在”可能泄露敏感信息。根据业务选择模糊响应、认证后校验、限速或完全取消提前检查。记录网络失败与业务无效的区别,不能把服务不可用误报为字段不合法。