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

当你在互联网上注册账号、填写收货地址,或是提交一份在线问卷时,你大概率都在和表单打交道。表单是我们与网页交互的重要方式,可别小瞧了它。但你知道吗?表单里藏着一个强大却常被忽视的功能 ------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属性值,还有patternmaxlength等属性,都能设置限制条件。其中setCustomValidity这个 DOM 方法最厉害,能自己定义各种验证逻辑。但它有个大问题,只能通过方法调用,没有对应的属性。在 React 这类声明式组件里使用时,特别麻烦。验证初始值时,要写好多重复代码,代码内聚性也差。要是表单输入框多,那看着就更乱了,这也是大家不咋用它的主要原因。

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

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

相关推荐
懒羊羊我小弟12 分钟前
使用 ECharts GL 实现交互式 3D 饼图:技术解析与实践
前端·vue.js·3d·前端框架·echarts
前端小巷子12 分钟前
CSS3 遮罩
前端·css·面试·css3
运维@小兵17 分钟前
vue访问后端接口,实现用户注册
前端·javascript·vue.js
雨汨20 分钟前
web:InfiniteScroll 无限滚动
前端·javascript·vue.js
Samuel-Gyx1 小时前
前端 CSS 样式书写与选择器 基础知识
前端·css
天天打码1 小时前
Rspack:字节跳动自研 Web 构建工具-基于 Rust打造高性能前端工具链
开发语言·前端·javascript·rust·开源
字节高级特工1 小时前
【C++】”如虎添翼“:模板初阶
java·c语言·前端·javascript·c++·学习·算法
db_lnn_20212 小时前
【vue】全局组件及组件模块抽离
前端·javascript·vue.js
Qin_jiangshan3 小时前
vue实现进度条带指针
前端·javascript·vue.js
菜鸟una3 小时前
【layout组件 与 路由镶嵌】vue3 后台管理系统
前端·vue.js·elementui·typescript