第2周-HTML表单与语义化进阶
第2周:HTML 表单与语义化进阶
本周目标:掌握表单体系,理解表单验证机制,能制作复杂的数据录入页面
核心思维:表单是前后端交互的桥梁——前端收集数据,后端处理数据
2.1 表单是什么?为什么重要?
概念讲解
你有没有想过,为什么每天都要填那么多表单?登录、注册、搜索、下单、评论……几乎所有需要"用户输入"的场景都是表单。
表单的本质:
表单 = 一组输入控件 + 一个提交按钮 + 一个目标地址
类比 Java:
┌─────────────────────────────────────┐
│ HTML 表单 │ ← 前端数据收集
│ <form action="/api/login"> │
│ <input name="username"> │ 类比 Java 的 DTO 对象:
│ <input name="password"> │ class LoginDTO {
│ <button type="submit">登录</button> String username;
│ </form> │ String password;
│ │ }
└─────────────────────────────────────┘
↓ 用户点击提交按钮
┌─────────────────────────────────────┐
│ HTTP 请求发送到后端 │ ← 网络传输
│ POST /api/login │ 类比 Spring Boot 的:
│ username=张三&password=123456 │ @PostMapping("/api/login")
│ │ public Result login(@RequestBody LoginDTO dto)
└─────────────────────────────────────┘
表单控件 = Java DTO 对象的字段:
<input name="username"> ↔ dto.username
<input name="password"> ↔ dto.password
<select name="role"> ↔ dto.role
<input name="hobby" type="checkbox" value="java"> ↔ dto.hobbies(数组)
2.2 表单基础结构
概念讲解
<form> 标签是所有表单控件的外层容器,它定义了三个关键信息:
| 属性 | 含义 | 类比 Java |
|---|---|---|
action | 数据提交到哪里(URL) | @PostMapping("/api/xxx") |
method | 用什么方式提交 | GET / POST / PUT / DELETE |
enctype | 数据的编码格式 | Content-Type 请求头 |
method 详解:
GET 方式:
- 表单数据拼在 URL 后面:/api/search?keyword=java&page=1
- 类比 Java 的 @GetMapping
- 适合:搜索、筛选、分页(不改变服务器数据的操作)
- 缺点:数据暴露在地址栏,有长度限制,不能传文件
POST 方式:
- 表单数据放在请求体(body)中
- 类比 Java 的 @PostMapping + @RequestBody
- 适合:登录、注册、提交订单(改变服务器数据的操作)
- 优点:数据安全,没有长度限制,可以传文件
enctype 详解:
| 值 | 含义 | 使用场景 |
|---|---|---|
application/x-www-form-urlencoded | 默认值,键值对格式 key=value&key2=value2 | 普通文本表单 |
multipart/form-data | 多部分格式,支持二进制数据 | 文件上传 |
text/plain | 纯文本(很少用) | — |
代码示例
<!-- 最简单的表单 -->
<form action="/api/login" method="POST">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<button type="submit">登录</button>
</form>
<!--
当用户点击"登录"按钮时:
1. 浏览器收集表单中所有控件的值
2. 按 name=value 的格式组装数据
3. 向 action 指定的地址发送 POST 请求
4. 后端接收参数,处理业务逻辑,返回结果
-->
<!-- 文件上传表单 -->
<form action="/api/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">上传</button>
</form>
<!--
注意:文件上传必须用 enctype="multipart/form-data"
类比 Java 后端的 @RequestParam("file") MultipartFile file
-->
2.3 label 标签——提升表单体验
概念讲解
<label> 标签用来给表单控件添加文字说明。它有一个非常重要的特性:点击 label 会自动聚焦到对应的输入框。
没有 label 的情况:
用户名:[输入框]
→ 用户必须精确地点击那个小小的输入框才能输入
有 label 的情况:
[用户名:] 点击这里也能聚焦到输入框
[_______]
→ 用户点击文字和点击输入框效果一样,体验更好
label 和输入框的关联方式有两种:
<!-- 方式一:用 for 属性关联(推荐) -->
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
<!--
for 的值 = input 的 id
点击"用户名"文字 → 光标自动聚焦到输入框
-->
<!-- 方式二:把 input 包在 label 里面 -->
<label>
用户名:
<input type="text" name="username">
</label>
最佳实践:每个表单控件都应该配合
<label>使用,这是无障碍访问的基本要求。
2.4 input 标签类型大全
概念讲解
<input> 是最常用的表单控件,通过 type 属性可以变成不同的输入方式。这是 HTML5 带来的巨大改进——以前所有输入框都是 type="text",现在浏览器可以根据类型提供更合适的键盘、验证和 UI。
后端程序员可以这样理解:
<input type="text"> ↔ private String name;
<input type="number"> ↔ private Integer age;
<input type="email"> ↔ @Email private String email;
<input type="date"> ↔ private LocalDate birthday;
<input type="checkbox"> ↔ private Boolean agreed;
<input type="file"> ↔ private MultipartFile file;
代码示例
<!-- ========== 文本输入类 ========== -->
<!-- text:普通文本框(最基础) -->
<label for="username">用户名:</label>
<input type="text" id="username" name="username"
placeholder="请输入用户名" maxlength="20" required>
<!--
placeholder:占位提示文字(灰色,输入后消失)
maxlength:最大输入长度
required:必填
-->
<!-- password:密码框(输入内容显示为圆点) -->
<label for="pwd">密码:</label>
<input type="password" id="pwd" name="password"
placeholder="请输入密码" minlength="6" required>
<!-- hidden:隐藏域(用户看不到,但提交时会发送) -->
<!--
类比 Java 中在请求参数里塞一个不展示给用户的 token
常用场景:CSRF token、记录 ID、版本号
-->
<input type="hidden" name="token" value="abc123xyz">
<!-- search:搜索框(和 text 类似,但有些浏览器会加清除按钮) -->
<input type="search" name="keyword" placeholder="搜索...">
<!-- email:邮箱输入(移动端会弹出带 @ 的键盘) -->
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" placeholder="your@email.com">
<!--
浏览器会自动验证邮箱格式
类比 Java 的 @Email 注解
-->
<!-- url:网址输入(移动端会弹出带 .com 的键盘) -->
<label for="website">个人网站:</label>
<input type="url" id="website" name="website" placeholder="https://">
<!-- tel:电话输入(移动端会弹出数字键盘) -->
<label for="phone">手机号:</label>
<input type="tel" id="phone" name="phone" placeholder="13800138000">
<!--
注意:tel 不会自动验证手机号格式
需要配合 pattern 属性手动验证
-->
<!-- ========== 数字类 ========== -->
<!-- number:数字输入框(有上下箭头) -->
<label for="age">年龄:</label>
<input type="number" id="age" name="age"
min="1" max="150" step="1" value="25">
<!--
min:最小值
max:最大值
step:步长(每次增减多少)
value:默认值
-->
<!-- range:滑动条 -->
<label for="volume">音量:</label>
<input type="range" id="volume" name="volume"
min="0" max="100" value="50">
<!--
常用于不需要精确数值的场景(音量、亮度、评分等)
不会显示具体数字,需要配合 JS 显示当前值
-->
<!-- ========== 日期时间类 ========== -->
<!-- date:日期选择器(日历弹窗) -->
<label for="birthday">出生日期:</label>
<input type="date" id="birthday" name="birthday">
<!--
类比 Java 的 LocalDate
值的格式:2026-05-07
-->
<!-- time:时间选择器 -->
<label for="alarm">闹钟时间:</label>
<input type="time" id="alarm" name="alarm">
<!-- datetime-local:日期+时间选择器 -->
<label for="meeting">会议时间:</label>
<input type="datetime-local" id="meeting" name="meeting">
<!--
类比 Java 的 LocalDateTime
-->
<!-- month:月份选择 -->
<input type="month" name="expireMonth">
<!-- week:周选择 -->
<input type="week" name="workWeek">
<!-- ========== 选择类 ========== -->
<!-- checkbox:复选框(多选) -->
<label>兴趣爱好:</label>
<input type="checkbox" name="hobby" value="java"> Java
<input type="checkbox" name="hobby" value="python"> Python
<input type="checkbox" name="hobby" value="javascript"> JavaScript
<input type="checkbox" name="hobby" value="go"> Go
<!--
name 相同的 checkbox 提交时会组成数组
类比 Java 的 private List<String> hobbies;
只选了 Java 和 Python 的话,提交数据是:hobby=java&hobby=python
-->
<!-- radio:单选框(互斥选择) -->
<label>性别:</label>
<input type="radio" name="gender" value="male"> 男
<input type="radio" name="gender" value="female"> 女
<!--
name 相同的 radio 会互斥(只能选一个)
这就像 Java 中枚举:Gender gender = Gender.MALE;
-->
<!-- color:颜色选择器 -->
<label for="themeColor">主题色:</label>
<input type="color" id="themeColor" name="themeColor" value="#1890ff">
<!--
点击会弹出系统颜色选择器
值是十六进制颜色值
-->
<!-- ========== 文件上传 ========== -->
<!-- 单文件上传 -->
<label for="avatar">头像:</label>
<input type="file" id="avatar" name="avatar" accept="image/*">
<!--
accept 限制可选文件类型:
accept="image/*" → 所有图片
accept=".jpg,.png,.gif" → 指定扩展名
accept=".pdf" → 只能选 PDF
类比 Java 的 MultipartFile 接口
-->
<!-- 多文件上传 -->
<label for="files">附件:</label>
<input type="file" id="files" name="files" multiple>
<!-- 加上 multiple 属性就可以选多个文件 -->
2.5 其他表单元素
概念讲解
除了 <input>,表单还有其他几种重要控件,每种都有特定的使用场景。
下拉选择(select)
<label for="city">城市:</label>
<select id="city" name="city">
<option value="">请选择城市</option>
<option value="bj">北京</option>
<option value="sh">上海</option>
<option value="gz" selected>广州</option> <!-- selected:默认选中 -->
<option value="sz">深圳</option>
<!-- 用 optgroup 分组 -->
<optgroup label="其他城市">
<option value="hz">杭州</option>
<option value="cd">成都</option>
<option value="wh">武汉</option>
</optgroup>
</select>
<!--
select 类比 Java 的枚举下拉框或字典表
value 是提交给后端的值("bj")
标签之间的文字是用户看到的文字("北京")
类比:
前端 <option value="bj">北京</option>
后端 public enum City { BEIJING("bj", "北京"), ... }
-->
多行文本框(textarea)
<label for="bio">个人简介:</label>
<textarea id="bio" name="bio" rows="5" cols="40"
placeholder="请输入个人简介(最多500字)"
maxlength="500"></textarea>
<!--
textarea 和 input 的区别:
- input 是单行输入
- textarea 是多行输入
- textarea 的默认值写在标签之间:<textarea>默认内容</textarea>
(不像 input 用 value 属性)
rows:可见行数
cols:可见列数(按字符宽度计算)
类比 Java 中的 String 类型——可以存很长很长的文本
-->
分组(fieldset + legend)
<form>
<!-- fieldset 把相关的表单控件分组 -->
<!-- legend 是分组的标题 -->
<fieldset>
<legend>基本信息</legend>
<label>姓名:<input type="text" name="name"></label><br><br>
<label>年龄:<input type="number" name="age" min="1" max="150"></label>
</fieldset>
<fieldset>
<legend>联系方式</legend>
<label>手机:<input type="tel" name="phone"></label><br><br>
<label>邮箱:<input type="email" name="email"></label>
</fieldset>
</form>
<!--
fieldset 会自动给分组加上一个边框
legend 显示在边框上方
类比 Java 中把相关的字段放在一个内部类里
-->
自动补全(datalist)
<label for="lang">编程语言:</label>
<input type="text" id="lang" name="language" list="lang-list">
<datalist id="lang-list">
<option value="Java">
<option value="JavaScript">
<option value="Python">
<option value="Go">
<option value="Rust">
<option value="TypeScript">
</datalist>
<!--
datalist 和 select 的区别:
- select:只能从列表里选(下拉框)
- datalist:既可以自己输入,也可以从建议列表里选
类比 Java:
- select → 枚举类型(只能取固定值)
- datalist → 自动补全(可以接受任意输入,但提供建议)
工作原理:
input 通过 list 属性关联到 datalist(通过 id)
用户输入时,浏览器会过滤匹配的选项显示出来
-->
按钮类型
<!-- submit:提交表单(默认值) -->
<button type="submit">提交</button>
<!-- 点击后会触发表单提交,数据发送到 action 地址 -->
<!-- reset:重置表单 -->
<button type="reset">重置</button>
<!-- 点击后所有输入框恢复默认值 -->
<!-- button:普通按钮(不提交) -->
<button type="button">点击我</button>
<!-- 不会触发表单提交,需要配合 JS 使用 -->
<!-- 实际开发中最常用!因为现代前端通常用 JS 发送请求,而不是表单原生提交 -->
<!-- 也可以用 input 做按钮 -->
<input type="submit" value="提交">
<input type="reset" value="重置">
<input type="button" value="普通按钮">
重要提示:现代前端开发中,表单的提交通常不用
<button type="submit">,而是用<button type="button">配合 JavaScript 的fetch来发送请求。因为原生提交会刷新整个页面,用户体验不好。
2.6 表单验证
概念讲解
表单验证是在用户提交数据之前,检查数据是否符合要求。这是前后端都需要做的事情:
前端验证(HTML5 + JavaScript):
→ 目的:提升用户体验,减少无效请求
→ 特点:即时反馈,用户填完马上就知道对不对
→ 类比:Java Swing 中的输入监听器
后端验证(@Valid + 业务校验):
→ 目的:保证数据安全性(前端验证可以被绕过!)
→ 特点:不能跳过,是最后一道防线
→ 类比:Service 层的业务校验
核心原则:前端验证永远不能替代后端验证!
就像你家里锁了门(前端验证),但小偷还是能翻窗户进来——
所以还需要警察(后端验证)来保证安全。
HTML5 内置验证
<form>
<!-- required:必填 -->
<label>用户名:</label>
<input type="text" name="username" required>
<!-- 不填就提交时,浏览器会弹出提示:"请填写此字段" -->
<!-- pattern:正则表达式验证 -->
<label>手机号:</label>
<input type="text" name="phone"
pattern="^1[3-9]\d{9}$"
title="请输入11位手机号">
<!--
pattern 接受一个正则表达式
类比 Java 的 @Pattern 注解:
@Pattern(regexp = "^1[3-9]\\d{9}$")
private String phone;
常用正则:
手机号:^1[3-9]\d{9}$
邮箱:^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
身份证:^\d{17}[\dXx]$
用户名:^[a-zA-Z0-9_]{4,16}$
-->
<!-- min / max:数值范围 -->
<label>年龄:</label>
<input type="number" name="age" min="1" max="150">
<!-- 类比 Java 的 @Min(1) @Max(150) -->
<!-- minlength / maxlength:长度范围 -->
<label>密码:</label>
<input type="password" name="password"
minlength="6" maxlength="20"
title="密码长度为6-20位">
<!-- 类比 Java 的 @Size(min=6, max=20) -->
<!-- min / max:日期范围 -->
<label>出生日期:</label>
<input type="date" name="birthday" max="2010-01-01">
<!-- 限制只能选2010年之前的日期 -->
<!-- 表单级验证 -->
<!-- novalidate:禁用整个表单的浏览器验证 -->
<!-- <form novalidate> -->
<!-- 场景:你想完全用 JS 自定义验证逻辑 -->
<!-- 自定义错误信息 -->
<label>邮箱:</label>
<input type="email" name="email" id="emailInput"
required
oninvalid="this.setCustomValidity('请输入正确的邮箱格式,如 zhangsan@qq.com')"
oninput="this.setCustomValidity('')">
<!--
setCustomValidity 可以自定义浏览器默认的验证提示文字
oninvalid:验证失败时触发
oninput:输入时清除自定义错误(否则一旦报错就一直报)
-->
</form>
前后端验证对照表
| 验证规则 | HTML5 前端 | Java 后端 |
|---|---|---|
| 必填 | required | @NotNull / @NotBlank |
| 邮箱格式 | type="email" | @Email |
| 数值范围 | min="" max="" | @Min() @Max() |
| 长度范围 | minlength="" maxlength="" | @Size(min=, max=) |
| 正则验证 | pattern="" | @Pattern(regexp="") |
2.7 实战:用户注册表单
概念讲解
把本周学到的所有知识综合起来,制作一个完整的用户注册表单。
完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户注册</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, "Microsoft YaHei", sans-serif;
background: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.register-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 40px;
width: 500px;
max-width: 100%;
}
.register-card h1 {
text-align: center;
margin-bottom: 30px;
color: #333;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #555;
}
.form-group label .required {
color: #ff4d4f;
margin-left: 2px;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24,144,255,0.2);
}
.radio-group, .checkbox-group {
display: flex;
gap: 20px;
padding: 8px 0;
}
.radio-group label, .checkbox-group label {
display: flex;
align-items: center;
gap: 6px;
font-weight: normal;
}
.form-group input[type="radio"],
.form-group input[type="checkbox"] {
width: auto;
}
.btn-submit {
width: 100%;
padding: 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
.btn-submit:hover {
background: #40a9ff;
}
.form-group .hint {
font-size: 12px;
color: #999;
margin-top: 4px;
}
</style>
</head>
<body>
<div class="register-card">
<h1>用户注册</h1>
<form action="/api/register" method="POST" id="registerForm">
<!-- 基本信息 -->
<fieldset>
<legend>基本信息</legend>
<div class="form-group">
<label for="username">用户名 <span class="required">*</span></label>
<input type="text" id="username" name="username"
placeholder="4-16位字母、数字或下划线"
required minlength="4" maxlength="16"
pattern="^[a-zA-Z0-9_]{4,16}$"
title="用户名只能包含字母、数字和下划线">
<div class="hint">4-16位字母、数字或下划线</div>
</div>
<div class="form-group">
<label for="email">邮箱 <span class="required">*</span></label>
<input type="email" id="email" name="email"
placeholder="your@email.com" required>
</div>
<div class="form-group">
<label for="password">密码 <span class="required">*</span></label>
<input type="password" id="password" name="password"
placeholder="6-20位,至少包含字母和数字"
required minlength="6" maxlength="20">
<div class="hint">6-20位,至少包含字母和数字</div>
</div>
<div class="form-group">
<label for="confirmPassword">确认密码 <span class="required">*</span></label>
<input type="password" id="confirmPassword" name="confirmPassword"
placeholder="请再次输入密码" required>
</div>
</fieldset>
<!-- 个人信息 -->
<fieldset>
<legend>个人信息</legend>
<div class="form-group">
<label>性别</label>
<div class="radio-group">
<label><input type="radio" name="gender" value="male"> 男</label>
<label><input type="radio" name="gender" value="female"> 女</label>
</div>
</div>
<div class="form-group">
<label for="birthday">出生日期</label>
<input type="date" id="birthday" name="birthday" max="2010-01-01">
</div>
<div class="form-group">
<label for="city">所在城市</label>
<select id="city" name="city">
<option value="">请选择</option>
<option value="bj">北京</option>
<option value="sh">上海</option>
<option value="gz">广州</option>
<option value="sz">深圳</option>
<option value="hz">杭州</option>
<option value="cd">成都</option>
</select>
</div>
<div class="form-group">
<label>兴趣爱好</label>
<div class="checkbox-group">
<label><input type="checkbox" name="hobby" value="coding"> 编程</label>
<label><input type="checkbox" name="hobby" value="reading"> 阅读</label>
<label><input type="checkbox" name="hobby" value="gaming"> 游戏</label>
<label><input type="checkbox" name="hobby" value="sports"> 运动</label>
<label><input type="checkbox" name="hobby" value="music"> 音乐</label>
</div>
</div>
<div class="form-group">
<label for="avatar">头像</label>
<input type="file" id="avatar" name="avatar" accept="image/*">
</div>
<div class="form-group">
<label for="bio">个人简介</label>
<textarea id="bio" name="bio" rows="3"
placeholder="简单介绍一下自己..." maxlength="200"></textarea>
<div class="hint">最多200字</div>
</div>
</fieldset>
<!-- 协议 -->
<div class="form-group">
<label>
<input type="checkbox" name="agree" value="true" required>
我已阅读并同意<a href="#">《用户服务协议》</a>
</label>
</div>
<button type="submit" class="btn-submit">立即注册</button>
</form>
</div>
</body>
</html>
2.8 表单数据如何在前后端传递?
概念讲解
这是后端程序员最关心的问题。前端填好的表单,到底是以什么格式发给后端的?
方式一:表单原生提交(传统方式)
<form action="/api/register" method="POST">
<input name="username" value="zhangsan">
<input name="age" value="25">
<button type="submit">提交</button>
</form>
浏览器会发送这样的请求:
POST /api/register HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=zhangsan&age=25
Java 后端接收:
// 方式A:用 @RequestParam 逐个接收
@PostMapping("/api/register")
public Result register(@RequestParam String username, @RequestParam Integer age)
// 方式B:用对象接收(推荐)
@PostMapping("/api/register")
public Result register(UserDTO dto) // dto.username 和 dto.age 会自动绑定
方式二:JavaScript 发送 JSON(现代方式)
// 前端代码
const formData = {
username: "zhangsan",
age: 25
};
fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData)
});
浏览器会发送:
POST /api/register HTTP/1.1
Content-Type: application/json
{"username":"zhangsan","age":25}
Java 后端接收:
@PostMapping("/api/register")
public Result register(@RequestBody UserDTO dto)
// 注意:用 JSON 提交时必须加 @RequestBody!
方式三:文件上传
POST /api/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
(二进制文件数据...)
------WebKitFormBoundary--
Java 后端接收:
@PostMapping("/api/upload")
public Result upload(@RequestParam("file") MultipartFile file)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)