Skip to content
This page has been auto-translated and may contain errors.View in English

表单与输入 ​

docs.scrimba.com

几乎每个能替你做点事的网站——搜索框、登录、结账、评论区——背后都是一个表单在支撑。表单让页面不再只是单向展示,而是开始"倾听"访问者:它收集别人输入、勾选或选择的内容,然后把这些数据交出去,让后面的程序去处理。这一章要讲的,就是让这一切成为可能的 HTML。

form 元素 ​

表单(form) 是一个容器,把访问者要填写的东西都装在一起。你把输入框和按钮包在一个 <form> 元素里,表单的任务就是在访问者填完之后,把所有内容收集起来一并发出去。

html
<form>
  <label>你的名字</label>
  <input>
  <button>发送</button>
</form>

可以把它想成你在诊所填的纸质表格:整张纸上有很多空格,你逐一填写,最后把整张纸一起交回去。<form> 元素就是那张纸。

<form> 是把它内部的控件收集起来、打包一起提交的容器。有两个属性决定了提交时会发生什么:action 是数据要发送到的地址,method 是发送的方式。

html
<form action="/subscribe" method="post">
  <label for="email">邮箱地址</label>
  <input id="email" name="email" type="email">
  <button type="submit">订阅</button>
</form>

当访问者触发提交时,浏览器会收集表单内所有带 name 的控件,把这些值打包,然后向 action 指定的地址发出请求。这一切都不需要你自己动手实现。<form> 元素免费给你提供了这套行为,这也是为什么把控件组织在一个真正的表单里,比把零散的输入框随意堆在页面上要好得多。

<form> 是浏览器内置的**提交(submission)**机制:浏览器会自动把表单的控件收集打包成一个请求并发出,全程不需要写脚本。提交时浏览器会读取 action(请求要发往的 URL)和 method(get 或 post,本章后面会讲到),把控件序列化进请求,然后进行页面跳转。正是这个默认行为,让一个表单即便在 JavaScript 被禁用或尚未加载完成时依然能用——这是你构建功能的起点,而不是你要替换掉的东西。

html
<form action="/subscribe" method="post">
  <label for="email">邮箱地址</label>
  <input id="email" name="email" type="email">
  <button type="submit">订阅</button>
</form>

有两个行为容易让人踩坑。第一,当焦点停留在单行文本框中时,按 Enter 键就会提交表单,不只是点按钮才会提交,所以键盘提交这个行为是内置的,值得专门测试一下。第二,默认的提交行为会重新加载或跳转页面;当你用 JavaScript 来处理表单时,需要调用 event.preventDefault() 来阻止这次跳转,自己接管后续流程。把原生提交当作最低保障:即使在任何脚本运行之前,它也应该能正常工作。渐进增强——在能正常运作的 HTML 之上再叠加脚本——正是要从这里起步的原因。

Junoform 元素<form> 是装着你的输入框和按钮的容器,当有人提交时,它会把所有内容一起发出去。想象一下桌上的纸质表格:一张纸,很多格子,一次性交回去。把字段包在一个表单里,你就能获得这种"收集再发送"的行为,什么额外的事都不用做。
Junoform 元素<form> 把控件打包并提交;action 说明发到哪里,method 说明怎么发。收集和发送都是免费拿到的,所以应该用真正的表单,而不是零散的输入框加上自己写的点击处理逻辑。这样既省代码,又能在任何脚本加载之前就正常工作。
Junoform 元素 原生提交会收集控件并向 action 发出请求,全程不涉及脚本,这正是为什么在 JavaScript 关闭时表单依然能用。记住它在文本框里按 Enter 也会提交,不只是靠点击。当你确实要用 JavaScript 接管时,event.preventDefault() 会阻止默认的页面跳转,而底下那个普通表单依然是你的安全网。

输入框与标签 ​

<input> 是访问者输入内容的框。但单独放一个输入框,它就只是个空盒子,谁也不知道它是干什么用的。这正是**标签(label)**的作用:它是那段文字,告诉访问者这个框里该填什么。

html
<label for="city">城市</label>
<input id="city">

每个输入框都需要一个标签。把两者关联起来的方法是:给输入框一个 id,然后用 for 让标签指向它,两边用同一个词。关联好之后,点击标签就会把光标定位到输入框里,这个小细节能让表单对每个人都更好用。

<input> 收集单个值;<label> 给它命名。你通过让标签的 for 属性匹配输入框的 id 来把两者关联起来:

html
<label for="username">用户名</label>
<input id="username" name="username" type="text" required>

有几个属性在大多数输入框上都值得加上。name 是这个值提交时所用的键名(命名那一节会讲到)。required 会阻止表单提交,直到字段被填写。placeholder 会在框内显示一段淡色的提示文字,但占位符不是标签:一旦有人开始输入,它就会消失,所以它不能替代真正的标签。每个输入框依然需要自己的 <label>。

把 <label> 和它对应的控件关联起来有两种方式,这个区别对无障碍访问很重要。第一种是显式关联:标签带一个 for 属性,其值等于控件的 id。第二种是隐式关联:把控件直接包在标签元素里面,这样就不需要 for 或 id。

html
<!-- 显式关联:for 匹配 id -->
<label for="phone">电话</label>
<input id="phone" name="phone" type="tel">

<!-- 隐式关联:input 放在 label 内部 -->
<label>
  电话
  <input name="phone" type="tel">
</label>

两种方式都会给控件赋予一个**无障碍名称(accessible name)**——也就是当字段获得焦点时屏幕阅读器会读出的文字,这样看不到页面布局的访问者也能知道该输入什么。更推荐用显式关联:即便 CSS 布局把标签挪到远离输入框的位置,它依然有效;样式把两者强行分开时它照样能工作。没有关联标签的字段,会被读作一个没有任何用途的空白编辑框,这是网络上最常见的无障碍问题之一。无障碍这一章会更深入地讲这个话题。

Juno输入框与标签<input> 是给人输入的框,<label> 是告诉他们该填什么的文字。用匹配的 for 和 id 把它们关联起来,点击标签就能直接跳到输入框。给每一个输入框都配一个标签;一个孤零零、旁边没有任何文字说明的框只会让人困惑。
Juno输入框与标签 让标签的 for 匹配输入框的 id,两者就关联上了。可以用 required 来阻止空提交,但别让 placeholder 冒充标签:有人一开始输入,它立刻就消失了。真正的标签,每个字段都要有,没有例外。
Juno输入框与标签 关联标签有两种方式:for 匹配 id,或者把 input 包在 label 里面。两者都会给字段一个无障碍名称,也就是屏幕阅读器会读出的文字,而当 CSS 把两者拉开时,显式的 for/id 更稳妥。没有标签的字段会被读成一个没有名字的空框,这是最常见的无障碍疏漏之一。

其他控件 ​

不是所有内容都要用纯文本框来问。有些问题更适合用打勾、选择或从列表中挑选的方式来回答。勾选框(checkbox)是一个开/关的开关,下拉菜单则让人从多个选项中选一个。

html
<label>
  <input type="checkbox"> 给我发送订阅邮件
</label>

<label for="size">尺码</label>
<select id="size">
  <option>小</option>
  <option>中</option>
  <option>大</option>
</select>

这些不用死记硬背。现在你只需要知道:表单可以用最适合的方式来提问——输入文字、打勾,或者从列表里选。

<input> 元素会根据 type 属性变换形态,选对类型能让你免费获得更合适的键盘、内置校验和原生的选择器:

html
<input type="email">     <!-- 校验 @ 格式,手机上会弹出邮箱专用键盘 -->
<input type="number">    <!-- 数字键盘,带上下调整按钮 -->
<input type="password">  <!-- 遮盖输入的字符 -->
<input type="date">      <!-- 原生日期选择器 -->
<input type="checkbox">  <!-- 单个开/关开关 -->
<input type="radio">     <!-- 从一组选项中选一个 -->

除了 <input>,还有三个元素能覆盖剩下的场景。<textarea> 是用来写较长内容的多行文本框。<select> 配上 <option> 子元素就是下拉列表。当你需要把一组相关字段在视觉和语义上组合在一起时,就把它们包在带有 <legend>(给这组内容命名)的 <fieldset> 里:

html
<fieldset>
  <legend>配送速度</legend>
  <label><input type="radio" name="speed" value="standard"> 标准配送</label>
  <label><input type="radio" name="speed" value="express"> 加急配送</label>
</fieldset>

<label for="message">留言</label>
<textarea id="message" name="message" rows="4"></textarea>

按钮也有 type。type="submit" 会提交表单(表单内按钮的默认类型),type="reset" 会把所有字段清空重置为初始值,type="button" 本身什么都不做,是留给 JavaScript 挂钩用的。

每种控件类型在上线之前都有一些值得了解的行为。单选按钮(radio)靠共同的 name 分组:同一组里一次只能有一个被选中,正是这种分组让它们互相排斥,所以 name 缺失或不一致会悄无声息地把一组单选按钮拆成互不相关的独立开关。

html
<fieldset>
  <legend>配送速度</legend>
  <label><input type="radio" name="speed" value="standard" checked> 标准配送</label>
  <label><input type="radio" name="speed" value="express"> 加急配送</label>
</fieldset>

<fieldset> 和 <legend> 不只是视觉效果。屏幕阅读器会把 <legend> 作为该组内每个控件的上下文读出来,所以"配送速度"这个分组里的单选按钮会被读成"配送速度,标准配送"——这就是一个能听懂的表单和一堆孤零零选项之间的区别。还有两个实战中的要点。复选框只有在勾选时才会发送它的值;未勾选的框会完全不出现在提交内容里,所以服务器那边看到的是"这个字段不存在",而不是"值为 false",这会影响你在服务器端读取数据的方式。另外,<button> 在表单内的默认类型就是 type="submit",所以一个你本来只想当作 JavaScript 触发器用的普通 <button>,如果没有设置 type="button",就会提交表单并刷新页面。这个默认行为经常让人栽跟头。

Juno其他控件 表单不止一种提问方式:勾选框用来回答"是或否",下拉菜单用来从列表里选一个。checkbox 就是 <input type="checkbox">,下拉菜单就是包着若干 <option> 选项的 <select>。不用死记,只要知道当问题需要时,表单不止有纯文本框这一种手段。
Juno其他控件 input 的 type 会改变它的形态:email、number、date、password、checkbox、radio,每种都配有合适的键盘或选择器。长文本用 <textarea>,下拉选择用 <select>,要把相关字段分组就用带 <legend> 的 <fieldset>。留意按钮的类型:在表单里,submit 是默认值。
Juno其他控件 单选按钮靠共同的 name 分组,<legend> 会给 fieldset 里每个控件提供可被读出的上下文,所以它是结构,不是装饰。两个坑:未勾选的复选框根本不会发送任何值,不是发送 false;而一个普通的 <button> 默认就是 type="submit",如果不设置 type="button",它就会刷新页面。

表单数据是如何命名和提交的 ​

表单被发送时,每个答案都需要一个标记,好让接收方知道它来自哪个框。这个标记就是输入框的 name。你设置一次,它就会跟着这个值一起传递出去。

html
<label for="city">城市</label>
<input id="city" name="city">

如果访问者在那个框里输入"北京",表单发送的就是"city 是北京"这样一对信息。name 就是接收方用来区分不同答案的依据。没有 name 的输入框会被完全排除在提交内容之外,所以正是这个不起眼的属性,才让数据真正能显示出来。

表单数据会以一组**名值对(name/value pairs)**的形式发送:每个控件的 name 成为键,访问者输入的内容成为值。只有带 name 的控件才会被包含在内,所以正是 name 把一个控件变成了可提交的数据。

html
<form action="/search" method="get">
  <label for="q">搜索</label>
  <input id="q" name="q" type="search">
  <label for="sort">排序方式</label>
  <select id="sort" name="sort">
    <option value="recent">最新</option>
    <option value="top">评分最高</option>
  </select>
  <button type="submit">搜索</button>
</form>

method 属性决定这些名值对如何传输。method="get" 会把它们拼接到 URL 末尾作为查询字符串(/search?q=boots&sort=recent),适合那些你可能想收藏或分享的搜索、筛选场景。method="post" 会把它们放进请求体里,不会显示出来,适合任何会改变数据、或者不该出现在 URL 里的内容,比如密码或支付信息。

每次提交都是一组名值对,而三个细节决定了它们的编码方式和发送方式。

第一个是 method。get 会把这些名值对序列化进 URL,作为查询字符串——也就是地址后面那段 ?key=value&key=value 文本。这让这个请求变得可重复、可收藏,但也意味着这些值会暴露在 URL、浏览器历史记录和服务器日志里,所以 get 只适合读取操作(搜索、筛选),绝不能用来传递敏感信息。post 则把这些名值对放进请求体,不放在 URL 里,这才是任何会改变状态或涉及敏感信息的操作应该用的方式。

第二个是 enctype:也就是说明请求体格式的编码方式。默认值 application/x-www-form-urlencoded 会把这些名值对打包成一串 key=value&... 的文本,对纯文本来说没问题。如果要发送用户选中的文件,就必须把 enctype 切换成 multipart/form-data,这种格式才能同时携带文件字节和文本字段;没有它,文件输入框是没法正确上传的。

html
<form action="/upload" method="post" enctype="multipart/form-data">
  <label for="avatar">头像</label>
  <input id="avatar" name="avatar" type="file">
  <button type="submit">上传</button>
</form>

第三个是 autocomplete,这也是命名工作在用户体验和无障碍访问上真正回报你的地方。autocomplete 的取值会用标准化的方式告诉浏览器这个字段的含义,让浏览器能填入正确的已保存值:比如 autocomplete="email"、autocomplete="name"、autocomplete="current-password"、autocomplete="street-address"。这些取值是固定词汇表,不是随便写的文本,把它们设置正确,能让浏览器或密码管理器一键填完整个表单,这对用手机输入或使用辅助技术的人来说尤其重要。

html
<label for="email">邮箱</label>
<input id="email" name="email" type="email" autocomplete="email">

保证内置校验的可靠性是另一件事,会在表单校验这一章讲到;这一节讲的是数据在校验通过之后,是怎样被组织和发送的。

Juno表单数据是如何命名和提交的 每个输入框的 name 就是它答案上的标签,这样接收方才知道某个值来自城市框还是邮箱框。设置好 name,框里的值就会作为一个带标签的数据对被发送出去。忘了设置,这个字段就会悄悄被漏掉,很多人第一次遇到表单什么都没送回来时,就是栽在这里。
Juno表单数据是如何命名和提交的 提交的数据就是一组名值对,只有带 name 的控件才会被包含进去。根据任务选择 method:get 把值放进 URL,适合你可能想收藏的搜索和筛选场景;post 把值藏在请求体里,适合任何会改变数据或涉及敏感信息的场景。把密码放进 URL 是绝对不能犯的错误。
Juno表单数据是如何命名和提交的get 会把名值对写进 URL,所以只适合读取操作,绝不能用来传敏感信息;post 把它们放进请求体。要上传文件就得把 enctype 切换成 multipart/form-data,否则文件输入框上传不了。也值得花点精力设置 autocomplete 的取值,比如 email 和 current-password:它们是固定词汇表,设置正确后手机或密码管理器就能一键填完表单。