【Web 表单】与用户数据打交道-2(mdn笔记)

75be4c7f0a61b86daf6cbe11f46db79e.png

8. UI 伪类

8.1 都有什么样的伪类?

我们可以使用的(截至 CSS 2.1)与表单相关的原始伪类是:

  • :hover:只在鼠标指针悬停在一个元素上时选择该元素。

  • :focus:只在元素被聚焦时选择该元素(也就是说,通过键盘上的 tab 键选中该元素)。

  • :active:只在元素被激活时选择该元素(也就是说,通过点击或键盘上的 Return / Enter 键选中该元素)。

CSS Selector Level 3 和 CSS Basic UI Level 3 增加了更多与 HTML 表单有关的伪类.简单地说,我们要看的主要内容是:

  • :required 和 :optional:针对必需的或可选的表单控件。

  • :valid 和 :invalid、:in-range 和 :out-of-range:针对表单控件,根据对其设置的表单验证约束,或范围内/范围外,是有效/无效的

  • :enabled 和 :disabled、:read-only 和 :read-write:针对启用或禁用的表单控件(例如,设置了 disabled HTML 属性),以及读写或只读的表单控件(例如,设置了 readonly HTML 属性)。

  • :checked、:indeterminate 和 :default:分别针对被选中的复选框和单选按钮,处于不确定的状态(既不被选中也不被选中),以及页面加载时默认选择的选项(例如,一个设置了 checked 属性的 <input type="checkbox">,或者一个设置了 selected 属性的 <option> 元素)。

备注:这里讨论的许多伪类都是关于根据表单控件的验证状态(它们的数据是否有效?)你会在我们的下一篇文章客户端表单验证中学习到更多关于设置和控制验证约束的知识,但现在我们将保持关于表单验证的简单内容,以便不会混淆。

8.2 根据必需与否为输入控件添加样式

一个表单输入是必需的(在提交表单之前必须填写)还是可选的
<input>、<select> 和 <textarea> 元素都有一个 required 属性可用,设置后意味着你必须在表单成功提交前填入该控件。比如说:

2e5e578a7ed57048d6b4d1d69a1f0327.png

<form>
  <fieldset> <!-- 定义字段集 -->
    <legend>Feedback form</legend> <!-- 定义字段集标题 -->
    <div>
      <label for="fname">First name: </label> <!-- 定义标签 -->
      <input id="fname" name="fname" type="text" required /> <!-- 定义文本输入框,要求必填 -->
    </div>
    <div>
      <label for="lname">Last name: </label> <!-- 定义标签 -->
      <input id="lname" name="lname" type="text" required /> <!-- 定义文本输入框,要求必填 -->
    </div>
    <div>
      <label for="email">
        Email address (include if you want a response):
      </label> <!-- 定义标签 -->
      <input id="email" name="email" type="email" /> <!-- 定义电子邮件输入框 -->
    </div>
    <div><button>Submit</button></div> <!-- 定义提交按钮 -->
  </fieldset>
</form>

在这里,first name 和 last name 是必需的,但电子邮件地址是可选的。

可以使用 :required 和 :optional 伪类来匹配这两种状态。必需的表单控件会有一个黑色的边框,可选的表单控件会有一个银色的边框.

input:required {
  border: 1px solid black;
}

input:optional {
  border: 1px solid silver;
}

在不填写表单的情况下提交,观察浏览器默认给你的客户端验证错误信息
网络上关于必填状态的标准惯例是一个星号(*),或者将“必需”这个词与相关的控件联系起来。

备注:如果一组同名的单选按钮中的一个单选按钮具有 required 属性,那么所有的单选按钮都将无效,直到有一个被选中,但只有分配了该属性的单选按钮才会真正匹配 :required。

8.3 使用伪类生成内容

使用 ::before 和 ::after 伪元素以及 content 属性来使一大块内容出现在受影响元素的前面或后面。
这块内容没有被添加到 DOM 中,所以对屏幕阅读器来说是看不见的;它是文档样式的一部分
当你想给一个元素添加一个视觉指示器,比如一个标签或图标,但又不想让它被辅助技术发现时,这就非常有用。

例如,在我们的 自定义单选按钮示例中,我们使用生成的内容来处理单选按钮被选中时内圈的位置和动画:

79b2b7e96865bc8c609d8f89266bca43.png

<fieldset>
      <legend>Choose your favourite fruit</legend>

      <p>
        <label>
          <input type="radio" name="fruit" value="cherry">
          Cherry
        </label>
      </p>
      <p>
        <label>
          <input type="radio" name="fruit" value="banana">
          Banana
        </label>
      </p>
      <p>
        <label>
          <input type="radio" name="fruit" value="strawberry">
          Strawberry
        </label>
      </p>
    </fieldset>
input[type="radio"] {
  appearance: none; /* 移除默认外观 */
}

input[type="radio"] {
  width: 20px; /* 设置宽度为20px */
  height: 20px; /* 设置高度为20px */
  border-radius: 10px; /* 设置圆角半径为10px */
  border: 2px solid gray; /* 设置边框为2px实线灰色 */
  /* 调整单选框在文本基线上的位置 */
  vertical-align: -2px;
  outline: none; /* 移除轮廓线 */
}

input[type="radio"]::before {
  display: block; /* 设置display属性为block */
  content: " "; /* 设置内容为空格 */
  width: 10px; /* 设置宽度为10px */
  height: 10px; /* 设置高度为10px */
  border-radius: 6px; /* 设置圆角半径为6px */
  background-color: red; /* 设置背景颜色为红色 */
  font-size: 1.2em; /* 设置字体大小为1.2em */
  transform: translate(3px, 3px) scale(0); /* 设置变换效果,平移3px并缩放为0 */
  transform-origin: center; /* 设置变换原点为中心 */
  transition: all 0.3s ease-in; /* 设置过渡效果,所有属性在0.3秒内以ease-in方式过渡 */
}

input[type="radio"]:checked::before {
  transform: translate(3px, 3px) scale(1); /* 当单选框被选中时,设置变换效果,平移3px并缩放为1 */
  transition: all 0.3s cubic-bezier(0.25, 0.25, 0.56, 2); /* 设置过渡效果,所有属性在0.3秒内以cubic-bezier方式过渡 */
}

回到我们之前的必填/可选的例子,这次我们不会改变输入本身的外观.使用生成的内容来添加一个指示标签.

e7673aeb49272f194a1108d7edd3563a.png

<form>
  <fieldset> <!-- 定义字段集 -->
    <legend>Feedback form</legend> <!-- 定义字段集标题 -->
    <p>Required fields are labelled with "required".</p> <!-- 添加段落说明 -->
    <div>
      <label for="fname">First name: </label> <!-- 定义标签 -->
      <input id="fname" name="fname" type="text" required> <!-- 定义文本输入框,要求必填 -->
      <span></span>
    </div>
    <div>
      <label for="lname">Last name: </label> <!-- 定义标签 -->
      <input id="lname" name="lname" type="text" required> <!-- 定义文本输入框,要求必填 -->
      <span></span>
    </div>
    <div>
      <label for="email">Email address (include if you want a response): </label> <!-- 定义标签 -->
      <input id="email" name="email" type="email"> <!-- 定义电子邮件输入框 -->
      <span></span>
    </div>
    <div><button>Submit</button></div> <!-- 定义提交按钮 -->
  </fieldset>
</form>
body {
  font-family: 'Josefin Sans', sans-serif; /* 设置字体为'Josefin Sans' */
  margin: 20px auto; /* 设置顶部和底部边距为20px,并使body水平居中 */
  max-width: 460px; /* 设置body的最大宽度为460px */
}

fieldset {
  padding: 10px 30px 0; /* 设置字段集的内边距 */
}

legend {
  color: white; /* 设置字段集标题的颜色为白色 */
  background: black; /* 设置字段集标题的背景颜色为黑色 */
  padding: 5px 10px; /* 设置字段集标题的内边距 */
}
/*由于 input 和 label 都设置了 width: 100%,span 会沉到输入框下一行中。为了修复这一点,我们令父 <div> 为弹性容器,同时令它如果内容变得太长,就把它的内容换行:这样做的效果是,标签和输入是分开的,因为它们都是 width: 100%,但 <span> 的宽度是 0,所以它可以和 input 位于同一行。*/
fieldset > div {
  margin-bottom: 20px; /* 设置字段集内直接子div元素的底部边距为20px */
  display: flex; /* 设置display属性为flex */
  flex-flow: row wrap; /* 设置flex容器为行方向并换行 */
}

button, label, input {
  display: block; /* 设置display属性为block */
  font-family: inherit; /* 继承父元素的字体 */
  font-size: 100%; /* 设置字体大小为100% */
  padding: 0; /* 设置内边距为0 */
  margin: 0; /* 设置外边距为0 */
  box-sizing: border-box; /* 设置盒模型为border-box */
  width: 100%; /* 设置宽度为100% */
  padding: 5px; /* 设置内边距为5px */
  height: 30px; /* 设置高度为30px */
}
input {
  box-shadow: inset 1px 1px 3px #ccc; /* 添加内阴影效果 */
  border-radius: 5px; /* 添加圆角效果 */
}

input:hover, input:focus {
  background-color: #eee; /* 当鼠标悬停或输入框获得焦点时,设置背景颜色为#eee */
}

input + span {
  position: relative; /* 设置相对定位 */
}

input:required + span::after {
  font-size: 0.7rem; /* 设置字体大小为0.7rem */
  position: absolute; /* 设置绝对定位 */
  content: "required"; /* 设置内容为"required" */
  color: white; /* 设置颜色为白色 */
  background-color: black; /* 设置背景颜色为黑色 */
  padding: 5px 10px; /* 设置内边距为5px和10px */
  top: -26px; /* 设置顶部边距为-26px */
  left: -70px; /* 设置左边距为-70px */
}

button {
  width: 60%; /* 设置宽度为60% */
  margin: 0 auto; /* 水平居中按钮 */
}

8.4 根据数据是否有效为控件添加样式

表单验证中另一个真正重要的基本概念是表单控件的数据是否有效,具有约束限制的表单控件可以根据这些状态来进行定位
:valid 和 :invalid
使用 :valid 和 :invalid 伪类来定位表单控件.

  • 没有约束验证的表单控件永远是有效的,因此永远与 :valid 匹配。

  • 设定了 required,且没有任何值的表单控件是无效的——它们与 :invalid 和 :required 匹配。

  • 具有内置验证功能的控件,如 <input type="email"> 或 <input type="url">,当输入的数据与它们所需的模式不匹配时,会被(与):invalid 匹配(但当它们为空时是有效的)。

  • 当前值超出 min 和 max 属性所指定的范围限制的控件,会被(与):invalid 匹配,但也会被 :out-of-range 匹配,后面还会看到。

  • 还有一些其他的方法可以使元素被 :valid/:invalid 匹配,你会在客户端表单验证文章中看到。

一个简单的 :valid/:invalid 的示例:

4891f6c9397bbe31d4e14fb2ec0c303e.png

input + span {
  position: relative;/* 设置相对定位 */
}

input + span::before {
  position: absolute; /* 设置绝对定位 */
  right: -20px; /* 设置右边距为-20px */
  top: 5px; /* 设置顶部边距为5px */
}

input:invalid {
  border: 2px solid red; /* 当输入无效时,设置边框为2px实线红色 */
}

input:invalid + span::before {
  content: '?'; /* 当输入无效时,在相邻的span元素的:before伪元素中添加"?" */
  color: red; /* 设置颜色为红色 */
}

input:valid + span::before {
  content: '?'; /* 当输入有效时,在相邻的span元素的:before伪元素中添加"?" */
  color: green; /* 设置颜色为绿色 */
}

我们将 <span> 设置为 position: relative,这样我们就可以将生成的内容相对于它们定位。我们根据表单的数据是有效还是无效,绝对定位不同的生成内容——分别是绿色复选框或红色叉号。为了给无效数据增加一点额外的紧迫感,我们还在无效时给输入的数据加上了厚厚的红边。

备注:我们使用 ::before 来添加这些标签,因为我们已经使用 ::after 来添加“required”标签

在范围内和不在范围内的数据
两个相关的伪类需要考虑——:in-range 和 :out-of-range。这些与数字输入相匹配,其中范围限制由 min 和 max 指定,分别供其数据在指定范围之内或之外所使用。

备注:数值输入类型包括 date、month、week、time、datetime-local、number 和 range。数据在范围内的输入也会被 :valid 伪类匹配,而数据在范围外的输入也会被 :invalid 伪类匹配。

示例:

数值输入看起来像这样:

<div>
  <label for="age">Age (must be 12+): </label>
  <input id="age" name="age" type="number" min="12" max="120" required />
  <span></span>
</div>

CSS 样式看起来像这样:

input + span {
  position: relative; /* 设置相对定位 */
}

input + span::after {
  font-size: 0.7rem; /* 设置字体大小为0.7rem */
  position: absolute; /* 设置绝对定位 */
  padding: 5px 10px; /* 设置内边距为5px和10px */
  top: -26px; /* 设置顶部边距为-26px */
}

input:required + span::after {
  color: white; /* 当输入要求必填时,设置颜色为白色 */
  background-color: black; /* 设置背景颜色为黑色 */
  content: "Required"; /* 设置内容为"Required" */
  left: -70px; /* 设置左边距为-70px */
}

input:out-of-range + span::after {
  color: white; /* 当输入超出范围时,设置颜色为白色 */
  background-color: red; /* 设置背景颜色为红色 */
  width: 155px; /* 设置宽度为155px */
  content: "Outside allowable value range"; /* 设置内容为"Outside allowable value range" */
  left: -182px; /* 设置左边距为-182px */
}

beae05b75e3c02d6abf144ff40ceee71.png

数字输入有可能同时是必需的和超出范围的,那么会发生什么呢?因为 :out-of-range 规则在源代码中出现的时间比 :required 规则晚,层叠规则开始发挥作用,并且显示“超出范围”信息。

当页面第一次加载时,会显示“required”,还有一个红叉和边界。当你输入了一个有效的年龄(即在 12-120 的范围内),输入就会变成有效。然而,如果你将年龄输入改为超出范围,则会弹出“Outside allowable value range”的信息,取代了原先的“required”

8.5 为启用或禁用、只读或可读写输入控件添加样式

一个启用的元素是一个可以被激活的元素;它可以被选择、点击、输入等等。另一方面,一个禁用的元素不能以任何方式进行互动,它的数据甚至不会被发送到服务器上
可以用 :enabled 和 :disabled 来定位
看看一个这样做的示例。
其 HTML 代码是一个简单的表单,包含文本输入,加上一个复选框来切换禁用账单地址的开关。账单地址字段默认是禁用的。可以在这里看到它的运行版本。

8f6e1226e1dcf30a4dade9f285cfab10.png

<form>
  <fieldset id="shipping"> <!-- 定义字段集 -->
    <legend>Shipping address</legend> <!-- 定义字段集标题 -->
    <div>
      <label for="name1">Name: </label> <!-- 定义标签 -->
      <input id="name1" name="name1" type="text" required /> <!-- 定义文本输入框,要求必填 -->
    </div>
    <div>
      <label for="address1">Address: </label> <!-- 定义标签 -->
      <input id="address1" name="address1" type="text" required /> <!-- 定义文本输入框,要求必填 -->
    </div>
    <div>
      <label for="pcode1">Zip/postal code: </label> <!-- 定义标签 -->
      <input id="pcode1" name="pcode1" type="text" required /> <!-- 定义文本输入框,要求必填 -->
    </div>
  </fieldset>
  <fieldset id="billing"> <!-- 定义字段集 -->
    <legend>Billing address</legend> <!-- 定义字段集标题 -->
    <div>
      <label for="billing-checkbox">Same as shipping address:</label> <!-- 定义标签 -->
      <input type="checkbox" id="billing-checkbox" checked /> <!-- 定义复选框,默认选中 -->
    </div>
    <div>
      <label for="name" class="billing-label disabled-label">Name: </label> <!-- 定义标签,添加类名 -->
      <input id="name" name="name" type="text" disabled required /> <!-- 定义文本输入框,禁用并要求必填 -->
    </div>
    <div>
      <label for="address2" class="billing-label disabled-label">
        Address:
      </label> <!-- 定义标签,添加类名 -->
      <input id="address2" name="address2" type="text" disabled required /> <!-- 定义文本输入框,禁用并要求必填 -->
    </div>
    <div>
      <label for="pcode2" class="billing-label disabled-label">
        Zip/postal code:
      </label> <!-- 定义标签,添加类名  我们也想把相应的文本标签弄成灰色。这些并不那么容易选择,所以我们用一个类来为它们提供这种风格。-->
      <input id="pcode2" name="pcode2" type="text" disabled required /> <!-- 定义文本输入框,禁用并要求必填 -->
    </div>
  </fieldset>

  <div><button>Submit</button></div> <!-- 定义提交按钮 -->
</form>
input[type="text"]:disabled { /* 选择所有被禁用的文本输入框 */
  background: #eee; /* 背景颜色为浅灰色 */
  border: 1px solid #ccc; /* 边框为1像素宽,实线,颜色为深灰色 */
}

.disabled-label { /* 选择所有类名为 disabled-label 的元素 */
  color: #aaa; /* 文本颜色为深灰色 */
}

最后我们用一些 JavaScript 代码来切换账单地址字段的禁用状态

// 等待页面完成加载
document.addEventListener(
  "DOMContentLoaded",
  () => {
    // 向复选框附加 `change` 事件
    document
      .getElementById("billing-checkbox")
      .addEventListener("change", toggleBilling);
  },
  false,/*布尔值如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。*/
);

function toggleBilling() {
  // 选择账单文本字段
  const billingItems = document.querySelectorAll('#billing input[type="text"]');
  // 选择账单文本标签
  const billingLabels = document.querySelectorAll(".billing-label");

  // 切换账单文本字段和标签
  for (let i = 0; i < billingItems.length; i++) {
    billingItems[i].disabled = !billingItems[i].disabled;
    //通过修改标签的类属性 达到修改标签启用或禁用的效果
    if (
      billingLabels[i].getAttribute("class") === "billing-label disabled-label"
    ) {
      billingLabels[i].setAttribute("class", "billing-label");
    } else {
      billingLabels[i].setAttribute("class", "billing-label disabled-label");
    }
  }
}

它使用 change 事件来让用户启用/禁用账单字段,并切换相关标签的样式

只读或可读写
与 :disabled 和 :enabled 类似,:read-only 和 :read-write 伪类 针对表单输入的两种状态进行了切换。只读输入的值提交给服务器,但用户不能编辑它们,而可读写输入意味着它们可以被编辑——这是它们的默认状态。
使用 readonly 属性可将一个输入设置为只读

举个例子,设想一个确认页面,开发者将之前页面上填写的细节发送到这个页面,目的是让用户在一个地方检查所有细节,添加任何需要的最终数据,然后通过提交确认订单。在这一点上,所有最终的表单数据都可以一次性发送到服务器上。(见运行实例 readonly-confirmation.html。

4d0bfe25d5bdcd7ed58ca8a138a31077.png

HTML 的一个片段如下,注意其中的 readonly 属性

<div>
  <label for="name">Name: </label>
  <input id="name" name="name" type="text" value="Mr Soft" readonly />
</div>

如果你尝试了运行实例,你会发现最上面的一组表单元素是不可聚焦的,当表单被提交时,其值会被提交。
我们使用 :read-only 和 :read-write 伪类为表单控件添加样式:

:is(
    input:read-only,
    input:-moz-read-only,
    textarea:-moz-read-only,
    textarea:read-only
  ) {
  /* 选择所有只读的输入框和文本域 */
  border: 0; /* 边框宽度为0 */
  box-shadow: none; /* 没有阴影 */
  background-color: white; /* 背景颜色为白色 */
}

:is(textarea:-moz-read-write, textarea:read-write) {
  /* 选择所有可写的文本域 */
  box-shadow: inset 1px 1px 3px #ccc; /* 内阴影为1像素宽,颜色为深灰色 */
  border-radius: 5px; /* 边框圆角半径为5像素 */
}

备注: :enabled 和 :read-write 是另外两个可能很少使用的伪类,它们描述了输入元素的默认状态

8.6 单选和复选按钮状态——选中、默认和中间状态

单选按钮和复选框可以被选中或不被选中。但也有一些其他的状态需要考虑

  • :default:匹配在页面加载时默认选中的单选钮/复选框(即通过设置 checked 属性),这些匹配 :default 伪类,即使用户取消选中。

  • :indeterminate:当单选钮/复选框既没有被选中也没有被取消时,它们是中间状态,并将与 :indeterminate 伪类匹配。下文将阐述其细节。

:checked
当单选钮或复选框被选中时,它们将被 :checked 伪类所匹配.
最常见的用途是在复选框或单选按钮被选中时添加不同的样式,在这种情况下,已经使用 appearance: none; 删除了系统默认的样式
我们的有样式的单选钮示例中的 :checked 代码看起来像这样:

input[type="radio"]::before {
  /* 选择所有单选按钮的伪元素 ::before */
  display: block; /* 显示为块级元素 */
  content: " "; /* 内容为空格 */
  width: 10px; /* 宽度为10像素 */
  height: 10px; /* 高度为10像素 */
  border-radius: 6px; /* 边框圆角半径为6像素 */
  background-color: red; /* 背景颜色为红色 */
  font-size: 1.2em; /* 字体大小为1.2em */
  transform: translate(3px, 3px) scale(0); /* 平移3像素并缩放为0 */
  transform-origin: center; /* 变换原点为中心 */
  transition: all 0.3s ease-in; /* 过渡时间为0.3秒,缓动函数为ease-in */
}

input[type="radio"]:checked::before {
  /* 选择所有被选中的单选按钮的伪元素 ::before */
  transform: translate(3px, 3px) scale(1); /* 平移3像素并缩放为1 */
  transition: all 0.3s cubic-bezier(0.25, 0.25, 0.56, 2); /* 过渡时间为0.3秒,缓动函数为cubic-bezier(0.25,0.25,0.56,2) */
}

2b053d0ec30ec447caf309265997a011.png

基本上,我们使用 ::before 伪元素建立了单选按钮“内圈”的样式,
使用一个 transition 来使它在被选择时能有一个很好的动画效果。使用变换而不是过渡 width/height 的好处是,你可以使用 transform-origin 来使它从圆的中心生长,而不是让它看起来从圆的角落生长

:default 和 :indeterminate
:default 伪类可以匹配在页面加载时默认勾选的单选钮或复选框,即使未勾选也是如此。
上面提到的单选钮或复选框在处于既没有选中也没有取消选中的状态时,会被 :indeterminate 伪类所匹配。不确定的元素包括:

  • <input/radio> 输入,当同名组中的所有单选按钮都取消勾选时

  • <input/checkbox> 输入,其 indeterminate 属性通过 JavaScript 代码设置为 true。

  • 没有值的 <progress> 元素。

前面示例的几个修改版本,它们提醒用户默认选项是什么,并在不确定的情况下对单选按钮进行样式设计

<fieldset>
      <legend>Choose your favourite fruit</legend>

      <p>
        <input type="radio" name="fruit" value="cherry" id="cherry">
        <label for="cherry">Cherry</label>
        <span></span>
      </p>
      <p>
        <input type="radio" name="fruit" value="banana" id="banana" checked>
        <label for="banana">Banana</label>
        <span></span>
      </p>
      <p>
        <input type="radio" name="fruit" value="strawberry" id="strawberry">
        <label for="strawberry">Strawberry</label>
        <span></span>
      </p>
    </fieldset>

对于 :default 示例,我们给中间的单选按钮输入添加了 checked 属性,所以它在加载时将被默认选择。然后我们用下面的 CSS 来设计这个样式:

input ~ span {
  /* 选择所有与 input 元素同级且在其后面的 span 元素 */
  position: relative; /* 定位方式为相对定位 */
}

input:default ~ span::after {
  /* 选择所有与默认 input 元素同级且在其后面的 span 元素的伪元素 ::after */
  font-size: 0.7rem; /* 字体大小为0.7rem */
  position: absolute; /* 定位方式为绝对定位 */
  content: "Default"; /* 内容为 "Default" */
  color: white; /* 文本颜色为白色 */
  background-color: black; /* 背景颜色为黑色 */
  padding: 5px 10px; /* 内边距为5像素上下,10像素左右 */
  right: -65px; /* 距离右边界-65像素 */
  top: -3px; /* 距离上边界-3像素 */
}

页面加载时最初选择的项目提供了一个小小的“default”标签。
这里我们使用的是通用兄弟组合器(~),而不是相邻兄弟组合器(+)
备注:你也可以在 GitHub 的 radios-checked-default.html 中找到这个示例的实时演示

对于 :indeterminate 示例,我们没有默认的选定的单选按钮.我们用下面的 CSS 来设计不确定的单选按钮

c182079f99dc8548f124714323b3727a.png

input[type="radio"]:indeterminate {
  /* 选择所有处于不确定状态的单选按钮 */
  border: 2px solid red; /* 边框为2像素宽,实线,颜色为红色 */
  animation: 0.4s linear infinite alternate border-pulse; /* 动画名称为border-pulse,持续时间为0.4秒,线性缓动函数,无限循环,交替播放 */
}

@keyframes border-pulse {
  /* 定义关键帧动画 border-pulse */
  from {
    /* 起始状态 */
    border: 2px solid red; /* 边框为2像素宽,实线,颜色为红色 */
  }

  to {
    /* 结束状态 */
    border: 6px solid red; /* 边框为6像素宽,实线,颜色为红色 */
  }
}

备注:你也可以在 GitHub 的 radios-checked-indeterminate.html 上找到这个示例的运行实例。

中间状态复选框
除了选中和未选中的状态外,复选框还有第三种状态:不确定.
通过 JavaScript 设置的 HTMLInputElement 对象的 indeterminate 属性.
大多数浏览器中,处于不确定状态的复选框在框中有一条横线.
最常见的是当一个复选框“拥有”一些子选项(也是复选框)的时候。如果所有的子选项都被选中,拥有的复选框也被选中,如果它们都没有被选中,拥有的复选框就没有被选中。如果 任何一个或多个子选项的状态与其他选项不同,拥有的复选框就处于不确定的状态

在这个例子中,我们记录了为一个配方收集的原料。当你勾选或不勾选一个原料的复选框时,一个 JavaScript 函数会检查勾选的原料总数:

  • 如果没有一项被选中,配方名称的复选框是未选中状态。

  • 如果选中了一项或两项,配方名称的复选框是 indeterminate(中间)状态。

  • 如果选中了全部三项,配方名称的复选框是 checked(已选中)状态。

const overall = document.querySelector('#enchantment'); // 选择 id 为 enchantment 的元素
const ingredients = document.querySelectorAll('ul input'); // 选择所有 ul 元素下的 input 元素

overall.addEventListener('click', (e) => {
  e.preventDefault(); // 阻止默认事件
});

for (const ingredient of ingredients) {
  ingredient.addEventListener('click', updateDisplay); // 向每个 ingredient 元素附加 click 事件
}

function updateDisplay() {
  let checkedCount = 0; // 定义变量 checkedCount 并初始化为0
  for (const ingredient of ingredients) {
    if (ingredient.checked) { // 如果 ingredient 元素被选中
      checkedCount++; // checkedCount 加1
    }
  }

  if (checkedCount === 0) { // 如果 checkedCount 等于0
    overall.checked = false; // overall 元素未选中
    overall.indeterminate = false; // overall 元素不处于不确定状态
  } else if (checkedCount === ingredients.length) { // 如果 checkedCount 等于 ingredients 的长度
    overall.checked = true; // overall 元素被选中
    overall.indeterminate = false; // overall 元素不处于不确定状态
  } else { // 否则
    overall.checked = false; // overall 元素未选中
    overall.indeterminate = true; // overall 元素处于不确定状态
  }
}

8.7 更多伪类

这些伪类在现代浏览器中得到了相当好的支持

  • :focus-within 伪类匹配一个已经收到焦点的元素或包含一个已经收到焦点的元素。如果你想让整个表单在其内部的输入被聚焦时以某种方式突出显示,这很有用。

  • :focus-visible 伪类匹配**通过键盘交互(而不是触摸或鼠标)获得焦点的元素。**如果你想对键盘焦点与鼠标(或其他)焦点显示不同的样式,这很有用。

  • :placeholder-shown 伪类匹配占位符(即 placeholder 属性的内容)正在显示的 <input> 和 <textarea> 元素,因为该元素的值为空。

下面这些也很有意思,但在浏览器中还没有得到很好的支持

  • :blank 伪类可以选择空表单控件:empty 也匹配没有子元素的元素,如 <input>,但它更普遍——它也匹配其他空元素,如 <br> 和 <hr>。:empty 有合理的浏览器支持;:blank 伪类的规范还没有完成,所以它还不被任何浏览器支持。

  • :user-invalid 伪类,如果支持,将类似于 :invalid,但有更好的用户体验。如果输入收到焦点时值是有效的,当用户输入数据时,如果值暂时无效,该元素可能会匹配 :invalid,但只有当该元素失去焦点时才会匹配 :user-invalid。如果该值最初是无效的,它将在整个焦点持续期间同时匹配 :invalid 和 :user-invalid。与 :invalid 类似,如果该值确实变得有效,它将停止匹配 :user-invalid。

9. 表单数据校验

表单校验帮助我们确保用户以正确格式填写表单数据,确保提交的数据能使我们的应用程序正常工作。

9.1 什么是表单数据校验?

当你提交了没有输入符合预期格式的信息的表单时,注册页面都会给你一个反馈:

  • “该字段是必填的”(该字段不能留空)

  • “请输入你的电话号码,它的格式是:xxx-xxxx”(它要求你输入的数据格式为三个数字接一个横杠,然后再接着是四个数字)

  • “请输入一个合法的邮箱地址”(如果你输入的数据不符合“[email protected]“的邮箱格式)

  • “你的密码长度应该是 8 至 30 位的,并且至少应该包含一个大写字母、一个符号以及一个数字”

这就是表单校验。表单校验可以通过许多不同的方式实现。表单是一个很烦人的东西。

进行表单的数据校验有三个最主要的原因:

  • 以正确的格式获取到正确的数据 —— 如果我们的用户数据以不正确的格式存储,或者他们没有输入正确的信息/完全省略信息,我们的应用程序将无法正常运行。

  • 保护我们的用户 ——强制用户输入安全的密码,有利于保护他们的账户信息。

  • 保护我们自己 —— 恶意用户有很多通过滥用应用中缺乏保护的表单破坏应用的方法

警告:永远不要相信从客户端传递到服务器的数据。即使您的表单正确验证并防止输入格式错误,恶意用户仍然可以更改网络请求。

不同类型的表单数据校验

客户端校验
发生在浏览器端,表单数据被提交到服务器之前,这种方式相较于服务器端校验来说,用户体验更好,它能实时的反馈用户的输入校验结果,这种类型的校验可以进一步细分成下面这些方式:

  • JavaScript 校验,这是可以完全自定义的实现方式;

  • HTML5 内置校验,这不需要 JavaScript,而且性能更好,但是不能像 JavaScript 那样可自定义。

服务器端校验
发生在浏览器提交数据并被服务器端程序接收之后 —— 通常服务器端校验都是发生在将数据写入数据库之前,如果数据没通过校验,则会直接从服务器端返回错误消息,并且告诉浏览器端发生错误的具体位置和原因,服务器端校验不像客户端校验那样有好的用户体验,因为它直到整个表单都提交后才能返回错误信息
但是服务器端校验是你的应用对抗错误/恶意数据的最后防线,在这之后,数据将被持久化至数据库。当今所有的服务端框架都提供了数据校验与清洁功能(让数据更安全)。

在真实的项目开发过程中,开发者一般都倾向于使用客户端校验与服务器端校验的组合校验方式以更好的保证数据的正确性与安全性。

9.2 使用内置表单数据校验

HTML5 一个特别有用的新功能就是,可以在不写一行脚本代码的情况下,即对用户的输入进行数据校验,这都是通过表单元素的校验属性 (en-US)实现的.
这些属性可以让你定义一些规则,用于限定用户的输入,比如某个输入框是否必须输入,或者某个输入框的字符串的最小最大长度限制,或者某个输入框必须输入一个数字、邮箱地址等;还有数据必须匹配的模式。

当一个元素校验通过时:

  • 该元素将可以通过 CSS 伪类 :valid 进行特殊的样式化;

  • 如果用户尝试提交表单,如果没有其他的控制来阻止该操作(比如 JavaScript 即可阻止提交),那么该表单的数据会被提交。

如果一个元素未校验通过

  • 该元素将可以通过 CSS 伪类 :invalid 进行特殊的样式化

  • 如果用户尝试提交表单,浏览器会展示出错误消息,并停止表单的提交。

input 元素的校验约束 — starting simple
一些用于<input>元素校验的 HTML5 的特性
required属性

342eaeafe88078873e74d1c3775d548b.png

<form>
  <label for="choose">Would you prefer a banana or cherry?</label>
  <input id="choose" name="i_like" required />
  <button>Submit</button>
</form>
input:invalid {
  border: 2px dashed red;
}

input:valid {
  border: 2px solid black;
}

使用正则表达式校验
另一个常用的校验功能是 pattern 属性,以 Regular Expression 作为 value 值。
下面是一些例子,让你对它们的工作原理有个基本的了解:

  • a — 匹配一个字符a(不能匹配 b, aa等等.)

  • abc — 匹配 a、其次 b、最后 c.

  • a* — 匹配 0 个或者多个字符 a (+ 代表至少匹配一个或者多个).

  • [^a] — 匹配一个字符,但它不能是a.

  • a|b — 匹配一个字符 a 或者 b.

  • [abc] — 匹配一个字符,它可以是a、b或c.

  • [^abc] — 匹配一个字符,但它不可以是 a、b 或 c.

  • [a-z] — 匹配字符范围 a-z且全部小写 (你可以使用 [A-Za-z] 涵盖大小写,或 [A-Z] 来限制必须大写).

  • a.c——匹配字符 a,中间匹配任意一个字符,最后匹配字符 c。

  • a{5} — 匹配字符 a 五次。

  • a{5,7} — 匹配字符 a 五到七次,不能多或者少。

  • [ -] — 匹配一个空格或者虚线。

  • [0-9] — 匹配数字范围 0~9.

你可以任意地组合这些,你可以任意指定不同的部分:

  • [Ll].*k — 匹配一个大写L或者小写的l, 之后匹配 0 个或多个任意类型的字符,最后匹配一个小写字母 k.

  • [A-Z][A-Za-z' -]+ — 一个大写字母后面跟着匹配一个及以上的大小写字母或者中划线或者撇号或者空格。这个可以用于校验英语会话中城市或城镇名,但这需要首字母以大写开头,不包括其他字符,例如来自英国的 Manchester, Ashton-under-lyne, 以及 Bishop's Stortford 等。

  • [0-9]{3}[ -][0-9]{3}[ -][0-9]{4} — 简单的匹配一个美国内的电话号码 — 三个数字 0-9, 后面跟着一个空格或者中划线,之后匹配三个数字 0-9, 再跟着一个空格或者中划线,最后跟着四个数字 0-9. 但实际情况可能更加复杂,因为有些人会给号码加上括号什么的,这里的表达式只是用来做一个简单的演示。

实现这些例子 — 更新你的 html 文档表单增加一个 pattern 属性,如下:

4a8ce5d4e73ca2e4b61f35f031a79083.png

<form>
  <label for="choose">Would you prefer a banana or a cherry?</label>
  <input id="choose" name="i_like" required pattern="banana|cherry" />
  <button>Submit</button>
</form>

该 <input> 元素接受两个值中的一个:字符串 "banana" 或者字符串"cherry".

备注:一些 <input> 元素类型不需要pattern 属性进行校验。指定特定 email 类型 就会使用匹配电子邮件格式的正则表达式来校验 (如果有 multiple 属性请用逗号来分割多个邮箱). 进一步来说,字段 url 类型则会自动校验输入的是否为一个合法的链接。

备注:该 <textarea> 元素不支持pattern 属性

限制输入的长度
所有文本框 (<input> 或 <textarea>) 都可以使用minlength 和 maxlength 属性来限制长度。

在数字条目中 (i.e. <input type="number">), 该 min 和 max 属性同样提供校验约束。如果字段的值小于min 属性的值或大于 max 属性的值,该字段则无效。

看看另外一个例子:

4b0239f7042f42a8e3b4d3e6f6950a07.png

<form>
  <div>
    <label for="choose">Would you prefer a banana or a cherry?</label>
    <input id="choose" name="i_like" required minlength="6" maxlength="6" />
  </div>
  <div>
    <label for="number">How many would you like?</label>
    <input type="number" id="number" name="amount" value="1" min="1" max="10" />
  </div>
  <div>
    <button>Submit</button>
  </div>
</form>

备注:<input type="number"> (或者其他类型,像 range) 也可以获取到一个step 属性,指定了值在增减过程固定改变的值 (如向上增加和向下减少的按钮).

完整的例子
一个完整的展示 HTML 中使用校验属性的例子:

a338e74a578178a580f15c09fec0238c.png

<form>
  <p>
    <fieldset>
      <legend>Title<abbr title="This field is mandatory">*</abbr></legend> <!-- 标题字段的标题 -->
      <input type="radio" required name="title" id="r1" value="Mr"><label for="r1">Mr.</label> <!-- 单选按钮,选择 "Mr." -->
      <input type="radio" required name="title" id="r2" value="Ms"><label for="r2">Ms.</label> <!-- 单选按钮,选择 "Ms." -->
    </fieldset>
  </p>
  <p>
    <label for="n1">How old are you?</label> <!-- 年龄字段的标题 -->
    <!-- 这里的 pattern 属性可以用作不支持 number 类 input 浏览器的备用方法
         请注意当与数字输入框一起使用时,支持 pattern 属性的浏览器会使它默认失效。
         它仅仅是在这里用作备用 -->
    <input type="number" min="12" max="120" step="1" id="n1" name="age"
           pattern="\d+"> <!-- 数字输入框,输入年龄 -->
  </p>
  <p>
    <label for="t1">What's your favorite fruit?<abbr title="This field is mandatory">*</abbr></label> <!-- 最喜欢的水果字段的标题 -->
    <input type="text" id="t1" name="fruit" list="l1" required
           pattern="[Bb]anana|[Cc]herry|[Aa]pple|[Ss]trawberry|[Ll]emon|[Oo]range"> <!-- 文本输入框,输入最喜欢的水果 -->
    <datalist id="l1"> <!-- 列表,提供可选水果 -->
      <option>Banana</option> <!-- 香蕉 -->
      <option>Cherry</option> <!-- 樱桃 -->
      <option>Apple</option> <!-- 苹果 -->
      <option>Strawberry</option> <!-- 草莓 -->
      <option>Lemon</option> <!-- 柠檬 -->
      <option>Orange</option> <!-- 橙子 -->
    </datalist>
  </p>
  <p>
    <label for="t2">What's your e-mail?</label> <!-- 电子邮件字段的标题 -->
    <input type="email" id="t2" name="email"> <!-- 电子邮件输入框,输入电子邮件地址 -->
  </p>
  <p>
    <label for="t3">Leave a short message</label> <!-- 留言字段的标题 -->
    <textarea id="t3" name="msg" maxlength="140" rows="5"></textarea> <!-- 文本域,输入留言 -->
  </p>
  <p>
    <button>Submit</button> <!-- 提交按钮 -->
  </p>
</form>
body {
  font: 1em sans-serif; /* 字体大小为1em,字体族为sans-serif */
  padding: 0; /* 内边距为0 */
  margin: 0; /* 外边距为0 */
}

form {
  max-width: 200px; /* 最大宽度为200像素 */
  margin: 0; /* 外边距为0 */
  padding: 0 5px; /* 内边距为5像素左右,0像素上下 */
}
/* 选择了所有作为 p 元素直接子元素的 label 元素 */
p > label {
  display: block; /* 显示为块级元素 */
}

input[type="text"],
input[type="email"],
input[type="number"],
textarea,
fieldset {
  /* 需要在基于 WebKit 的浏览器上对表单元素进行恰当的样式设置 */
  -webkit-appearance: none; /* 取消默认外观 */

  width: 100%; /* 宽度为100% */
  border: 1px solid #333; /* 边框为1像素宽,实线,颜色为深灰色 */
  margin: 0; /* 外边距为0 */

  font-family: inherit; /* 继承父元素的字体族 */
  font-size: 90%; /* 字体大小为90% */

  -moz-box-sizing: border-box; /* 在 Firefox 浏览器中,盒模型为border-box */
  box-sizing: border-box; /* 盒模型为border-box */
}

input:invalid {
  box-shadow: 0 0 5px 1px red; /* 阴影为红色,向右下偏移5像素,模糊半径为1像素 */
}

input:focus:invalid {
  outline: none; /* 取消轮廓线 */
}

自定义错误信息
每次我们提交无效的表单数据时,浏览器总会显示错误信息。但是显示的信息取决于你所使用的浏览器。

这些自动生成的错误有两个缺点

  • 没有标准可以让 CSS 来改变他们的界面外观。

  • 这依赖于他们使用的浏览器环境,意味着你可能在这种语言的页面里得到另一种语言的错误提示

    521e1aa26e1c2b5e022076ad6debdb1d.png

自定义这些消息的外观和文本,你必须使用 JavaScript; 不能使用 HTML 和 CSS 来改变。

HTML5 提供 constraint validation API 来检测和自定义表单元素的状态。他可以改变错误信息的文本:
看一个例子:

d448a790c2b828cdad6bbeb05ace9e62.png

var email = document.getElementById("mail"); // 获取 id 为 "mail" 的元素

email.addEventListener("input", function (event) { // 为 email 元素添加 input 事件监听器
  if (email.validity.typeMismatch) { // 如果 email 元素的值不符合类型要求
    email.setCustomValidity("I expect an e-mail, darling!"); // 设置自定义验证信息
  } else { // 否则
    email.setCustomValidity(""); // 清除自定义验证信息
  }
});

9.3 使用 JavaScript 校验表单

如果你想控制原生错误信息的界面外观,或者你想处理不支持 HTML 内置表单校验的浏览器,则必须使用 Javascript。

约束校验的 API
越来越多的浏览器支持限制校验 API,并且这逐渐变得可靠。
这些 API 由成组的方法和属性构成,可在特定的表单元素接口上调用:

  • HTMLButtonElement

  • HTMLFieldSetElement

  • HTMLInputElement

  • HTMLOutputElement (en-US)

  • HTMLSelectElement

  • HTMLTextAreaElement

约束校验的 API 及属性

属性 描述
validationMessage 一个本地化消息,描述元素不满足校验条件时(如果有的话)的文本信息。如果元素无需校验(willValidate 为 false),或元素的值满足校验条件时,为空字符串。
validity 一个 ValidityState 对象,描述元素的验证状态。详见有关可能的验证状态的文章。
validity.customError 如果元素设置了自定义错误,返回 true ;否则返回false。
validity.patternMismatch 如果元素的值不匹配所设置的正则表达式,返回 true,否则返回 false。当此属性为 true 时,元素将命中 :invalid CSS 伪类。
validity.rangeOverflow 如果元素的值高于所设置的最大值,返回 true,否则返回 false。当此属性为 true 时,元素将命中 :invalid CSS 伪类。
validity.rangeUnderflow 如果元素的值低于所设置的最小值,返回 true,否则返回 false。当此属性为 true 时,元素将命中 :invalid CSS 伪类。
validity.stepMismatch 如果元素的值不符合 step 属性的规则,返回 true,否则返回 false。当此属性为 true 时,元素将命中 :invalid CSS 伪类。
validity.tooLong 如果元素的值超过所设置的最大长度,返回 true,否则返回 false。当此属性为 true 时,元素将命中 :invalid CSS 伪类。
validity.typeMismatch 如果元素的值出现语法错误,返回 true,否则返回 false。当此属性为 true 时,元素将命中 :invalid CSS 伪类。
validity.valid 如果元素的值不存在校验问题,返回 true,否则返回 false。当此属性为 true 时,元素将命中 :valid CSS 伪类,否则命中 :invalid CSS 伪类。
validity.valueMissing 如果元素设置了 required 属性且值为空,返回 true,否则返回 false。当此属性为 true 时,元素将命中 :invalid CSS 伪类。
willValidate 如果元素在表单提交时将被校验,返回 true,否则返回 false。

约束校验 API 的方法

方法 描述
checkValidity() 如果元素的值不存在校验问题,返回 true,否则返回 false。如果元素校验失败,此方法会触发 invalid 事件
HTMLFormElement.reportValidity() 如果元素或它的子元素控件符合校验的限制,返回 true . 当返回为 false 时,对每个无效元素可撤销 invalid 事件会被唤起并且校验错误会报告给用户。
setCustomValidity(message) 为元素添加一个自定义的错误消息;如果设置了自定义错误消息,该元素被认为是无效的,则显示指定的错误。这允许你使用 JavaScript 代码来建立校验失败,而不是用标准约束校验 API 所提供的。这些自定义信息将在向用户报告错误时显示。如果参数为空,则清空自定义错误。

对于旧版浏览器,可以使用 polyfill(例如 Hyperform),来弥补其对约束校验 API 支持的不足。既然你已经使用 JavaScript,在您的网站或 Web 应用程序的设计和实现中使用 polyfill 并不是累赘。

使用约束校验 API 的例子
使用这个 API 来构建自定义错误消息。

<form novalidate> <!-- 表单,禁用浏览器默认验证 -->
  <p>
    <label for="mail"> <!-- 标签,与 id 为 "mail" 的元素关联 -->
      <span>Please enter an email address:</span> <!-- 提示文本 -->
      <input type="email" id="mail" name="mail" /> <!-- 电子邮件输入框 -->
      <span class="error" aria-live="polite"></span> <!-- 错误信息显示区域 -->
    </label>
  </p>
  <button>Submit</button> <!-- 提交按钮 -->
</form>

表单使用 novalidate 属性关闭浏览器的自动校验.这允许我们使用脚本控制表单校验
并不禁止对约束校验 API 的支持或是以下 CSS 伪类::valid、:invalid、:in-range 、:out-of-range 的应用。

aria-live (en-US) 属性确保我们的自定义错误信息将呈现给所有人,包括使用屏幕阅读器等辅助技术的人。

以下 CSS 样式使我们的表单和其错误输出看起来更有吸引力:

/* 仅为了使示例更好看 */
body {
  font: 1em sans-serif; /* 字体大小为1em,字体族为sans-serif */
  padding: 0; /* 内边距为0 */
  margin: 0; /* 外边距为0 */
}

form {
  max-width: 200px; /* 最大宽度为200像素 */
}

p * {
  display: block; /* 显示为块级元素 */
}

input[type="email"] {
  -webkit-appearance: none; /* 取消默认外观 */

  width: 100%; /* 宽度为100% */
  border: 1px solid #333; /* 边框为1像素宽,实线,颜色为深灰色 */
  margin: 0; /* 外边距为0 */

  font-family: inherit; /* 继承父元素的字体族 */
  font-size: 90%; /* 字体大小为90% */

  -moz-box-sizing: border-box; /* 在 Firefox 浏览器中,盒模型为border-box */
  box-sizing: border-box; /* 盒模型为border-box */
}
/* 校验失败的元素样式 */
input:invalid {
  border-color: #900; /* 边框颜色为深红色 */
  background-color: #fdd; /* 背景颜色为浅红色 */
}

input:focus:invalid {
  outline: none; /* 取消轮廓线 */
}
/* 错误消息的样式 */
.error {
  width: 100%; /* 宽度为100% */
  padding: 0; /* 内边距为0 */

  font-size: 80%; /* 字体大小为80% */
  color: white; /* 文本颜色为白色 */
  background-color: #900; /* 背景颜色为深红色 */
  border-radius: 0 0 5px 5px; /* 边框圆角半径,左上角和右上角为0,左下角和右下角为5像素 */

  -moz-box-sizing: border-box; /* 在 Firefox 浏览器中,盒模型为border-box */
  box-sizing: border-box; /* 盒模型为border-box */
}

.error.active {
  padding: 0.3em; /* 内边距为0.3em */
}

以下 JavaScript 代码演示如何设置自定义错误校验:

// 有许多方式可以获取 DOM 节点;在此我们获取表单本身和
// email 输入框,以及我们将放置错误信息的 span 元素。

var form = document.getElementsByTagName("form")[0];
var email = document.getElementById("mail");
var error = document.querySelector(".error");

email.addEventListener(
  "input",
  function (event) {
    // 当用户输入信息时,校验 email 字段
    if (email.validity.valid) {
      // 如果校验通过,清除已显示的错误消息
      error.innerHTML = ""; // 重置消息的内容
      error.className = "error"; // 重置消息的显示状态
    }
  },
  false,
);
form.addEventListener(
  "submit",
  function (event) {
    // 当用户提交表单时,校验 email 字段
    if (!email.validity.valid) {
      // 如果校验失败,显示一个自定义错误
      error.innerHTML = "I expect an e-mail, darling!";
      error.className = "error active";
      // 还需要阻止表单提交事件,以取消数据传送
      event.preventDefault();
    }
  },
  false,
);

0c1e6cd9c7feb2a5eac76f46fec9a79f.png

约束校验 API 为您提供了一个强大的工具来处理表单校验,让您可以对用户界面进行远超过仅仅使用 HTML 和 CSS 所能得到的控制。

不使用内建 API 时的表单校验
有时,例如使用旧版浏览器或自定义小部件 (en-US),您将无法(或不希望)使用约束校验 API。
可以使用 JavaScript 来校验您的表单。校验表单比起真实数据校验更像是一个用户界面问题。
我应该进行什么样的校验?
你需要确定如何校验你的数据:字符串操作,类型转换,正则表达式等。
如果表单校验失败,我该怎么办?
您必须决定表单的行为方式:表单是否发送数据?是否突出显示错误的字段?是否显示错误消息?
如何帮助用户纠正无效数据?
您应该提供前期建议,以便他们知道预期的输入是什么以及明确的错误消息。

不使用约束校验 API 的例子

<!--我们只是关闭了 HTML 校验功能。-->
<form> 
  <p>
    <label for="mail"> <!-- 标签,与 id 为 "mail" 的元素关联 -->
      <span>Please enter an email address:</span> <!-- 提示文本 -->
      <input type="text" class="mail" id="mail" name="mail" /> <!-- 文本输入框,输入电子邮件地址 -->
      <span class="error" aria-live="polite"></span> <!-- 错误信息显示区域 -->
    </label>
  </p>

  <p>
    <button type="submit">Submit</button> <!-- 提交按钮  某些旧版浏览器需要将“button”元素上的“type”属性显式设置为“submit” -->
  </p>
</form>

CSS 也不需要太多的改动,我们只需将 :invalid 伪类变成真实的类,并避免使用不适用于 Internet Explorer 6 的属性选择器。

/* 校验失败的元素样式 */
input.invalid {
  border-color: #900;
  background-color: #fdd;
}

input:focus.invalid {
  outline: none;
}

JavaScript 代码有很大的变化,需要做更多的工作:

// 使用旧版浏览器选择 DOM 节点的方法较少
var form = document.getElementsByTagName("form")[0]; // 获取第一个 form 元素
var email = document.getElementById("mail"); // 获取 id 为 "mail" 的元素
// 以下是在 DOM 中访问下一个兄弟元素的技巧
// 这比较危险,很容易引起无限循环
// 在现代浏览器中,应该使用 element.nextElementSibling
var error = email;
while ((error = error.nextSibling).nodeType != 1); // 获取 email 元素后面的第一个元素节点

var emailRegExp =
  /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; // 定义电子邮件正则表达式
// 许多旧版浏览器不支持 addEventListener 方法
// 这只是其中一种简单的处理方法
function addEvent(element, event, callback) { // 定义 addEvent 函数,用于添加事件监听器
  var previousEventCallBack = element["on" + event]; // 获取元素原有的事件监听器
  element["on" + event] = function (e) { // 设置元素的新事件监听器
    var output = callback(e); // 调用回调函数
    
    if (output === false) return false; // 如果回调函数返回 false,则阻止默认事件和冒泡// 返回 `false` 来停止回调链,并中断事件的执行

    if (typeof previousEventCallBack === "function") { // 如果原有的事件监听器是函数
      output = previousEventCallBack(e); // 调用原有的事件监听器
      if (output === false) return false; // 如果原有的事件监听器返回 false,则阻止默认事件和冒泡
    }
  };
}
// 现在我们可以重构字段的约束校验了
// 由于不使用 CSS 伪类,我们必须明确地设置 valid 或 invalid 类到 email 字段上
addEvent(window, "load", function () { // 页面加载完成后执行以下代码
  var test = email.value.length === 0 || emailRegExp.test(email.value); // 检查 email 元素的值是否为空或符合正则表达式

  email.className = test ? "valid" : "invalid"; // 根据检查结果设置 email 元素的类名
});
// 处理用户输入事件
addEvent(email, "input", function () { // 当 email 元素的值改变时执行以下代码
  var test = email.value.length === 0 || emailRegExp.test(email.value); // 检查 email 元素的值是否为空或符合正则表达式
  if (test) { // 如果检查结果为真
    email.className = "valid"; // 设置 email 元素的类名为 "valid"
    error.innerHTML = ""; // 清空错误信息显示区域的内容
    error.className = "error"; // 设置错误信息显示区域的类名为 "error"
  } else { // 否则
    email.className = "invalid"; // 设置 email 元素的类名为 "invalid"
  }
});
// 处理表单提交事件
addEvent(form, "submit", function () { // 当表单提交时执行以下代码
  var test = email.value.length === 0 || emailRegExp.test(email.value); // 检查 email 元素的值是否为空或符合正则表达式

  if (!test) { // 如果检查结果为假
    email.className = "invalid"; // 设置 email 元素的类名为 "invalid"
    error.innerHTML = "I expect an e-mail, darling!"; // 设置错误信息显示区域的内容
    error.className = "error active"; // 设置错误信息显示区域的类名为 "error active"
    return false; // 阻止表单提交
  } else { // 否则
    email.className = "valid"; // 设置 email 元素的类名为 "valid"
    error.innerHTML = ""; // 清空错误信息显示区域的内容
    error.className = "error"; // 设置错误信息显示区域的类名为 "error"
  }
});

ddf61b3a7df1a4acf1ffbb906137c629.png

建立自己的校验系统并不难。困难的部分是使其足够通用,以跨平台和任何形式使用它可以创建。
有许多库可用于执行表单校验; 你应该毫不犹豫地使用它们。这里有一些例子:

  • 独立的库(原生 Javascript 实现):

    • Validate.js

  • jQuery 插件:

    • Validation

    • Valid8

远程校验
当用户输入的数据与存储在应用程序服务器端的附加数据绑定时,这种校验是必要的。
一个应用实例就是注册表单,在这里你需要一个用户名。为了避免重复,执行一个 AJAX 请求来检查用户名的可用性,要比让先用户发送数据,然后因为表单重复了返回错误信息要好得多。
执行这样的校验需要采取一些预防措施

  • 要求公开 API 和一些数据;您需要确保它不是敏感数据

  • 网络滞后需要执行异步校验 这需要一些用户界面的工作,以确保如果校验没有适当的执行,用户不会被阻止。

10. 发送表单数据

当用户提交表单时发生了什么——数据去了哪,以及当它到达时该如何处理?我们还研究了与发送表单数据相关的一些安全问题。

10.1 数据都去哪儿了?

客户端/服务器体系结构
客户端(通常是 web 浏览器) 向服务器发送请求 (大多数情况下是Apache、Nginx、IIS、Tomcat等 web 服务器),使用HTTP 协议。服务器使用相同的协议来回答请求。

c855783211cd2318ac831bd7ce3d40f7.png

在客户端,HTML 表单只不过是一种方便的用户友好的方式,可以配置 HTTP 请求将数据发送到服务器。这使用户能够提供在 HTTP 请求中传递的信息。

在客户端:定义如何发送数据
<form>元素定义了如何发送数据。它的所有属性都是为了让您配置当用户点击提交按钮时发送的请求。两个最重要的属性是action和method

action 属性
这个属性定义了发送数据要去的位置。它的值必须是一个有效的 URL。如果没有提供此属性,则数据将被发送到包含这个表单的页面的 URL。
在这个例子中,数据被发送到一个绝对 URL —— http://foo.com:

<form action="http://foo.com">…</form>

这里,我们使用相对 URL——数据被发送到服务器上的不同 URL

<form action="/somewhere_else">…</form>

在没有属性的情况下,像下面一样,<form>数据被发送到表单出现的相同页面上

<form>…</form>

许多较老的页面使用下面的符号表示数据应该被发送到包含表单的相同页面;这是必需的,因为直到 HTML5action属性都需要该符号。现在,这不再需要了。

<form action="#">…</form>

备注:可以指定使用 HTTPS(安全 HTTP) 协议的 URL。当您这样做时,数据将与请求的其余部分一起加密,即使表单本身是托管在使用 HTTP 访问的不安全页面上。另一方面,如果表单是在安全页面上托管的,但是您指定了一个不安全的 HTTP URL,它带有action属性,所有的浏览器都会在每次尝试发送数据时向用户显示一个安全警告,因为数据不会被加密。

method 属性
该属性定义了如何发送数据。
HTTP 协议提供了几种执行请求的方法:最常见的是GET方法和POST方法。

GET 方法-请求服务器返回给定的资源
考虑下面这个表单:

<form action="http://foo.com" method="get">
  <div>
    <label for="say">What greeting do you want to say?</label>
    <input name="say" id="say" value="Hi" />
  </div>
  <div>
    <label for="to">Who do you want to say it to?</label>
    <input name="to" id="to" value="Mom" />
  </div>
  <div>
    <button>Send my greetings</button>
  </div>
</form>

由于已经使用了GET方法,当你提交表单的时候,您将看到www.foo.com/?say=Hi&to=Mom在浏览器地址栏里。

bfb8fdbb73c4c2d49923b50bdf477399.png

数据作为一系列的名称/值对被附加到 URL。
在 URL web 地址结束之后,我们得到一个问号 (?),后面跟着由一个与符号 (&) 互相分隔开的名称/值对。

HTTP 请求如下:

GET /?say=Hi&to=Mom HTTP/2.0
Host: foo.com

POST 方法-浏览器在询问响应时使用与服务器通信的方法.该响应考虑了 HTTP 请求正文中提供的数据.
如果使用该方法发送表单,则将数据追加到 HTTP 请求的主体中。

<form action="http://foo.com" method="post">
  <div>
    <label for="say">What greeting do you want to say?</label>
    <input name="say" id="say" value="Hi" />
  </div>
  <div>
    <label for="to">Who do you want to say it to?</label>
    <input name="to" id="to" value="Mom" />
  </div>
  <div>
    <button>Send my greetings</button>
  </div>
</form>

当使用POST方法提交表单时,没有数据会附加到 URL,HTTP 请求看起来是这样的,而请求主体中包含的数据是这样的:

POST / HTTP/2.0
Host: foo.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 13

say=Hi&to=Mom

de3d3b77f42fc17eb20daa4fd1af34ee.png

Content-Length数据头表示主体的大小,Content-Type数据头表示发送到服务器的资源类型。稍后我们将讨论这些标头。

查看 HTTP 请求
HTTP 请求永远不会显示给用户.
您的表单数据将显示在 Chrome 网络选项卡中:

  1. 按下 F12

  2. 选择 "Network"

  3. 选择 "All"

  4. 在 "Name" 标签页选择 "foo.com"

  5. 选择 "Headers"

e1a5029da6f6607243251b9f189b2cd9.png

唯一显示给用户的是被调用的 URL。正如我们上面提到的,使用GET请求用户将在他们的 URL 栏中看到数据,但是使用POST请求用户将不会看到。

  • 如果您需要发送一个密码 (或其他敏感数据),永远不要使用GET方法否则数据会在 URL 栏中显示,这将非常不安全。

  • 如果您需要发送大量的数据,那么POST方法是首选的,因为一些浏览器限制了 URL 的大小。此外,许多服务器限制它们接受的 URL 的长度。

在服务器端:检索数据
无论选择哪种 HTTP 方法,服务器都会接收一个字符串并解析,以便将数据作为键/值对序列获取。

例子:Python
在 web 页面上显示提交的数据。这将使用Flask framework来呈现模板、处理表单数据提交等.
这是一个简单的 Flask 应用程序,它定义了两个路由:/ 和 /hello。当用户访问 / 路由时,应用程序会渲染并返回 form.html 模板。当用户访问 /hello 路由时,应用程序会从表单数据中获取 say 和 to 字段的值,并将它们传递给 greeting.html 模板进行渲染,然后返回渲染后的模板。

from flask import Flask, render_template, request
app = Flask(__name__)

@app.route('/', methods=['GET', 'POST']) # 定义 / 路由
def form():
    return render_template('form.html') # 渲染并返回 form.html 模板

@app.route('/hello', methods=['GET', 'POST']) # 定义 /hello 路由
def hello():
    return render_template('greeting.html', say=request.form['say'], to=request.form['to']) # 获取表单数据并渲染 greeting.html 模板

if __name__ == "__main__":
    app.run() # 运行应用程序

以上代码中引用的两个模板如下:

  • form.html: 与我们在 POST 方法 小节中看到的相同的表单,但是将action设置为{ { url_for('hello') }}。(这是一个Jinja2模板,它基本上是 HTML,但是可以包含对运行包含在花括号中的 web 服务器的 Python 代码的调用。url_for('hello')基本上是在 “提交表单时重定向到/hello”

  • greeting.html: 这个模板只包含一行,用于呈现渲染时传递给它的两个数据块。这是通过前面所见的hello()函数完成的,该函数在/helloURL 被导向时运行。

备注:同样,如果您只是尝试将其直接加载到浏览器中,那么这段代码将无法工作。Python 的工作方式与 PHP 略有不同——要在本地运行此代码,您需要安装 Python/pip,然后使用pip3 install flask安装 Flask。此时,您应该能够使用python3 python-example.py来运行这个示例,然后在浏览器中导航到localhost:5000。

其他语言和框架
有许多其他的服务器端技术可以用于表单处理,包括Perl、Java、 .Net、Ruby等。只挑你最喜欢的用就好。
直接使用这些技术并不常见,因为这可能很棘手。更常见的是使用许多优秀的框架,这些框架使处理表单变得更容易,例如:

  • Django for Python(比Flask要重量级一些,但是有更多的工具和选项。)

  • Express for Node.js

  • Laravel for PHP

  • Ruby On Rails for Ruby

  • Phoenix for Elixir

10.2 特殊案例:发送文件

用 HTML 表单发送文件是一个特殊的例子。
文件是二进制数据——或者被认为是这样的——而所有其他数据都是文本数据。
由于 HTTP 是一种文本协议,所以处理二进制数据有特殊的要求。

enctype 属性
该属性允许您指定在提交表单时所生成的请求中的Content-Type的 HTTP 数据头的值。默认情况下,它的值是application/x-www-form-urlencoded。它的意思是:“这是已编码为 URL 参数的表单数据。”

如果你想要发送文件,你需要额外的三个步骤

  • method属性设置为POST,因为文件内容不能放入 URL 参数中。

  • enctype的值设置为multipart/form-data,因为数据将被分成多个部分,每个文件单独占用一个部分,表单正文中包含的文本数据(如果文本也输入到表单中)占用一个部分。

  • 包含一个或多个File picker小部件,允许用户选择将要上传的文件。

<form method="post" enctype="multipart/form-data">
  <div>
    <label for="file">Choose a file</label>
    <input type="file" id="file" name="myFile" />
  </div>
  <div>
    <button>Send the file</button>
  </div>
</form>

备注:一些浏览器支持<input>的multiple属性,它允许只用一个 <input> 元素选择一个以上的文件上传。服务器如何处理这些文件取决于服务器上使用的技术。如前所述,使用框架将使您的生活更轻松。

为了防止滥用,许多服务器配置了文件和 HTTP 请求的大小限制。在发送文件之前,先检查服务器管理员的权限是很重要的。

10.3 常见的安全问题

每次向服务器发送数据时,都需要考虑安全性。
HTML 表单是最常见的攻击路径
这些问题从来都不是来自 HTML 表单本身,它们来自于服务器如何处理数据

XSS 和 CSRF
跨站脚本 (XSS) 和 跨站点请求伪造 (CSRF) 是常见的攻击类型,它们发生在 当您将用户发送的数据显示给这个用户或另一个用户时

XSS 允许攻击者将客户端脚本注入到其他用户查看的 Web 页面中。
CSRF 攻击类似于 XSS 攻击,因为它们以相同的方式开始攻击——向 Web 页面中注入客户端脚本——但它们的目标是不同的。CSRF 攻击者试图将权限升级到特权用户 (比如站点管理员) 的级别,以执行他们不应该执行的操作 (例如,将数据发送给一个不受信任的用户)。
XSS 攻击利用用户对 web 站点的信任,而 CSRF 攻击则利用网站对其用户的信任。

为了防止这些攻击,您应该始终检查用户发送给服务器的数据 (如果需要显示),尽量不要显示用户提供的 HTML 内容。
当今市场上几乎所有的框架都实现了一个最小的过滤器,它可以从任何用户发送的数据中删除 HTML<script>、<iframe> 和<object> 元素。这有助于降低风险,但并不一定会消除风险。

SQL 注入
SQL 注入是一种试图在目标 web 站点使用的数据库上执行操作的攻击类型。
这通常包括发送一个 SQL 请求,希望服务器能够执行它.(通常发生在应用服务器试图存储由用户发送的数据时)。这实际上是攻击网站的主要途径之一。
其后果可能是可怕的,从数据丢失到通过使用特权升级控制整个网站基础设施的攻击。这是一个非常严重的威胁,您永远不应该存储用户发送的数据,而不执行一些清理工作

HTTP 数据头注入和电子邮件注入
这种类型的攻击出现在当您的应用程序基于表单上用户的数据输入构建 HTTP 头部或电子邮件时。
这些不会直接损害您的服务器或影响您的用户,但它们会引发一个更深入的问题,例如会话劫持或网络钓鱼攻击。
这些攻击大多是无声的,并且可以将您的服务器变成僵尸。

偏执:永远不要相信你的用户
你如何应对这些威胁呢?
这是一个远远超出本指南的主题,不过有一些规则需要牢记。最重要的原则是:永远不要相信你的用户,包括你自己;即使是一个值得信赖的用户也可能被劫持。

所有到达服务器的数据都必须经过检查和消毒。总是这样。没有例外。

  • 远离有潜在危险的字符转义。应该如何谨慎使用的特定字符取决于所使用的数据的上下文和所使用的服务器平台,但是所有的服务器端语言都有相应的功能。

  • 限制输入的数据量,只允许有必要的数据。

  • 沙箱上传文件 (将它们存储在不同的服务器上,只允许通过不同的子域访问文件,或者通过完全不同的域名访问文件更好)。

10.4 结论

发送表单数据很容易,但要确保应用程序的安全性是很棘手的。
前端开发人员不是应该定义数据安全模型的人。
我们将看到,执行客户端数据验证 (en-US)是可能的,但是服务器不能信任这种验证,因为它无法真正知道客户端到底发生了什么。

The End

猜你喜欢

转载自blog.csdn.net/cxyhjl/article/details/132267925