前端页面凡是涉及到用户输入,一般都会都数据校验。广义的说,涉及用户输入部分都可以统称之为表单。之所以需要对用户输入的数据进行校验,个人觉得有以下两个原因:
- 系统的稳定性,我们不能保证用户输入是符合我们预期的,通过对输入的数据进行格式、内容校验,避免系统出现问题。例如需要输入年龄只能是正整数。
- 用户体验,一般输入时就会给出提示,然后方便用户修正。例如输入的内容过长或者格式不对。
- 数据安全或合规,避免用户输入一些不合规数据,例如游戏中各种激情互喷词语。
为了保证数据的一致性,前、后端都会对数据进行校验,一般而言后端的校验会更加全面。前端更偏向于一些数据通用性质的校验,例如数据格式、是否为空、长度等。这里主要介绍如何实现一个前端的表单校验器,当然思路都是通用的。
给表单添加校验
假设我们需要实现一个收集用户姓名、年龄的的表单页面,要求是:
- 用户的输入姓名只支持中文,且长度不能超过 10;
- 输入的年龄只能整数,且在 20 至 60 之间;
- 数据都是必填,且姓名不能全部为空字符串;
首先我们需要完成这样的一个表单页面:
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
模拟异常提示,实际情况应该是对应的输入框展示对应的异常提示,展示的效果如下:
代码很简单易懂,如果是活动页面或一次性页面,这样实现已经足够了,但如果表单可能有变动的可能性,这段代码有比较显著的缺点:
- 表单提交函数比较庞大,包含很多条件(if)语句,条件语句多了对阅读容易产生干扰;
- 表单提交函数扩展性较差,如果需要新增一个规则,就需要深入函数了解所有逻辑,然后再新增一段校验逻辑;
- 代码可复用性较差,如果有另外一个类似表单,需要复制粘贴这段校验代码。
针对这些问题,我们可以通过策略模式来解决
基于策略模式的表单校验器
策略模式是定义一系列可以替换的函数/组件/实现,它们之间可以互相进行替换。例如我们想从上海去北京,可以选择飞机、火车、自驾等方式,不同出行方式可以理解就是不同的策略。过程不一致,但目标都是差不多的。
其实在 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); 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查看和体验