表单验证这功能,你咋不用呢?

当你在互联网上注册账号、填写收货地址,或是提交一份在线问卷时,你大概率都在和表单打交道。表单是我们与网页交互的重要方式,可别小瞧了它。但你知道吗?表单里藏着一个强大却常被忽视的功能 ------HTML 表单验证。这个功能就像游戏里的隐藏关卡,明明有着超高的实用价值,却因为开发者的忽视,一直没机会大展身手。今天,咱们就来聊聊,HTML 表单验证为何会被冷落,它又能在哪些地方帮我们打造出更优质的用户体验。

属性、方法和特性

通过添加required属性,禁止输入为空非常容易实现:

html 复制代码
<input required={true} />

除此之外,还有许多其他方式可以为输入添加限制条件。具体来说,有三种方法:

  • 使用特定的type属性值,如"email"(电子邮件)、"number"(数字)或"url"(网址);

  • 使用其他用于创建限制的输入属性,例如"pattern"(正则表达式匹配模式)或"maxlength"(最大长度);

  • 使用输入元素的setCustomValidity DOM 方法。

最后一种方法最为强大,因为它允许创建任意的验证逻辑,以处理复杂的验证场景。你注意到它与前两种方法的区别了吗?前两种是通过属性来定义的,而setCustomValidity是一个方法。

这里有一篇很棒的文章,解释了 DOM 属性和特性之间的区别:jakearchibald.com/2024/attrib...

命令式 API 的细微差别

setCustomValidity API 仅作为一种方法暴露,没有与之对应的属性,这导致了使用体验不佳。我通过一个示例为你说明。

首先,简单介绍一下这个 API 的工作原理:

javascript 复制代码
// 使输入无效
input.setCustomValidity("任意文本提示");

这会使输入变为无效状态,浏览器将显示原因 "任意文本提示"。

javascript 复制代码
// 移除自定义限制,使输入有效
input.setCustomValidity("");

传入空字符串可使输入变为有效(前提是没有应用其他限制条件)。

就是这么简单!现在我们来应用这些知识。

假设我们想要实现一个类似于required属性的功能,这意味着空输入必须阻止表单提交。

html 复制代码
<input
  name="example"
  placeholder="..."
  onChange={(event) => {
    const input = event.currentTarget;
    if (input.value === "") {
      input.setCustomValidity("自定义提示:输入为空");
    } else {
      input.setCustomValidity("");
    }
  }}
/>

看起来我们似乎完成了任务,这段代码应该足以实现该功能。但实际运行一下就会发现问题:

它看似正常工作,但存在一个重要的边缘情况:输入初始状态是有效的。如果你重置组件并按下 "提交" 按钮,表单提交会成功。然而,在我们尚未输入内容时,输入框显然为空,理应是无效状态。但我们的代码仅在输入值发生变化时才进行处理。

如何解决这个问题呢?

我们可以在组件挂载时执行一些代码:

javascript 复制代码
import { useRef, useLayoutEffect } from "react";

function Form() {
  const ref = useRef();
  useLayoutEffect(() => {
    // 如果输入为空,在初始渲染时使输入无效
    const input = ref.current;
    const empty = input.value === "";
    input.setCustomValidity(empty ? "初始提示:输入为空" : "");
  }, []);
  return (
    <form>
      <input
        ref={ref}
        name="example"
        onChange={(event) => {
          const input = event.currentTarget;
          if (input.value === "") {
            input.setCustomValidity("自定义提示:输入为空");
          } else {
            input.setCustomValidity("");
          }
        }}
      />
      <button>提交</button>
    </form>
  );
}

很好!现在一切按预期工作。但代价是什么呢?

样板代码问题

看看我们验证初始值时繁琐的方式:

javascript 复制代码
const ref = useRef();
useLayoutEffect(() => {
  // 如果输入为空,在初始渲染时使输入无效
  const input = ref.current;
  const empty = input.value === "";
  input.setCustomValidity(empty ? "初始提示:输入为空" : "");
}, []);

哎呀!肯定不想每次都写这样的代码。让我们思考一下问题出在哪里。

  • 验证逻辑在onChange事件处理程序和初始渲染阶段重复;

  • 初始验证代码没有和输入框的代码放在一起,这破坏了代码的内聚性。这种结构很脆弱:如果更新验证逻辑,可能会忘记同时更新两个地方的代码;

  • useRef + useLayoutEffect + onChange的组合过于繁琐,尤其是当表单中有大量输入框时。如果只有部分输入框使用customValidity,情况会变得更加混乱。

这就是在声明式组件中使用纯命令式 API 会出现的问题。

与验证属性不同,CustomValidity是一个纯命令式 API。换句话说,没有可以用来设置自定义有效性的输入属性。

实际上,我认为这就是原生表单验证使用率低的主要原因。如果 API 使用起来很麻烦,那么它再强大也无济于事。

缺失的部分

本质上,我们需要这样一个属性:

html 复制代码
<input custom-validity="错误提示信息" />

在声明式框架中,这将允许以非常强大的方式定义输入验证:

javascript 复制代码
function Form() {
  const [value, setValue] = useState();
  const handleChange = (event) => setValue(event.target.value);
  return (
    <form>
      <input
        name="example"
        value={value}
        onChange={handleChange}
        custom-validity={value.length ? "请填写此字段" : ""}
      />
      <button>提交</button>
    </form>
  );
}

很酷吧!至少我是这么认为的。不过你可能会反驳说,这仅仅实现了现有required属性已经具备的功能,强大之处体现在哪呢?

让我来展示一下。但首先,由于目前 HTML 规范中实际上并没有custom-validity属性,我们在用户代码中实现它。

javascript 复制代码
function Input({ customValidity, ...props }) {
  const ref = useRef();
  useLayoutEffect(() => {
    if (customValidity != null) {
      const input = ref.current;
      input.setCustomValidity(customValidity);
    }
  }, [customValidity]);
  return <input ref={ref} {...props} />;
}

对于演示目的来说,这段代码已经足够。

如需用于生产环境的组件,请查看更完整的实现。

强大之处

现在我们来探索这种设计可以解决哪些复杂的实际问题。

在实际应用中,验证往往比本地检查更复杂。想象一下,用户名输入框只有在用户名未被占用时才有效。这需要向服务器发起异步请求,并涉及中间状态:在检查过程中,表单不应被视为有效。让我们看看我们的抽象设计如何处理这种情况。

试试这个示例。它使用required属性防止输入为空,然后依靠customValidity在加载状态和根据响应结果将输入标记为无效。

实现过程

首先,创建一个异步函数来检查用户名是否唯一,通过延迟来模拟服务器请求。

javascript 复制代码
export async function verifyUsername(userValue) {
  // 模拟网络延迟
  await new Promise((r) => setTimeout(r, 3000));
  const value = userValue.trim().toLowerCase();
  if (value === "bad input") {
    throw new Error("输入错误");
  }
  const validationMessage = value === "taken" ? "用户名已被占用" : "";
  return { validationMessage };
}

接下来,创建一个受控表单组件,并在输入值变化时使用react-query来管理服务器请求:

javascript 复制代码
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { verifyUsername } from "./verifyUsername";
import { Input } from "./Input";

function Form() {
  const [value, setValue] = useState("");
  const { data, isLoading, isError } = useQuery({
    queryKey: ["verifyUsername", value],
    queryFn: () => verifyUsername(value),
    enabled: Boolean(value),
  });
  return (
    <form>
      <Input
        name="username"
        required={true}
        value={value}
        onChange={(event) => {
          setValue(event.currentTarget.value);
        }}
      />
      <button>提交</button>
    </form>
  );
}

很好!我们已经完成了基础设置。它包含两个关键部分:

  • 由useQuery管理的验证请求状态;

  • 能够接收customValidity属性的自定义<Input />组件。

让我们把这些部分组合起来:

javascript 复制代码
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { verifyUsername } from "./verifyUsername";
import { Input } from "./Input";

function Form() {
  const [value, setValue] = useState("");
  const { data, isLoading, isError } = useQuery({
    queryKey: ["verifyUsername", value],
    queryFn: () => verifyUsername(value),
    enabled: Boolean(value),
  });
  const validationMessage = data?.validationMessage;
  return (
    <form>
      <Input
        name="username"
        required={true}
        customValidity={
          isLoading
            ? "正在验证用户名..."
            : isError
            ? "无法验证"
            : validationMessage
        }
        value={value}
        onChange={(event) => {
          setValue(event.currentTarget.value);
        }}
      />
      <button>提交</button>
    </form>
  );
}

就这样!我们用一个属性描述了整个异步验证流程,包括加载、错误和成功状态。如果你想回顾,可以再去看看效果。

另一个示例

这个示例会简短一些,但也很有趣,因为它涉及到相关联的输入字段。我们来实现一个需要重复输入密码的表单:

javascript 复制代码
import { useState } from "react";
import { Input } from "./Input";

function ConfirmPasswordForm() {
  const [password, setPassword] = useState("");
  const [confirmedPass, setConfirmedPass] = useState("");

  const matches = confirmedPass === password;
  return (
    <form>
      <Input
        type="password"
        name="password"
        required={true}
        value={password}
        onChange={(event) => {
          setPassword(event.currentTarget.value);
        }}
      />
      <Input
        type="password"
        name="confirmedPassword"
        required={true}
        value={confirmedPass}
        customValidity={matches ? "" : "两次输入的密码必须一致"}
        onChange={(event) => {
          setConfirmedPass(event.currentTarget.value);
        }}
      />
      <button>提交</button>
    </form>
  );
}

总结

家人们,咱这篇文章讲的是 HTML 表单验证,功能超强大,却没被充分利用。

HTML 表单验证方法有好几种,加required属性,输入框就不能为空,用特定type属性值,还有pattern、maxlength等属性,都能设置限制条件。其中setCustomValidity这个 DOM 方法最厉害,能自己定义各种验证逻辑。但它有个大问题,只能通过方法调用,没有对应的属性。在 React 这类声明式组件里使用时,特别麻烦。验证初始值时,要写好多重复代码,代码内聚性也差。要是表单输入框多,那看着就更乱了,这也是大家不咋用它的主要原因。

我就琢磨着,要是有个custom-validity属性就好了,这样在声明式框架里验证输入就简单多了。虽然 HTML 规范里目前还没有这个属性,不过我自己在代码里模拟实现了类似效果。

文章还举了两个例子,来展示customValidity有多牛。一个是验证用户名,检查用户名是否被占用得向服务器发请求,这个过程结合useQuery,用customValidity就能把加载、出错和成功这些状态的验证提示处理得很到位。另一个是确认密码的表单,通过对比两次输入的密码,用customValidity就能轻松保证两次输入一致。总之,HTML 表单验证潜力巨大,就等大家好好去挖掘利用啦!

相关推荐
RobinDevNotes1 小时前
从 Electron 到 GPUI,9 个跨平台桌面方案该怎么选
前端·javascript·rust·electron·openresty·前端开发工具
jay神2 小时前
基于 YOLOv8 的轴承缺陷检测(Web 版)
前端·人工智能·yolo
jay神2 小时前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat2 小时前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss3 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan3 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真4 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment4 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan4 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么4 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模