Day-12-表单处理

Day 12: 表单处理

DOM 操作 – 第3天

🎯 学习目标

  • 理解 HTML 表单的基本结构
  • 掌握表单元素的访问和操作
  • 学会获取和验证用户输入
  • 实现表单数据的提交和处理

💡 核心概念

什么是表单?

表单(Form) 是网页上收集用户输入的主要方式,常见场景包括:

  • 用户登录/注册
  • 搜索框
  • 评论反馈
  • 在线问卷
  • 购物车结算

表单的基本结构

<form id="myForm" action="/submit" method="POST">
    <!-- 表单元素 -->
    <input type="text" name="username" placeholder="用户名">
    <input type="password" name="password" placeholder="密码">
    <button type="submit">提交</button>
</form>

重要属性

  • action: 表单提交的 URL
  • method: 提交方式(GET 或 POST)
  • id: 唯一标识符(用于 JavaScript 操作)

常见表单元素

元素 type 值 用途
<input> text 单行文本
<input> password 密码(隐藏输入)
<input> email 邮箱地址
<input> number 数字
<input> checkbox 复选框
<input> radio 单选按钮
<select> 下拉列表
<textarea> 多行文本
<button> submit 提交按钮

📝 DOM 操作:访问表单元素

方法 1:通过 ID 获取

// 获取单个元素
const usernameInput = document.getElementById('username');

// 获取值
const value = usernameInput.value;
console.log(value);

// 设置值
usernameInput.value = '新值';

方法 2:通过表单的 elements 集合

const form = document.getElementById('myForm');

// 通过 name 属性获取
const username = form.elements.username;
// 或
const username = form.elements['username'];

// 通过索引获取
const firstInput = form.elements[0];

方法 3:通过 querySelector

// 获取单个元素
const input = document.querySelector('#username');

// 获取特定类型的输入框
const textInputs = document.querySelectorAll('input[type="text"]');

// 获取所有选中的复选框
const checkedBoxes = document.querySelectorAll('input[type="checkbox"]:checked');

🎮 实战示例

示例 1:用户注册表单验证

<!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>
        body {
            font-family: Arial, sans-serif;
            max-width: 500px;
            margin: 50px auto;
            padding: 20px;
        }

        .form-group {
            margin-bottom: 15px;
        }

        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }

        input {
            width: 100%;
            padding: 8px;
            box-sizing: border-box;
            border: 1px solid #ddd;
            border-radius: 4px;
        }

        input:focus {
            outline: none;
            border-color: #4CAF50;
        }

        .error {
            color: red;
            font-size: 12px;
            margin-top: 5px;
            display: none;
        }

        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            width: 100%;
        }

        button:hover {
            background-color: #45a049;
        }

        .success {
            background-color: #d4edda;
            color: #155724;
            padding: 10px;
            border-radius: 4px;
            margin-top: 20px;
            display: none;
        }
    </style>
</head>
<body>
    <h2>用户注册</h2>

    <form id="registerForm">
        <div class="form-group">
            <label for="username">用户名:</label>
            <input type="text" id="username" name="username" placeholder="请输入用户名(3-15个字符)">
            <div class="error" id="usernameError">用户名长度必须在 3-15 个字符之间</div>
        </div>

        <div class="form-group">
            <label for="email">邮箱:</label>
            <input type="email" id="email" name="email" placeholder="请输入邮箱">
            <div class="error" id="emailError">请输入有效的邮箱地址</div>
        </div>

        <div class="form-group">
            <label for="password">密码:</label>
            <input type="password" id="password" name="password" placeholder="请输入密码(至少6个字符)">
            <div class="error" id="passwordError">密码长度必须至少 6 个字符</div>
        </div>

        <div class="form-group">
            <label for="confirmPassword">确认密码:</label>
            <input type="password" id="confirmPassword" name="confirmPassword" placeholder="请再次输入密码">
            <div class="error" id="confirmError">两次输入的密码不一致</div>
        </div>

        <div class="form-group">
            <label>
                <input type="checkbox" id="agree" name="agree">
                我同意用户协议
            </label>
            <div class="error" id="agreeError">请同意用户协议</div>
        </div>

        <button type="submit">注册</button>
    </form>

    <div class="success" id="successMessage">
        注册成功!欢迎加入我们。
    </div>

    <script>
        // 获取表单和元素
        const form = document.getElementById('registerForm');
        const usernameInput = document.getElementById('username');
        const emailInput = document.getElementById('email');
        const passwordInput = document.getElementById('password');
        const confirmPasswordInput = document.getElementById('confirmPassword');
        const agreeCheckbox = document.getElementById('agree');

        // 显示错误
        function showError(input, errorId) {
            const errorElement = document.getElementById(errorId);
            errorElement.style.display = 'block';
            input.style.borderColor = 'red';
        }

        // 隐藏错误
        function hideError(input, errorId) {
            const errorElement = document.getElementById(errorId);
            errorElement.style.display = 'none';
            input.style.borderColor = '#ddd';
        }

        // 验证用户名
        function validateUsername() {
            const username = usernameInput.value.trim();
            if (username.length < 3 || username.length > 15) {
                showError(usernameInput, 'usernameError');
                return false;
            }
            hideError(usernameInput, 'usernameError');
            return true;
        }

        // 验证邮箱
        function validateEmail() {
            const email = emailInput.value.trim();
            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
            if (!emailRegex.test(email)) {
                showError(emailInput, 'emailError');
                return false;
            }
            hideError(emailInput, 'emailError');
            return true;
        }

        // 验证密码
        function validatePassword() {
            const password = passwordInput.value;
            if (password.length < 6) {
                showError(passwordInput, 'passwordError');
                return false;
            }
            hideError(passwordInput, 'passwordError');
            return true;
        }

        // 验证确认密码
        function validateConfirmPassword() {
            const password = passwordInput.value;
            const confirmPassword = confirmPasswordInput.value;
            if (password !== confirmPassword) {
                showError(confirmPasswordInput, 'confirmError');
                return false;
            }
            hideError(confirmPasswordInput, 'confirmError');
            return true;
        }

        // 验证协议
        function validateAgree() {
            if (!agreeCheckbox.checked) {
                document.getElementById('agreeError').style.display = 'block';
                return false;
            }
            document.getElementById('agreeError').style.display = 'none';
            return true;
        }

        // 实时验证(用户输入时)
        usernameInput.addEventListener('input', validateUsername);
        emailInput.addEventListener('input', validateEmail);
        passwordInput.addEventListener('input', validatePassword);
        confirmPasswordInput.addEventListener('input', validateConfirmPassword);
        agreeCheckbox.addEventListener('change', validateAgree);

        // 表单提交处理
        form.addEventListener('submit', function(event) {
            // 阻止默认提交行为
            event.preventDefault();

            // 验证所有字段
            const isUsernameValid = validateUsername();
            const isEmailValid = validateEmail();
            const isPasswordValid = validatePassword();
            const isConfirmValid = validateConfirmPassword();
            const isAgreeValid = validateAgree();

            // 如果所有验证都通过
            if (isUsernameValid && isEmailValid && isPasswordValid && isConfirmValid && isAgreeValid) {
                // 收集表单数据
                const formData = {
                    username: usernameInput.value.trim(),
                    email: emailInput.value.trim(),
                    password: passwordInput.value
                };

                // 这里可以将数据发送到服务器
                console.log('注册数据:', formData);

                // 显示成功消息
                document.getElementById('successMessage').style.display = 'block';

                // 清空表单
                form.reset();

                // 3秒后隐藏成功消息
                setTimeout(() => {
                    document.getElementById('successMessage').style.display = 'none';
                }, 3000);
            }
        });
    </script>
</body>
</html>

示例 2:搜索框实时过滤

<!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>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }

        .search-box {
            margin-bottom: 20px;
        }

        input[type="text"] {
            width: 100%;
            padding: 10px;
            font-size: 16px;
            border: 2px solid #ddd;
            border-radius: 5px;
            box-sizing: border-box;
        }

        input[type="text"]:focus {
            outline: none;
            border-color: #4CAF50;
        }

        .item-list {
            list-style: none;
            padding: 0;
        }

        .item {
            padding: 15px;
            border-bottom: 1px solid #eee;
            cursor: pointer;
            transition: background-color 0.2s;
        }

        .item:hover {
            background-color: #f5f5f5;
        }

        .item.hidden {
            display: none;
        }

        .item-name {
            font-weight: bold;
            margin-bottom: 5px;
        }

        .item-desc {
            color: #666;
            font-size: 14px;
        }

        .no-results {
            text-align: center;
            color: #999;
            padding: 20px;
            display: none;
        }
    </style>
</head>
<body>
    <h2>实时搜索过滤</h2>

    <div class="search-box">
        <input type="text" id="searchInput" placeholder="搜索...">
    </div>

    <ul class="item-list" id="itemList">
        <li class="item">
            <div class="item-name">JavaScript</div>
            <div class="item-desc">一种动态编程语言,广泛用于网页开发</div>
        </li>
        <li class="item">
            <div class="item-name">Python</div>
            <div class="item-desc">简洁易学的编程语言,适合初学者</div>
        </li>
        <li class="item">
            <div class="item-name">Java</div>
            <div class="item-desc">跨平台的面向对象编程语言</div>
        </li>
        <li class="item">
            <div class="item-name">C++</div>
            <div class="item-desc">高性能的编程语言,用于系统开发</div>
        </li>
        <li class="item">
            <div class="item-name">HTML</div>
            <div class="item-desc">网页的标记语言,定义网页结构</div>
        </li>
        <li class="item">
            <div class="item-name">CSS</div>
            <div class="item-desc">样式表语言,控制网页外观</div>
        </li>
        <li class="item">
            <div class="item-name">React</div>
            <div class="item-desc">Facebook 开发的 JavaScript 库</div>
        </li>
        <li class="item">
            <div class="item-name">Vue.js</div>
            <div class="item-desc">渐进式 JavaScript 框架</div>
        </li>
    </ul>

    <div class="no-results" id="noResults">
        没有找到匹配的结果
    </div>

    <script>
        const searchInput = document.getElementById('searchInput');
        const items = document.querySelectorAll('.item');
        const noResults = document.getElementById('noResults');

        // 实时搜索
        searchInput.addEventListener('input', function() {
            const searchTerm = this.value.toLowerCase().trim();
            let visibleCount = 0;

            items.forEach(item => {
                const name = item.querySelector('.item-name').textContent.toLowerCase();
                const desc = item.querySelector('.item-desc').textContent.toLowerCase();

                // 检查名称或描述是否包含搜索词
                if (name.includes(searchTerm) || desc.includes(searchTerm)) {
                    item.classList.remove('hidden');
                    visibleCount++;
                } else {
                    item.classList.add('hidden');
                }
            });

            // 显示/隐藏"无结果"消息
            if (visibleCount === 0) {
                noResults.style.display = 'block';
            } else {
                noResults.style.display = 'none';
            }
        });
    </script>
</body>
</html>

示例 3:表单数据收集和序列化

<!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>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }

        .form-group {
            margin-bottom: 15px;
        }

        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }

        input, select, textarea {
            width: 100%;
            padding: 8px;
            box-sizing: border-box;
            border: 1px solid #ddd;
            border-radius: 4px;
        }

        textarea {
            min-height: 80px;
            resize: vertical;
        }

        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-right: 10px;
        }

        button:hover {
            background-color: #45a049;
        }

        .output {
            margin-top: 20px;
            padding: 15px;
            background-color: #f5f5f5;
            border-radius: 4px;
            white-space: pre-wrap;
            font-family: monospace;
        }
    </style>
</head>
<body>
    <h2>用户信息收集</h2>

    <form id="infoForm">
        <div class="form-group">
            <label for="name">姓名:</label>
            <input type="text" id="name" name="name" required>
        </div>

        <div class="form-group">
            <label for="age">年龄:</label>
            <input type="number" id="age" name="age" min="1" max="120">
        </div>

        <div class="form-group">
            <label for="gender">性别:</label>
            <select id="gender" name="gender">
                <option value="">请选择</option>
                <option value="male">男</option>
                <option value="female">女</option>
                <option value="other">其他</option>
            </select>
        </div>

        <div class="form-group">
            <label>兴趣:</label>
            <label>
                <input type="checkbox" name="hobbies" value="reading"> 阅读
            </label>
            <label>
                <input type="checkbox" name="hobbies" value="sports"> 运动
            </label>
            <label>
                <input type="checkbox" name="hobbies" value="music"> 音乐
            </label>
            <label>
                <input type="checkbox" name="hobbies" value="travel"> 旅行
            </label>
        </div>

        <div class="form-group">
            <label for="bio">个人简介:</label>
            <textarea id="bio" name="bio"></textarea>
        </div>

        <button type="submit">提交</button>
        <button type="button" id="showJsonBtn">显示 JSON</button>
        <button type="button" id="resetBtn">重置</button>
    </form>

    <div class="output" id="output"></div>

    <script>
        const form = document.getElementById('infoForm');
        const output = document.getElementById('output');
        const showJsonBtn = document.getElementById('showJsonBtn');
        const resetBtn = document.getElementById('resetBtn');

        // 方法 1:手动收集表单数据
        function collectFormData() {
            const data = {
                name: form.elements.name.value,
                age: parseInt(form.elements.age.value) || 0,
                gender: form.elements.gender.value,
                hobbies: [],
                bio: form.elements.bio.value
            };

            // 收集选中的复选框
            const checkboxes = form.querySelectorAll('input[name="hobbies"]:checked');
            checkboxes.forEach(cb => {
                data.hobbies.push(cb.value);
            });

            return data;
        }

        // 方法 2:使用 FormData
        function collectFormDataObject() {
            const formData = new FormData(form);
            const data = {};

            // 遍历 FormData
            for (let [key, value] of formData.entries()) {
                // 处理多选值(复选框)
                if (data[key]) {
                    if (Array.isArray(data[key])) {
                        data[key].push(value);
                    } else {
                        data[key] = [data[key], value];
                    }
                } else {
                    data[key] = value;
                }
            }

            return data;
        }

        // 显示输出
        function displayOutput(data, title = '表单数据') {
            output.textContent = `${title}:\n${JSON.stringify(data, null, 2)}`;
        }

        // 表单提交
        form.addEventListener('submit', function(event) {
            event.preventDefault();

            const data = collectFormData();
            displayOutput(data, '提交的数据');

            console.log('表单数据:', data);
        });

        // 显示 JSON 按钮
        showJsonBtn.addEventListener('click', function() {
            const data = collectFormDataObject();
            displayOutput(data, 'FormData 对象');
        });

        // 重置按钮
        resetBtn.addEventListener('click', function() {
            form.reset();
            output.textContent = '';
        });
    </script>
</body>
</html>

📊 表单验证技巧

1. 实时验证 vs 提交时验证

// 实时验证(用户输入时)
input.addEventListener('input', validate);

// 失去焦点验证
input.addEventListener('blur', validate);

// 提交时验证
form.addEventListener('submit', function(event) {
    if (!validate()) {
        event.preventDefault(); // 阻止提交
    }
});

2. 正则表达式验证

// 邮箱验证
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

// 手机号验证(中国)
const phoneRegex = /^1[3-9]\d{9}$/;

// 用户名验证(字母开头,允许字母数字下划线,4-16位)
const usernameRegex = /^[a-zA-Z][a-zA-Z0-9_]{3,15}$/;

// 密码验证(至少8位,包含字母和数字)
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;

// 使用示例
function validateEmail(email) {
    return emailRegex.test(email);
}

3. 自定义验证样式

// 添加验证样式
function setValid(input) {
    input.style.borderColor = '#4CAF50';
    input.classList.add('valid');
    input.classList.remove('invalid');
}

function setInvalid(input) {
    input.style.borderColor = '#f44336';
    input.classList.add('invalid');
    input.classList.remove('valid');
}

// CSS 样式
/*
input.valid {
    border-color: #4CAF50;
}

input.invalid {
    border-color: #f44336;
}

input.valid::after {
    content: '✓';
    color: #4CAF50;
}

input.invalid::after {
    content: '✗';
    color: #f44336;
}
*/

⚠️ 重要注意事项

1. 永远不要只依赖客户端验证

// ❌ 错误:只验证前端
form.addEventListener('submit', function(event) {
    if (isValid) {
        // 直接提交,不安全!
    }
});

// ✅ 正确:前端验证 + 后端验证
form.addEventListener('submit', function(event) {
    if (isValid) {
        // 发送到服务器进行验证
        fetch('/api/register', {
            method: 'POST',
            body: JSON.stringify(formData)
        }).then(response => {
            // 处理服务器验证结果
        });
    }
});

2. 防止 XSS 攻击

// ❌ 危险:直接显示用户输入
div.innerHTML = userInput;

// ✅ 安全:转义用户输入
function escapeHtml(text) {
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;'
    };
    return text.replace(/[&<>"']/g, m => map[m]);
}

div.textContent = userInput; // 或使用 textContent

3. 处理表单重置

// 重置表单时,也要重置自定义样式
resetBtn.addEventListener('click', function() {
    form.reset();
    // 重置验证状态
    document.querySelectorAll('.error').forEach(el => {
        el.style.display = 'none';
    });
    document.querySelectorAll('input').forEach(input => {
        input.style.borderColor = '#ddd';
    });
});

✍️ 练习任务

练习 1:登录表单(30分钟)

创建一个登录表单,包含用户名和密码字段,实现基本的验证。

要求

  • 用户名至少 3 个字符
  • 密码至少 6 个字符
  • 显示错误提示
  • 提交时验证所有字段

练习 2:问卷调查(1小时)

创建一个问卷调查表单,包含多种输入类型。

要求

  • 单选题(性别)
  • 多选题(兴趣)
  • 文本框(姓名)
  • 下拉选择(城市)
  • 文本域(建议)
  • 提交时验证必填项
  • 显示收集的数据

练习 3:购物车表单(2小时)

创建一个购物车结算表单。

要求

  • 商品列表(可勾选)
  • 数量选择
  • 配送信息表单
  • 支付方式选择
  • 实时计算总价
  • 提交前验证所有信息

🎓 今日挑战

挑战任务:创建一个完整的用户注册系统

功能要求

  • 实时验证用户名(检查用户名是否已存在,使用模拟数据)
  • 密码强度指示器
  • 邮箱验证(发送验证码倒计时)
  • 手机号验证(模拟发送验证码)
  • 同意协议必须勾选
  • 提交后显示加载动画
  • 模拟异步提交到服务器

评估标准

  • ✅ 所有验证正确实现
  • ✅ 用户体验良好(实时反馈)
  • ✅ 代码结构清晰
  • ✅ 界面美观

💡 常见问题

Q1: submit 事件和 click 事件有什么区别?

A:

  • submit:在 <form> 元素上,按回车键或点击提交按钮时触发
  • click:在按钮上,只有点击时触发

推荐:使用 submit 事件,因为它能捕获所有提交方式。

Q2: 如何禁用表单自动完成?

A:

<input type="text" autocomplete="off">
<!-- 或 -->
<form autocomplete="off">

Q3: FormData 对象有哪些优势?

A:

  • 自动处理多选值
  • 支持文件上传
  • 可以直接用于 fetch/XMLHttpRequest
  • 跨浏览器兼容性好
const formData = new FormData(form);
fetch('/api/submit', {
    method: 'POST',
    body: formData // 自动设置正确的 Content-Type
});

📚 拓展阅读

  • MDN 文档: HTML Forms
  • 相关章节:
    • Day 10: DOM 操作基础
    • Day 11: 事件处理
    • Day 13: CSS 基础

🔗 相关资源


学习时间: 2026-03-12
难度: ⭐⭐⭐☆☆
预计用时: 3-4 小时
关键词: 表单, 表单验证, DOM 操作, FormData
相关标签: #07-DOM操作 #JavaScript #前端开发

下一步: 学习 Day 13: CSS 基础