如何设计一个表单校验器

前端页面凡是涉及到用户输入,一般都会都数据校验。广义的说,涉及用户输入部分都可以统称之为表单。之所以需要对用户输入的数据进行校验,个人觉得有以下两个原因:

  1. 系统的稳定性,我们不能保证用户输入是符合我们预期的,通过对输入的数据进行格式、内容校验,避免系统出现问题。例如需要输入年龄只能是正整数。
  2. 用户体验,一般输入时就会给出提示,然后方便用户修正。例如输入的内容过长或者格式不对。
  3. 数据安全或合规,避免用户输入一些不合规数据,例如游戏中各种激情互喷词语。

为了保证数据的一致性,前、后端都会对数据进行校验,一般而言后端的校验会更加全面。前端更偏向于一些数据通用性质的校验,例如数据格式、是否为空、长度等。这里主要介绍如何实现一个前端的表单校验器,当然思路都是通用的。

给表单添加校验

假设我们需要实现一个收集用户姓名、年龄的的表单页面,要求是:

  1. 用户的输入姓名只支持中文,且长度不能超过 10;
  2. 输入的年龄只能整数,且在 20 至 60 之间;
  3. 数据都是必填,且姓名不能全部为空字符串;

首先我们需要完成这样的一个表单页面:

1
2
3
4
5
<form id="form">
<input type="text" name="name" placeholder="姓名" />
<input type="number" name="age" placeholder="年龄" />
<button type="submit">submit</button>
</form>

其次按照要求对用户填写的数据进行校验,最简单的实现方式可能为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const $form = document.querySelector("#form");
$form.addEventListener("submit", (event) => {
event.preventDefault();
const formData = new FormData($form);
const name = formData.get("name");
const age = formData.get("age");
if (!name.trim() || !age) {
return console.error("必填");
}
if (name.length > 10) {
return console.error("姓名长度不能超过10字符");
}
if (!/[\u4e00-\u9fa5]+/g.test(name)) {
return console.error("姓名必须为中文");
}
if (isNaN(Number(age)) || age < 20 || age > 60) {
return console.error("年龄需是20~60间的整数");
}
console.log("Success:", name, age);
});

这里校验不通过时,以console.error模拟异常提示,实际情况应该是对应的输入框展示对应的异常提示,展示的效果如下:

代码很简单易懂,如果是活动页面或一次性页面,这样实现已经足够了,但如果表单可能有变动的可能性,这段代码有比较显著的缺点:

  1. 表单提交函数比较庞大,包含很多条件(if)语句,条件语句多了对阅读容易产生干扰;
  2. 表单提交函数扩展性较差,如果需要新增一个规则,就需要深入函数了解所有逻辑,然后再新增一段校验逻辑;
  3. 代码可复用性较差,如果有另外一个类似表单,需要复制粘贴这段校验代码。

针对这些问题,我们可以通过策略模式来解决

基于策略模式的表单校验器

策略模式是定义一系列可以替换的函数/组件/实现,它们之间可以互相进行替换。例如我们想从上海去北京,可以选择飞机、火车、自驾等方式,不同出行方式可以理解就是不同的策略。过程不一致,但目标都是差不多的。

其实在 JavaScript 中,我们很多时候都在使用策略模式,例如场景的不同状态渲染不同的文字:

1
2
3
4
5
const status2text = {
1: "草稿",
2: "完成",
3: "删除",
};

同理,针对表单校验我们也可以定义对应的校验策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const validateStrategies = {
isNumber(value, message) {
if (isNaN(Number(value))) {
return message;
}
},
isNonEmpty(value, message) {
if (!value) {
return message;
}
},
maxLength(value, message, length) {
if (value.length > length) {
return message;
}
},
isChinese(value, message) {
if (!/[\u4e00-\u9fa5]+/g.test(value)) {
return message;
}
},
max(value, message, maxValue) {
if (value > maxValue) {
return message;
}
},
min(value, message, minValue) {
if (value < minValue) {
return message;
}
},
};

定义好校验策略之后,这些策略是针对具体某些场景的校验,实际应用时还需要一个数据和策略的关联:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const validateRule = {
name: [
{
strategy: "isNonEmpty",
message: "姓名不能为空",
},
{
strategy: "maxLength",
param: 10,
message: "姓名长度不能超过10字符",
},
{
strategy: "isChinese",
message: "姓名必须为中文",
},
],
age: [
{
strategy: "isNonEmpty",
message: "年龄不能为空",
},
{
strategy: "isNumber",
message: "年龄需要为数字类型",
},
{
strategy: "max",
param: 60,
message: "年龄不能超过 60",
},
{
strategy: "min",
param: 20,
message: "年龄不能低于 20",
},
],
};

因为不同校验规可能需要额外的参数的,例如最长长度,或者最大、最小值判断,需要有一个比较的参照值,通过额外的param来传递。接下来改造一下我们的提交函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const $form = document.querySelector("#form");
$form.addEventListener("submit", (event) => {
event.preventDefault();
const formData = new FormData($form);
// 这一段还可以继续抽象做为一个validate函数
for (let [key, value] of formData.entries()) {
const rules = validateRule[key];
for (let rule of rules) {
const { strategy, message, param } = rule;
const error = validateStrategies[strategy](value, message, param);
if (error) {
return console.error(error);
}
}
}
console.log("Success:", formData.get("name"), formData.get("age"));
});

效果和之前的基本一样

通过策略模式重构表单校验器之后,代码变得更加复杂了一些,代码量也更大了些,但代码更好理解了不少, 整体层次为:

校验规则可以多个表单复用,如果表单需要新增字段或者针对已有字段新增校验规则,只需要修改 validateRule 的映射即可,在实际开发过程中可以将 validateRule 做进一步的优化,例如放在后端下发,可以动态配置校验规则,也可以结合 Dom 节点,增加一些属性表示校验规则,这样就不同写单独的配置了。

文章中所涉及的代码可以在copen查看和体验