用最简单的方式实现前端校验,也许你并不需要任何框架

前端校验只是对用户的输入做一个简单的检测和提示,它并不是必须的选项。因为前端传入后端的数据一般都是不可靠的,它可以很容易的用一些手段跳过,这是由前端的特性造成的。但是做为一个负责任的工程师,前端传入给你的任何数据都必须经过检验。而前端做到的只是一些基本的校验,而真正的校验需要后端更严格的检测。但是为了用户体验,前端校验又是不得不做的一个基础功能,他针对用户输入的数据做一个提示与反馈,来告诉用户当前输入的数据是否有效。

Table of Contents

1. 什么是前端校验?

你去任何一个需要注册的网站,当你进入登录注册页面时,网站要求你输入一些信息进行登录或注册,当你输入正确时就是登录或注册成功然后跳到个人页面,当你输入不正确时,例如输入格式不对或者密码输入错误时,网页就会弹出红色提示框说你的输入不正确给予你提示,你可以根据提示,重新输入正确的信息,直到成功为止,然后用户的信息会被保存到数据库上。

你可以不做前端校验,只放一个输入框在页面上,让用户输入你所需要的信息。但是这样做对用户,对后端都会有很大的困扰。

  • 在用户这边是:
    1. 我已经输入确认完成了为什么还是停在这个页面,哪里出错了?
    2. 用户无法得到及时的反馈很容易消耗耐心。
  • 在后端这边是:
    1. 一个简单的前端校验可以大大提升用户的输入体验,何乐而不为呢。
    2. 计算机做任何动作都需要消耗资源,而前端校验可以过滤掉一些不必要的动作,例如邮箱格式错误,手机号格式错误等。
    3. 软件开发中外部输入的是一种非常不可靠的数据,它有可能传入任何数据,而我们想要保护我们的软件正确运行,就必须对外部传入的数据进行校验,而前端校验也是其中的一环。

1.1. 如何实现前端校验

  • 简单的用HTML内置方法

    如果简单直接的用HTML标签中的属性就能满足你的需求,那就JavaScript都不需要了,直接HTML,CSS就能实现了。只是缺少了一定的定制性了。

  • 用JavaScript

    用JavaScript实现就可以完全定制你的需求了,还有JavaScript的海量框架。

2. 一个简单的用做开始的范例

前端开发最原生的就是三个文件类型:HTML,CSS和JavaScript。只是发展到后面为了提升开发效率,获得更好的开发体验,衍生出了很多框架。例如:React, VUE, TypeScript, jQuery, SASS等。而我们想要最简单的实现前端校验,就用最原生的前端。

  • 我们的HTML文件:

    form 标签中加入一个输入框和一个按钮,这时候还没有加入任何校验。

    <form>
      <label for="choose">Would you prefer a banana or cherry?</label>
      <input id="choose" name="i-like" />
      <button>Submit</button>
    </form>
    
  • 我们的css文件:

    我们添加了无效的和有效的输入框样式,即加了不同的边框颜色用以区分它们。

    input:invalid {
      border: 2px dashed red;
    }
    input:valid {
      border: 2px solid black;
    }
    

3. 只用HTML和CSS实现前端校验

HTML的标签本身包含了很多属性,而 <input> 标签也包含了一些校验输入的属性。其中包括:

  • required :即输入框不能为空,这个输入框是必须输入内容的,例如用户名、密码等是必须要输入的。
  • pattern :用正则进行校验,即你输入的内容要符合正则表达式。
  • minlengthmaxlength :即你可输入的字符串最短长度和最长长度。
  • minmax : 你可输入的最小数字和最大数字。
  • type :输入框的类型,指定输入框的类型,即这个是数字输入框还是邮箱输入框还是只是做为一个按钮,数字输入框就只能输入数字,邮箱输入框就只能输入邮箱等。

而在我们使用这些属性之后,输入框就具备了一定的校验功能了。当输入的信息満足这些要求,就会提交信息到后台处理,否则就无法提交信息。然后在一个简单的用做开始的范例的基础之上,我们就用这些属性编写我们的代码。

3.1. 当我们使用 require 属性

这时我们是要求必须输入内容,只要有内容输入就算満足了我们的校验要求。当输入框为空时不満足要求,输入框边框为红色,当你输入任何内容,边框就变为了黑色(你可以在下面的输入框中试试)。当你不満足要求是直接按 Submit 按钮就会提示你必须输入内容。

我们的HTML代码:

<form>
  <label for="choose">Would you prefer a banana or cherry? (required)</label>
  <!-- 直接在input中加上required就行了 -->
  <input id="choose" name="i-like" required />
  <button>Submit</button>
</form>

我们的CSS的代码:

input:invalid {
  border: 2px dashed red;
}
input:valid {
  border: 2px solid black;
}

3.2. 用正则表过式规范输入

现在我们要用 pattern 属性来校验输入,在 pattern 中用正则表达式作为它的值。前端正则包括这些。这里我们只会用一些简单的表达式介绍这个属性。

HTML代码是:

<form>
  <label for="choose">Would you prefer a banana or a cherry?</label>
  <input id="choose" name="i-like" required pattern="[Bb]anana|[Cc]herry" />
  <button>Submit</button>
</form>

在这个代码里我们在 <input> 中加入了 pattern="[Bb]anana|[Cc]herry" ,这里就是你只能输入:banana或cherry,首字母大小写都可以。CSS代码跟上面一样不变。(在下面试试吧)

可以使用正则大大扩展了 <input> 校验的可用性,因为你可以使用正则表达式的强大功能去实现一些复杂的校验,例如: /^([A-Za-z0-9]|[A-Za-z0-9][\w\-\/\\ ]*[A-Za-z0-9])$/ 就是你只能输入英文字符,且字符串前后不能是空格。

3.3.minlengthmaxlength 规范字符串的长度

在前端你可以用 minlengthmaxlength 在输入框中直接限制用户可输入内容的长度,例如限制字符为20, 即 maxlength="20" , 当用户输入20个字符之后,用户再输入则输入框不会有任何反应,然后会告知用户你输入内容已超出最大长度限制,一般很多网站的用户名都有这个限制,为了防止用户名占用太多空间,这些网站都会限制用户输入过长的用户名。

在HTML中的应用,CSS保持不变(在下面试试吧),这里为了方便展示我们限制字符最短为6最长也为6:

<form>
  <label for="choose">Would you prefer a banana or a cherry?</label>
  <input
    type="text"
    id="choose"
    name="i-like"
    required
    minlength="6"
    maxlength="6" />
  <button>Submit</button>
</form>

3.4. 确定输入框的类型

输入框 <input> 提供了一个类型的选项 type , type 包括哪些在这里。这里我们展示 type="number" 即数字输入框,且再用 minmax 限定它的最小值和最大值,如果输入的数字超出范围则边框变红色。

HTML代码如下,CSS不变:

<form>
  <label for="number">How many would you like?</label>
  <input type="number" id="number" name="amount" value="1" min="1" max="10" />
  <button>Submit</button>
</form>

当然这里展示的只是 <input> 标签的部份属性,关于更多<input>标签的属性,例如更多的类型 type ,可以到这里查看。

3.5. 总结下HTML+CSS的方法

对于代码我们秉承着能简单就不要复杂,原生能实现就不要用框架的原则。如果你不需要复杂的校验,上面的代码能満足你的需求就尽量用简单的方法实现它,毕竟大型软件的本质就是管理复杂,而其中能简单的地方就简单化。而如果你已经使用了一套框架的流程,上面的方法无法満足,或许你可以试试以下使用JavaScript的实现。

4. 用JavaScript实现前端校验

作为前端语言,JavaScript给了我们更多的选择。不管是更深的定制化还是使用框架节省时间,都大大的提升了效率和可能性。首先在JavaScript中我们可以使HTML标签内置的API了,所以在剩下章节中我会先使用该API在原有基础上进行一些定制。或者你并不喜欢使用这些内置的方法,那么我就会用JavaScript写出我们自己需要的校验代码。然后关于框架我会展示使用ReactJS框架如何进行校验并配合RamdaJS1框架进行函数式编程。最后我会加入 Promise 2进行异步式校验,例如验证账号是否已被注册就需要异步验证需要后端处理后返回结果给我们,然后我们再提醒用户账号名已存在等。

4.1. 在JavaScript中调用 <input> 的API

每个HTML标签都有自己的API,在JS(后面就用于指代JavaScript)中用DOM来指这些标签,例如我们使用的输入框 <input> 在JS中是HTMLInputElement(意思就是html中的input元素)。什么是DOM呢?简单的理解就是JS中的全局变量 document ,我们可以使用这个全局变量来操作HTML中的所有元素。这里只展示操用 <input> 元素,然后代码我们也要添加JS代码了。

4.1.1. 定制报错提示框中的内容

就是当你的输入不符合规则时会提示哪里出错了。这里我们使用邮箱输入框,当你输入不是邮箱格式就会报错,并且提交后也会弹出错误提示。

HTML代码如下,我在 form 中添加了一个 id 属性方便我们在JS中的操作:

<form id="input-api-mail-form">
  <label for="mail">
    I would like you to provide me with an email address:
  </label>
  <input type="email" name="mail" />
  <button>Submit</button>
</form>

CSS代码我们保持不变:

input:invalid {
  border: 2px dashed red;
}
input:valid {
  border: 2px solid black;
}

JS代码如下,这里原本会显示的错误信息是 Please enter an email address. ,这里被我们改成了 I am expecting an email address! ,所以当你输入错误然后提交之后就会弹出这个信息:

// 通过id获取我们的form元素
const email = document.getElementById("input-api-mail-form");

email.addEventListener("submit", (event) => {
  // 通过validity.typeMismatch返回邮箱格式是否正确
  if (email.validity.typeMismatch) {
    // 返回true则说明格式不对,用setCustomValidity设定提示信息
    email.setCustomValidity("I am expecting an email address!");
  } else {
    email.setCustomValidity("");
  }
});

在下面试试吧:

4.1.2. 更多的校验定制

现在我们用一个更全面的例子来定制我们的校验,展示了HTML的API更强大的功能。我们会在用户输入时进行校验,在提交时也会校验,我们会取消默认的提示框,定制我们自己的错误提示框。

HTML代码如下,在 formnovalidate 是取消默认提示框,我们自己加提示框,然后 <input> 用于输入邮箱,必须输入值且至少8个字符,我们的提示框是在 <div class="error"> 这个标签下:

<form novalidate class="more-api-form">
  <label for="mail">
    Please enter an email address:
  </label>
  <p>
    <input type="email" id="more-api-mail" name="mail" required minlength="8" />
    <span class="error"></span>
  </p>
  <button>Submit</button>
</form>

CSS代码,添加提示框样式,其他不变:

p {
    width: 200px;
}
input:invalid {
  border: 2px dashed red;
}
input:valid {
  border: 2px solid black;
}
/* 添加错误提示框样式 */
.error {
    color: hsl(0, 0%, 100%);
    border-radius: 0 0 5px 5px;
    background-color: hsl(348, 100%, 61%);
    padding: 0;
    display: block;
}
.error.active {
    padding: 0.25em;
}

JS代码:

// 获取form表单
const form = document.querySelector(".more-api-form");
// 获取邮箱输入框
const email = document.getElementById("more-api-mail");
// 获取错误提示框元素
const emailError = document.querySelector("#more-api-mail + span.error");

email.addEventListener("input", (event) => {
  // 用户的每次输入都会检查是否合规
  if (email.validity.valid) {
    // 合规就不显示内容
    emailError.textContent = "";
    emailError.className = "error";
  } else {
    // 不合规就显示错误信息
    showError();
  }
});

form.addEventListener("submit", (event) => {
  // 合规就提交
  if (!email.validity.valid) {
    // 不合规就显示错误信息且阻止提交
    showError();
    event.preventDefault();
  }
});

function showError() {
  // 些函数用来检查输入内容是否合规
  if (email.validity.valueMissing) {
    // 检查内容不允许为空,为空就提示如下:
    emailError.textContent = "You need to enter an email address.";
  } else if (email.validity.typeMismatch) {
    // 检查邮箱格式是否正确,不正确就显示如下信息:
    emailError.textContent = "Entered value needs to be an email address.";
  } else if (email.validity.tooShort) {
    // 检杳字符是否太短,太短就显示如下信息:
    emailError.textContent = `Email should be at least ${email.minLength} characters; you entered ${email.value.length}.`;
  }

  // 添加错误信息样式
  emailError.className = "error active";
}

4.2. 不使用HTML的API,只使用JS

你不喜欢用HTML的API,那我们只用JS就可以了。前面的HTML,CSS代码保持不变,我们只要在JS代码中把涉及HTML API的部份更改为“手动”校验。

HTML代码,把HTML校验的属性移除:

<form novalidate class="more-api-form">
  <label for="mail">
    Please enter an email address:
  </label>
  <p>
    <input type="text" id="more-api-mail" name="mail" />
    <span class="error"></span>
  </p>
  <button>Submit</button>
</form>

CSS代码,因为不用API了所以我们css class类型选择器用于样式区分:

input.invalid {
  border: 2px dashed red;
}
input.valid {
  border: 2px solid black;
}

JS代码:

// 获取form表单
const form = document.querySelector(".more-api-form");
// 获取邮箱输入框
const email = document.getElementById("more-api-mail");
// 获取错误提示框元素
const error = document.querySelector("#more-api-mail + span.error");
// 用于校验的正则表达式,跟上面一样这个表达式也是校验邮箱的
const emailRegExp =
      /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;

email.addEventListener("input", () => {
  // 跟据用户输入自动校验
  const isValid = email.value.length === 0 || emailRegExp.test(email.value);
  if (isValid) {
    // 符合规范,就添加class到元素中
    email.className = "valid";
    error.textContent = "";
    error.className = "error";
  } else {
    // 不符合规范
    email.className = "invalid";
  }
});

// 这里我们要手动添加校验消息
form.addEventListener("submit", (event) => {
  event.preventDefault();

  const isValid = email.value.length === 0 || emailRegExp.test(email.value);
  if (!isValid) {
    // 不合规就显示错误信息且阻止提交
    email.className = "invalid";
    error.textContent = "I expect an email, darling!";
    error.className = "error active";
  } else {
    // 合规就提交
    email.className = "valid";
    error.textContent = "";
    error.className = "error";
  }
});

4.3. 用函数式编程优化代码并扩展更多可能

到这里基础功能讲完了,后面就要做一些优化和扩展了,方便我们对于更复杂需求和更多场景下的使用。首先我们对上面的代码做一些优化,使用函数式编程,好处就是对于复杂场景也能用简单易懂的代码表达清楚,关于函数式编程可以看下我的这篇文章函数编程在JavaScript中的简单应用。只更改JS代码,其他代码保持不变。

JS代码,在这段代码中我们会加入高阶函数 pipe 3用于函数组合,就像拼积木一样,把函数一块一块的拼在一起:

// 获取form表单
const form = document.querySelector(".more-api-form");
// 获取邮箱输入框
const email = document.getElementById("more-api-mail");
// 获取错误提示框元素
const error = document.querySelector("#more-api-mail + span.error");
// 用于校验的正则表达式,跟上面一样这个表达式也是校验邮箱的
const emailRegExp =
      /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
// 函数组合
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

// 把所有所需要的校验都放这个函数里
function isValid(value) {
  const toObj = (v) => {v, valid:false, err:""};

  const notEmpty = (obj) => {
    // 不能为空
    if(obj.v.length === 0) {
      return {...obj, valid:false, err:"字符不能为空!"}
    };
    return {...obj, valid:true};
  }
  const tooShort = (obj) => {
    // 不能太短
    if(obj.v.length >= 8) return {...obj, valid:true};
    return {...obj, valid:false, err:"字符长度不能太短!"};
  }
  const isFormat = (obj) => {
    // 邮箱格式要正确
    if(emailRegExp.test(obj.v)) return {...obj, valid:true};
    return {...obj, valid:false, err:"邮箱格式不符!"};
  }

  // 我们可以在这里添加更多校验条件,例如字符不能太长等

  return  pipe(toObj, notEmpty, tooShort, isFormat)(value)
}

email.addEventListener("input", () => {
  // 跟据用户输入自动校验
  const obj = isValid(email.value)
  if (obj.valid) {
    // 符合规范,就添加class到元素中
    email.className = "valid";
    error.textContent = "";
    error.className = "error";
  } else {
    // 不符合规范
    email.className = "invalid";
  }
});

// 这里我们要手动添加校验消息
form.addEventListener("submit", (event) => {
  event.preventDefault();

  const obj = isValid(email.value)
  if (!obj.valid) {
    // 不合规就显示错误信息且阻止提交
    email.className = "invalid";
    error.textContent = obj.err
    error.className = "error active";
  } else {
    // 合规就提交
    email.className = "valid";
    error.textContent = "";
    error.className = "error";
  }
});

4.4. 用reactJS和RamdaJS实现前端校验

后面因为要用到框架就没法展示,参考下代码就行。我们选择ReactJS UI4框架作展示算是当下最流行的UI框架吧。函数式编程我们用RamdaJS框架,用框架的好处是我们不用自己测试了,我们直接用框架提供给我们的函数就可以了。这里的用RamdaJS写的代码不懂也没关系,反正它实现的功能和上面的例子是一样的,如果对RamdaJS编程感兴趣可以看这里

JS代码,样式CSS代码保持不变,HTML和JS代码通过ReactJS框架合并到下面中:

import { useState } from "react"
import * as R from "ramda"

export default function Validation() {
  const [value,setValue] = useState("")
  const [err,setErr] = useState("")

  const handleChange = (event) => {
    setValue(event.target.value)
    const obj = isValid(event.target.value)
    if(!obj.valid) {
      setErr(obj.err)
    }
  }

  const handleSubmit = (event) => {
    event.preventDefault();
    const obj = isValid(value)
    if(obj.valid) {
      setValue("")
      // 提交数据
    } else {
      setErr(obj.err)
    }
  }

  // 把所有所需要的校验都放这个函数里
  function isValid(value) {
    const toObj = R.applySpec({v:R.identity, valid:false, err:""});

    const notEmpty = R.ifElse(
      R.prop("v"),
      R.mergetLeft({valid:true}),
      R.mergeLeft({valid:false, err:"字符不能为空!"})
    )

    const tooShort = R.ifElse(
      R.pipe(R.prop("v"), R.gt(R.__, 8)),
      R.mergeLeft({valid:true}),
      R.mergeLeft({valid:false, err:"字符长度不能太短!"});
    )

    const isFormat = R.ifElse({
      R.pipe(R.prop("v"), R.test(/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/)),
      R.mergeLeft({valid:true}),
      R.mergeLeft({valid:false, err:"邮箱格式不符!"})
    })

    return  R.pipe(toObj, notEmpty, tooShort, isFormat)(value)
  }

  return (
      <form onSubmit={handleSubmit}>
        <label for="mail">
          Please enter an email address:
        </label>
        <p>
          <input type="text" name="mail" value={value} onChange={handleChange} />
          <span className={`error ${!err&&"active"}`}>{err}</span>
        </p>
        <button>Submit</button>
      </form>
  )
}

4.5. 添加异步校验

异步校验一般是需要后端的校验,后端根据前端传回的数据,与数据库中的数据进行比对,看数据是否合规。一般包括注册时用户名是否已经存在,输入的用户名和密码是否正确等。

JS代码如下,其他代码不变,因为异步校验比较耗资源,这里我们取消了根据输入同步校验:

import { useState } from "react"
import * as R from "ramda"

export default function Validation() {
  const [value,setValue] = useState("")
  const [err,setErr] = useState("")

  const handleChange = (event) => {
    setValue(event.target.value)
    // 这里取消了根据输入同步校验
  }

  const handleSubmit = (event) => {
    event.preventDefault();
    isValid(value).then(({valid, err}) => {
      if(valid) {
        setValue("")
      } else {
        setErr(err)
      }
    })
  }

  function isValid(value) {
    const toObj = R.applySpec({v:R.identity, valid:false, err:""});

    const notEmpty = R.ifElse(
      R.prop("v"),
      R.mergetLeft({valid:true}),
      R.mergeLeft({valid:false, err:"字符不能为空!"})
    )

    const tooShort = R.ifElse(
      R.pipe(R.prop("v"), R.gt(R.__, 8)),
      R.mergeLeft({valid:true}),
      R.mergeLeft({valid:false, err:"字符长度不能太短!"});
    )

    const isFormat = R.ifElse({
      R.pipe(R.prop("v"), R.test(/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/)),
      R.mergeLeft({valid:true}),
      R.mergeLeft({valid:false, err:"邮箱格式不符!"})
    })

    const isExist = (obj) => {
      const exist = "banana";
      return new Promise((resolve, reject) => setTimeout(() => {
        if(obj.v === exist) {
          resolve({...obj, valid:false, err:"字符串已存在!"})
        }
        resolve({...obj, valid:true})
      }, 300))
    }

    return  R.pipeWith(
      (f, res) => R.andThen(f, Promise.resolve(res)),
      [toObj, notEmpty, tooShort, isFormat, isExist]
    )(value)
  }

  return (
      <form onSubmit={handleSubmit}>
        <label for="mail">
          Please enter an email address:
        </label>
        <p>
          <input type="text" name="mail" value={value} onChange={handleChange} />
          <span className={`error ${!err&&"active"}`}>{err}</span>
        </p>
        <button>Submit</button>
      </form>
  )
}

5. 总结

前端校验说到底是为了用户的体验服务的,你必须准确的告知用户为什么错的错误的信息,哪里出现了错误,保证用户输入体验的一致性,特别对于有大量需要校验的场景。

6. 写在后面

Footnotes:

Author: JaneGwaww

Created: 2023-04-11 Tue 20:33