第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)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐