pin_drop当前位置:知识文库 ❯ 图文

后端通信:14. 表单数据提交 - 从入门到实践详解

教程简介

表单是Web应用中最基本的数据收集和提交机制,也是HTML与后端通信最传统、最核心的方式之一。从最早的HTML表单原生提交,到现代的JavaScript异步提交,表单数据提交技术经历了巨大的演变。本教程将全面讲解表单数据提交的各种方式、数据编码格式、验证机制以及最佳实践,帮助你掌握从前端表单到后端处理的完整数据流。

无论你是构建简单的登录页面,还是复杂的多步骤表单,理解表单数据提交的底层原理和现代实践都是必不可少的。本教程将涵盖传统表单提交、Ajax表单提交、FormData API、数据编码格式、表单验证等核心内容。

核心概念

表单提交的基本流程

代码示例

用户填写表单 → 前端验证 → 数据编码 → 发送请求 → 后端处理 → 返回响应 → 前端反馈

表单数据编码格式

编码格式 enctype值 说明 适用场景
URL编码 application/x-www-form-urlencoded 默认格式,键值对以&分隔 普通文本字段
多部分表单 multipart/form-data 用于文件上传 包含文件的表单
纯文本 text/plain 空格转为+号,不编码特殊字符 调试用途
JSON application/json JSON格式数据 API交互

表单提交方式对比

提交方式 页面刷新 用户体验 文件上传 进度监控 实现复杂度
原生提交 是 较差 支持 不支持 低
iframe提交 否 一般 支持 不支持 中
XMLHttpRequest 否 好 支持 支持 中
Fetch API 否 好 支持 有限 低
Axios 否 好 支持 支持 低

语法与用法

HTML表单基本结构

代码示例

<form action="/api/submit" method="POST" enctype="application/x-www-form-urlencoded">
    <label for="username">用户名:</label>
    <input type="text" id="username" name="username" required>

    <label for="email">邮箱:</label>
    <input type="email" id="email" name="email" required>

    <button type="submit">提交</button>
</form>

form元素关键属性

属性 说明 示例
action 提交目标URL action="/api/users"
method HTTP方法 method="POST"
enctype 编码格式 enctype="multipart/form-data"
target 响应显示位置 target="_blank"
novalidate 禁用浏览器验证 novalidate
autocomplete 自动补全 autocomplete="off"

FormData API

代码示例

// 方式1:从表单元素创建
const form = document.getElementById('myForm');
const formData = new FormData(form);

// 方式2:手动创建
const formData = new FormData();
formData.append('username', 'zhangsan');
formData.append('email', 'zhangsan@example.com');

// 方式3:从表单创建后追加数据
const formData = new FormData(form);
formData.append('extraField', 'extraValue');

FormData常用方法

方法 说明 示例
append(name, value) 追加数据 formData.append('key', 'value')
set(name, value) 设置数据(覆盖) formData.set('key', 'value')
get(name) 获取第一个值 formData.get('key')
getAll(name) 获取所有值 formData.getAll('key')
has(name) 检查是否存在 formData.has('key')
delete(name) 删除数据 formData.delete('key')
entries() 遍历所有键值对 for(let [k,v] of formData.entries())
keys() 遍历所有键 for(let k of formData.keys())
values() 遍历所有值 for(let v of formData.values())

代码示例

示例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>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .form-container {
            background: white;
            border-radius: 16px;
            padding: 40px;
            width: 100%;
            max-width: 480px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
        }
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 30px;
            font-size: 24px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 6px;
            color: #555;
            font-weight: 500;
            font-size: 14px;
        }
        input, select, textarea {
            width: 100%;
            padding: 12px 16px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 15px;
            transition: border-color 0.3s, box-shadow 0.3s;
            outline: none;
        }
        input:focus, select:focus, textarea:focus {
            border-color: #667eea;
            box-shadow: 0 0 0 3px rgba(102,126,234,0.2);
        }
        input:invalid:not(:placeholder-shown) {
            border-color: #e74c3c;
        }
        input:valid:not(:placeholder-shown) {
            border-color: #2ecc71;
        }
        .submit-btn {
            width: 100%;
            padding: 14px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .submit-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 25px rgba(102,126,234,0.4);
        }
        .submit-btn:active {
            transform: translateY(0);
        }
        .info-box {
            background: #f0f4ff;
            border-left: 4px solid #667eea;
            padding: 12px 16px;
            border-radius: 4px;
            margin-bottom: 24px;
            font-size: 13px;
            color: #555;
            line-height: 1.6;
        }
        .method-select {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
        }
        .method-option {
            flex: 1;
            padding: 10px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s;
            font-weight: 500;
        }
        .method-option.active {
            border-color: #667eea;
            background: #f0f4ff;
            color: #667eea;
        }
    </style>
</head>
<body>
    <div class="form-container">
        <h1>传统表单提交演示</h1>
        <div class="info-box">
            传统表单提交会导致页面刷新跳转。本演示使用 target="result_frame" 将响应显示在iframe中,避免页面跳转。
        </div>

        <div class="method-select">
            <div class="method-option active" data-method="POST" onclick="switchMethod(this)">POST</div>
            <div class="method-option" data-method="GET" onclick="switchMethod(this)">GET</div>
        </div>

        <!-- 传统表单 - 使用iframe接收响应 -->
        <form id="traditionalForm"
              action="https://httpbin.org/post"
              method="POST"
              enctype="application/x-www-form-urlencoded"
              target="result_frame">
            <div class="form-group">
                <label for="name">姓名</label>
                <input type="text" id="name" name="name" placeholder="请输入姓名" required>
            </div>
            <div class="form-group">
                <label for="email">邮箱</label>
                <input type="email" id="email" name="email" placeholder="请输入邮箱" required>
            </div>
            <div class="form-group">
                <label for="age">年龄</label>
                <input type="number" id="age" name="age" min="1" max="150" placeholder="请输入年龄">
            </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 for="message">留言</label>
                <textarea id="message" name="message" rows="3" placeholder="请输入留言内容"></textarea>
            </div>
            <button type="submit" class="submit-btn">提交表单</button>
        </form>

        <div style="margin-top: 24px;">
            <h3 style="margin-bottom: 10px; color: #555; font-size: 14px;">服务器响应:</h3>
            <iframe name="result_frame" style="width:100%; height:200px; border:2px solid #e0e0e0; border-radius:8px; background:#f8f9fa;"></iframe>
        </div>
    </div>

    <script>
        function switchMethod(el) {
            // 切换方法选项样式
            document.querySelectorAll('.method-option').forEach(opt => opt.classList.remove('active'));
            el.classList.add('active');

            // 更新表单method和action
            const method = el.dataset.method;
            const form = document.getElementById('traditionalForm');
            form.method = method;

            if (method === 'GET') {
                form.action = 'https://httpbin.org/get';
            } else {
                form.action = 'https://httpbin.org/post';
            }
        }
    </script>
</body>
</html>

示例2:Ajax表单提交(XMLHttpRequest)

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ajax表单提交 - XMLHttpRequest</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: #f5f7fa;
            min-height: 100vh;
            padding: 40px 20px;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
        }
        h1 {
            text-align: center;
            color: #2c3e50;
            margin-bottom: 30px;
        }
        .card {
            background: white;
            border-radius: 12px;
            padding: 30px;
            margin-bottom: 20px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.08);
        }
        .card h2 {
            font-size: 18px;
            color: #2c3e50;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 2px solid #eee;
        }
        .form-group {
            margin-bottom: 16px;
        }
        label {
            display: block;
            margin-bottom: 6px;
            color: #555;
            font-weight: 500;
            font-size: 14px;
        }
        input, textarea {
            width: 100%;
            padding: 10px 14px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            transition: border-color 0.3s;
            outline: none;
        }
        input:focus, textarea:focus {
            border-color: #3498db;
        }
        .btn-group {
            display: flex;
            gap: 10px;
            margin-top: 20px;
        }
        button {
            flex: 1;
            padding: 12px 20px;
            border: none;
            border-radius: 8px;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s;
        }
        .btn-primary {
            background: #3498db;
            color: white;
        }
        .btn-primary:hover { background: #2980b9; }
        .btn-success {
            background: #2ecc71;
            color: white;
        }
        .btn-success:hover { background: #27ae60; }
        .btn-warning {
            background: #e67e22;
            color: white;
        }
        .btn-warning:hover { background: #d35400; }
        button:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }
        .result-area {
            background: #2c3e50;
            color: #2ecc71;
            padding: 16px;
            border-radius: 8px;
            font-family: 'Courier New', monospace;
            font-size: 13px;
            white-space: pre-wrap;
            word-break: break-all;
            max-height: 300px;
            overflow-y: auto;
            margin-top: 16px;
            display: none;
        }
        .result-area.show { display: block; }
        .loading {
            display: none;
            text-align: center;
            padding: 20px;
        }
        .loading.show { display: block; }
        .spinner {
            border: 3px solid #f3f3f3;
            border-top: 3px solid #3498db;
            border-radius: 50%;
            width: 30px;
            height: 30px;
            animation: spin 1s linear infinite;
            margin: 0 auto 10px;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        .progress-bar {
            width: 100%;
            height: 6px;
            background: #e0e0e0;
            border-radius: 3px;
            overflow: hidden;
            margin-top: 10px;
            display: none;
        }
        .progress-bar.show { display: block; }
        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #3498db, #2ecc71);
            border-radius: 3px;
            transition: width 0.3s;
            width: 0%;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Ajax表单提交 - XHR方式</h1>

        <!-- URL编码提交 -->
        <div class="card">
            <h2>方式1:URL编码提交 (application/x-www-form-urlencoded)</h2>
            <form id="urlEncodedForm">
                <div class="form-group">
                    <label>用户名</label>
                    <input type="text" name="username" placeholder="请输入用户名" required>
                </div>
                <div class="form-group">
                    <label>密码</label>
                    <input type="password" name="password" placeholder="请输入密码" required>
                </div>
                <div class="btn-group">
                    <button type="submit" class="btn-primary">URL编码提交</button>
                </div>
            </form>
            <div class="loading" id="loading1">
                <div class="spinner"></div>
                <span>提交中...</span>
            </div>
            <div class="result-area" id="result1"></div>
        </div>

        <!-- FormData提交 -->
        <div class="card">
            <h2>方式2:FormData提交 (multipart/form-data)</h2>
            <form id="formDataForm">
                <div class="form-group">
                    <label>姓名</label>
                    <input type="text" name="fullname" placeholder="请输入姓名" required>
                </div>
                <div class="form-group">
                    <label>头像</label>
                    <input type="file" name="avatar" accept="image/*">
                </div>
                <div class="form-group">
                    <label>个人简介</label>
                    <textarea name="bio" rows="3" placeholder="请输入个人简介"></textarea>
                </div>
                <div class="btn-group">
                    <button type="submit" class="btn-success">FormData提交</button>
                </div>
            </form>
            <div class="loading" id="loading2">
                <div class="spinner"></div>
                <span>提交中...</span>
            </div>
            <div class="progress-bar" id="progress2">
                <div class="progress-fill" id="progressFill2"></div>
            </div>
            <div class="result-area" id="result2"></div>
        </div>

        <!-- JSON提交 -->
        <div class="card">
            <h2>方式3:JSON提交 (application/json)</h2>
            <form id="jsonForm">
                <div class="form-group">
                    <label>产品名称</label>
                    <input type="text" name="productName" placeholder="请输入产品名称" required>
                </div>
                <div class="form-group">
                    <label>价格</label>
                    <input type="number" name="price" step="0.01" placeholder="请输入价格" required>
                </div>
                <div class="form-group">
                    <label>描述</label>
                    <textarea name="description" rows="2" placeholder="请输入描述"></textarea>
                </div>
                <div class="btn-group">
                    <button type="submit" class="btn-warning">JSON提交</button>
                </div>
            </form>
            <div class="loading" id="loading3">
                <div class="spinner"></div>
                <span>提交中...</span>
            </div>
            <div class="result-area" id="result3"></div>
        </div>
    </div>

    <script>
        // ===== 方式1:URL编码提交 =====
        document.getElementById('urlEncodedForm').addEventListener('submit', function(e) {
            e.preventDefault();

            const form = this;
            const loading = document.getElementById('loading1');
            const result = document.getElementById('result1');

            // 手动构建URL编码字符串
            const params = new URLSearchParams();
            params.append('username', form.username.value);
            params.append('password', form.password.value);

            loading.classList.add('show');
            result.classList.remove('show');

            const xhr = new XMLHttpRequest();
            xhr.open('POST', 'https://httpbin.org/post', true);
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

            xhr.onload = function() {
                loading.classList.remove('show');
                if (xhr.status === 200) {
                    const data = JSON.parse(xhr.responseText);
                    result.textContent = '提交成功!\n\n服务器收到的数据:\n' +
                        JSON.stringify(data.form, null, 2) +
                        '\n\nContent-Type: ' + data.headers['Content-Type'];
                    result.classList.add('show');
                } else {
                    result.textContent = '提交失败: ' + xhr.status;
                    result.classList.add('show');
                }
            };

            xhr.onerror = function() {
                loading.classList.remove('show');
                result.textContent = '网络错误';
                result.classList.add('show');
            };

            xhr.send(params.toString());
        });

        // ===== 方式2:FormData提交 =====
        document.getElementById('formDataForm').addEventListener('submit', function(e) {
            e.preventDefault();

            const form = this;
            const loading = document.getElementById('loading2');
            const result = document.getElementById('result2');
            const progressBar = document.getElementById('progress2');
            const progressFill = document.getElementById('progressFill2');

            // 使用FormData自动收集表单数据
            const formData = new FormData(form);

            loading.classList.add('show');
            progressBar.classList.add('show');
            result.classList.remove('show');

            const xhr = new XMLHttpRequest();
            xhr.open('POST', 'https://httpbin.org/post', true);
            // 注意:使用FormData时不要手动设置Content-Type
            // 浏览器会自动设置multipart/form-data并添加boundary

            // 监听上传进度
            xhr.upload.onprogress = function(e) {
                if (e.lengthComputable) {
                    const percent = Math.round((e.loaded / e.total) * 100);
                    progressFill.style.width = percent + '%';
                }
            };

            xhr.onload = function() {
                loading.classList.remove('show');
                if (xhr.status === 200) {
                    const data = JSON.parse(xhr.responseText);
                    result.textContent = '提交成功!\n\n服务器收到的表单数据:\n' +
                        JSON.stringify(data.form, null, 2) +
                        '\n\n上传的文件信息:\n' +
                        JSON.stringify(data.files, null, 2);
                    result.classList.add('show');
                }
            };

            xhr.onerror = function() {
                loading.classList.remove('show');
                result.textContent = '网络错误';
                result.classList.add('show');
            };

            xhr.send(formData);
        });

        // ===== 方式3:JSON提交 =====
        document.getElementById('jsonForm').addEventListener('submit', function(e) {
            e.preventDefault();

            const form = this;
            const loading = document.getElementById('loading3');
            const result = document.getElementById('result3');

            // 构建JSON对象
            const jsonData = {
                product: {
                    name: form.productName.value,
                    price: parseFloat(form.price.value),
                    description: form.description.value
                },
                timestamp: new Date().toISOString()
            };

            loading.classList.add('show');
            result.classList.remove('show');

            const xhr = new XMLHttpRequest();
            xhr.open('POST', 'https://httpbin.org/post', true);
            xhr.setRequestHeader('Content-Type', 'application/json');

            xhr.onload = function() {
                loading.classList.remove('show');
                if (xhr.status === 200) {
                    const data = JSON.parse(xhr.responseText);
                    result.textContent = '提交成功!\n\n发送的JSON数据:\n' +
                        JSON.stringify(jsonData, null, 2) +
                        '\n\n服务器解析的数据:\n' +
                        JSON.stringify(data.json, null, 2);
                    result.classList.add('show');
                }
            };

            xhr.onerror = function() {
                loading.classList.remove('show');
                result.textContent = '网络错误';
                result.classList.add('show');
            };

            xhr.send(JSON.stringify(jsonData));
        });
    </script>
</body>
</html>

示例3:Fetch API表单提交

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fetch API表单提交</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: #0f0f23;
            color: #e0e0e0;
            min-height: 100vh;
            padding: 40px 20px;
        }
        .container {
            max-width: 700px;
            margin: 0 auto;
        }
        h1 {
            text-align: center;
            background: linear-gradient(90deg, #ff6b6b, #feca57, #48dbfb, #ff9ff3);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            margin-bottom: 30px;
            font-size: 28px;
        }
        .card {
            background: #1a1a2e;
            border: 1px solid #2d2d44;
            border-radius: 12px;
            padding: 24px;
            margin-bottom: 20px;
        }
        .card h2 {
            font-size: 16px;
            color: #48dbfb;
            margin-bottom: 16px;
        }
        .form-row {
            display: flex;
            gap: 12px;
            margin-bottom: 14px;
        }
        .form-group {
            flex: 1;
        }
        .form-group label {
            display: block;
            font-size: 13px;
            color: #888;
            margin-bottom: 4px;
        }
        input, select, textarea {
            width: 100%;
            padding: 10px 12px;
            background: #16213e;
            border: 1px solid #2d2d44;
            border-radius: 6px;
            color: #e0e0e0;
            font-size: 14px;
            outline: none;
            transition: border-color 0.3s;
        }
        input:focus, select:focus, textarea:focus {
            border-color: #48dbfb;
        }
        .tag-group {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-bottom: 14px;
        }
        .tag {
            padding: 6px 14px;
            background: #16213e;
            border: 1px solid #2d2d44;
            border-radius: 20px;
            font-size: 13px;
            cursor: pointer;
            transition: all 0.3s;
            user-select: none;
        }
        .tag.selected {
            background: #48dbfb;
            color: #0f0f23;
            border-color: #48dbfb;
        }
        button {
            padding: 10px 24px;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s;
        }
        .btn-fetch {
            background: #48dbfb;
            color: #0f0f23;
        }
        .btn-fetch:hover { background: #0abde3; }
        .btn-json {
            background: #ff6b6b;
            color: white;
        }
        .btn-json:hover { background: #ee5a5a; }
        .btn-url {
            background: #feca57;
            color: #0f0f23;
        }
        .btn-url:hover { background: #f0b730; }
        .response-panel {
            background: #16213e;
            border: 1px solid #2d2d44;
            border-radius: 8px;
            padding: 16px;
            margin-top: 16px;
            font-family: 'Courier New', monospace;
            font-size: 12px;
            white-space: pre-wrap;
            word-break: break-all;
            max-height: 250px;
            overflow-y: auto;
            display: none;
        }
        .response-panel.show { display: block; }
        .status-badge {
            display: inline-block;
            padding: 2px 8px;
            border-radius: 4px;
            font-size: 12px;
            font-weight: 600;
        }
        .status-success { background: #2ecc71; color: white; }
        .status-error { background: #e74c3c; color: white; }
        .tabs {
            display: flex;
            gap: 4px;
            margin-bottom: 16px;
        }
        .tab {
            padding: 8px 16px;
            background: transparent;
            border: 1px solid #2d2d44;
            border-radius: 6px;
            color: #888;
            font-size: 13px;
            cursor: pointer;
        }
        .tab.active {
            background: #48dbfb;
            color: #0f0f23;
            border-color: #48dbfb;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Fetch API 表单提交</h1>

        <div class="card">
            <h2>用户注册表单</h2>
            <form id="registerForm">
                <div class="form-row">
                    <div class="form-group">
                        <label>用户名</label>
                        <input type="text" name="username" placeholder="请输入用户名" required>
                    </div>
                    <div class="form-group">
                        <label>邮箱</label>
                        <input type="email" name="email" placeholder="请输入邮箱" required>
                    </div>
                </div>
                <div class="form-row">
                    <div class="form-group">
                        <label>密码</label>
                        <input type="password" name="password" placeholder="请输入密码" required>
                    </div>
                    <div class="form-group">
                        <label>确认密码</label>
                        <input type="password" name="confirmPassword" placeholder="请再次输入密码" required>
                    </div>
                </div>
                <div class="form-group" style="margin-bottom:14px;">
                    <label>兴趣标签(点击选择)</label>
                    <div class="tag-group" id="tagGroup">
                        <span class="tag" data-value="frontend">前端开发</span>
                        <span class="tag" data-value="backend">后端开发</span>
                        <span class="tag" data-value="mobile">移动开发</span>
                        <span class="tag" data-value="ai">人工智能</span>
                        <span class="tag" data-value="devops">DevOps</span>
                        <span class="tag" data-value="design">设计</span>
                    </div>
                </div>
                <div class="form-group" style="margin-bottom:14px;">
                    <label>个人简介</label>
                    <textarea name="bio" rows="2" placeholder="介绍一下自己"></textarea>
                </div>

                <div style="display:flex; gap:10px; flex-wrap:wrap;">
                    <button type="submit" class="btn-fetch" data-format="formdata">FormData提交</button>
                    <button type="submit" class="btn-json" data-format="json">JSON提交</button>
                    <button type="submit" class="btn-url" data-format="urlencoded">URL编码提交</button>
                </div>
            </form>

            <div class="response-panel" id="responsePanel"></div>
        </div>

        <div class="card">
            <h2>请求详情对比</h2>
            <div class="tabs">
                <div class="tab active" data-tab="formdata" onclick="switchTab(this)">FormData</div>
                <div class="tab" data-tab="json" onclick="switchTab(this)">JSON</div>
                <div class="tab" data-tab="urlencoded" onclick="switchTab(this)">URL编码</div>
            </div>
            <div id="tabContent">
                <div id="tab-formdata" class="tab-panel">
                    <p style="color:#888; font-size:13px; line-height:1.8;">
                        <strong style="color:#48dbfb;">Content-Type:</strong> multipart/form-data; boundary=----WebKitFormBoundary...<br>
                        <strong style="color:#48dbfb;">特点:</strong><br>
                        - 浏览器自动设置Content-Type和boundary<br>
                        - 支持文件上传<br>
                        - 每个字段作为独立的部分传输<br>
                        - 适合包含二进制数据的表单<br>
                        <strong style="color:#ff6b6b;">请求体示例:</strong><br>
                        ------WebKitFormBoundary7MA4YWxkTrZu0gW<br>
                        Content-Disposition: form-data; name="username"<br>
                        <br>
                        zhangsan<br>
                        ------WebKitFormBoundary7MA4YWxkTrZu0gW<br>
                        Content-Disposition: form-data; name="avatar"; filename="photo.jpg"<br>
                        Content-Type: image/jpeg<br>
                        <br>
                        [二进制文件数据]<br>
                        ------WebKitFormBoundary7MA4YWxkTrZu0gW--
                    </p>
                </div>
                <div id="tab-json" class="tab-panel" style="display:none;">
                    <p style="color:#888; font-size:13px; line-height:1.8;">
                        <strong style="color:#48dbfb;">Content-Type:</strong> application/json<br>
                        <strong style="color:#48dbfb;">特点:</strong><br>
                        - 需要手动设置Content-Type<br>
                        - 数据序列化为JSON字符串<br>
                        - 不支持文件上传(需Base64编码)<br>
                        - 适合API交互,结构化数据<br>
                        - 后端解析方便<br>
                        <strong style="color:#ff6b6b;">请求体示例:</strong><br>
                        {<br>
                          "username": "zhangsan",<br>
                          "email": "zhangsan@example.com",<br>
                          "tags": ["frontend", "ai"]<br>
                        }
                    </p>
                </div>
                <div id="tab-urlencoded" class="tab-panel" style="display:none;">
                    <p style="color:#888; font-size:13px; line-height:1.8;">
                        <strong style="color:#48dbfb;">Content-Type:</strong> application/x-www-form-urlencoded<br>
                        <strong style="color:#48dbfb;">特点:</strong><br>
                        - HTML表单默认编码方式<br>
                        - 键值对以&连接,键和值用=连接<br>
                        - 特殊字符进行URL编码<br>
                        - 不支持文件上传<br>
                        - 适合简单的文本字段<br>
                        <strong style="color:#ff6b6b;">请求体示例:</strong><br>
                        username=zhangsan&email=zhangsan%40example.com&password=123456
                    </p>
                </div>
            </div>
        </div>
    </div>

    <script>
        // 标签选择
        const selectedTags = new Set();
        document.querySelectorAll('.tag').forEach(tag => {
            tag.addEventListener('click', function() {
                const value = this.dataset.value;
                if (selectedTags.has(value)) {
                    selectedTags.delete(value);
                    this.classList.remove('selected');
                } else {
                    selectedTags.add(value);
                    this.classList.add('selected');
                }
            });
        });

        // 表单提交
        let submitFormat = 'formdata';
        document.querySelectorAll('button[data-format]').forEach(btn => {
            btn.addEventListener('click', function(e) {
                e.preventDefault();
                submitFormat = this.dataset.format;
                submitForm();
            });
        });

        async function submitForm() {
            const form = document.getElementById('registerForm');
            const panel = document.getElementById('responsePanel');

            // 前端验证
            if (form.password.value !== form.confirmPassword.value) {
                panel.textContent = '验证失败:两次密码不一致!';
                panel.classList.add('show');
                return;
            }

            let fetchOptions = {};

            switch (submitFormat) {
                case 'formdata': {
                    const formData = new FormData(form);
                    // 移除不需要的字段
                    formData.delete('confirmPassword');
                    // 添加标签数据
                    selectedTags.forEach(tag => formData.append('tags', tag));

                    fetchOptions = {
                        method: 'POST',
                        body: formData
                        // 不设置Content-Type,让浏览器自动设置
                    };
                    break;
                }
                case 'json': {
                    const jsonData = {
                        username: form.username.value,
                        email: form.email.value,
                        password: form.password.value,
                        tags: Array.from(selectedTags),
                        bio: form.bio.value,
                        createdAt: new Date().toISOString()
                    };

                    fetchOptions = {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify(jsonData)
                    };
                    break;
                }
                case 'urlencoded': {
                    const params = new URLSearchParams();
                    params.append('username', form.username.value);
                    params.append('email', form.email.value);
                    params.append('password', form.password.value);
                    params.append('tags', Array.from(selectedTags).join(','));
                    params.append('bio', form.bio.value);

                    fetchOptions = {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/x-www-form-urlencoded'
                        },
                        body: params.toString()
                    };
                    break;
                }
            }

            try {
                panel.textContent = '请求中...';
                panel.classList.add('show');

                const response = await fetch('https://httpbin.org/post', fetchOptions);
                const data = await response.json();

                let output = `提交格式: ${submitFormat}\n`;
                output += `状态码: ${response.status}\n\n`;

                if (submitFormat === 'json') {
                    output += `服务器解析的JSON:\n${JSON.stringify(data.json, null, 2)}`;
                } else if (submitFormat === 'formdata') {
                    output += `表单数据:\n${JSON.stringify(data.form, null, 2)}`;
                    if (Object.keys(data.files).length > 0) {
                        output += `\n文件数据:\n${JSON.stringify(data.files, null, 2)}`;
                    }
                } else {
                    output += `表单数据:\n${JSON.stringify(data.form, null, 2)}`;
                }

                panel.textContent = output;
            } catch (error) {
                panel.textContent = `请求失败: ${error.message}`;
            }
        }

        // Tab切换
        function switchTab(el) {
            document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
            el.classList.add('active');

            document.querySelectorAll('.tab-panel').forEach(p => p.style.display = 'none');
            document.getElementById('tab-' + el.dataset.tab).style.display = 'block';
        }
    </script>
</body>
</html>

示例4:表单验证与数据序列化

代码示例

<!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>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: #fafafa;
            min-height: 100vh;
            padding: 40px 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
        }
        h1 {
            text-align: center;
            color: #1a1a2e;
            margin-bottom: 30px;
        }
        .main-layout {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        @media (max-width: 768px) {
            .main-layout { grid-template-columns: 1fr; }
        }
        .card {
            background: white;
            border-radius: 12px;
            padding: 24px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.06);
        }
        .card h2 {
            font-size: 16px;
            color: #333;
            margin-bottom: 16px;
        }
        .form-group {
            margin-bottom: 16px;
            position: relative;
        }
        label {
            display: block;
            font-size: 13px;
            color: #666;
            margin-bottom: 4px;
            font-weight: 500;
        }
        .required::after {
            content: ' *';
            color: #e74c3c;
        }
        input, select {
            width: 100%;
            padding: 10px 12px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            outline: none;
            transition: border-color 0.3s;
        }
        input:focus { border-color: #6c5ce7; }
        input.valid { border-color: #00b894; }
        input.invalid { border-color: #e74c3c; }
        .error-msg {
            font-size: 12px;
            color: #e74c3c;
            margin-top: 4px;
            display: none;
        }
        .error-msg.show { display: block; }
        .success-icon, .error-icon {
            position: absolute;
            right: 12px;
            top: 32px;
            font-size: 16px;
            display: none;
        }
        .success-icon.show { display: block; color: #00b894; }
        .error-icon.show { display: block; color: #e74c3c; }
        button {
            width: 100%;
            padding: 12px;
            background: #6c5ce7;
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 15px;
            font-weight: 600;
            cursor: pointer;
            transition: background 0.3s;
        }
        button:hover { background: #5a4bd1; }
        button:disabled { background: #b2bec3; cursor: not-allowed; }
        .serialized-data {
            background: #2d3436;
            color: #00b894;
            padding: 16px;
            border-radius: 8px;
            font-family: 'Courier New', monospace;
            font-size: 12px;
            white-space: pre-wrap;
            word-break: break-all;
            max-height: 400px;
            overflow-y: auto;
            line-height: 1.6;
        }
        .format-tabs {
            display: flex;
            gap: 4px;
            margin-bottom: 12px;
        }
        .format-tab {
            padding: 6px 12px;
            background: #f0f0f0;
            border: none;
            border-radius: 4px;
            font-size: 12px;
            cursor: pointer;
            color: #666;
        }
        .format-tab.active {
            background: #6c5ce7;
            color: white;
        }
        .validation-summary {
            padding: 12px;
            border-radius: 8px;
            margin-bottom: 16px;
            font-size: 13px;
            display: none;
        }
        .validation-summary.show { display: block; }
        .validation-summary.success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .validation-summary.error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>表单验证与数据序列化</h1>
        <div class="main-layout">
            <!-- 左侧:表单 -->
            <div class="card">
                <h2>注册表单(实时验证)</h2>
                <div class="validation-summary" id="validationSummary"></div>
                <form id="validatedForm" novalidate>
                    <div class="form-group">
                        <label class="required">用户名</label>
                        <input type="text" name="username"
                               data-rules="required|minLength:3|maxLength:20|pattern:^[a-zA-Z0-9_]+$"
                               placeholder="3-20位字母数字下划线">
                        <span class="success-icon">✓</span>
                        <span class="error-icon">✗</span>
                        <div class="error-msg"></div>
                    </div>
                    <div class="form-group">
                        <label class="required">邮箱</label>
                        <input type="email" name="email"
                               data-rules="required|email"
                               placeholder="请输入有效邮箱">
                        <span class="success-icon">✓</span>
                        <span class="error-icon">✗</span>
                        <div class="error-msg"></div>
                    </div>
                    <div class="form-group">
                        <label class="required">手机号</label>
                        <input type="tel" name="phone"
                               data-rules="required|pattern:^1[3-9]\\d{9}$"
                               placeholder="请输入11位手机号">
                        <span class="success-icon">✓</span>
                        <span class="error-icon">✗</span>
                        <div class="error-msg"></div>
                    </div>
                    <div class="form-group">
                        <label class="required">密码</label>
                        <input type="password" name="password"
                               data-rules="required|minLength:8|pattern:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$"
                               placeholder="至少8位,含大小写字母和数字">
                        <span class="success-icon">✓</span>
                        <span class="error-icon">✗</span>
                        <div class="error-msg"></div>
                    </div>
                    <div class="form-group">
                        <label class="required">确认密码</label>
                        <input type="password" name="confirmPassword"
                               data-rules="required|match:password"
                               placeholder="请再次输入密码">
                        <span class="success-icon">✓</span>
                        <span class="error-icon">✗</span>
                        <div class="error-msg"></div>
                    </div>
                    <div class="form-group">
                        <label>年龄</label>
                        <input type="number" name="age"
                               data-rules="min:1|max:150"
                               placeholder="请输入年龄">
                        <span class="success-icon">✓</span>
                        <span class="error-icon">✗</span>
                        <div class="error-msg"></div>
                    </div>
                    <button type="submit" id="submitBtn" disabled>提交表单</button>
                </form>
            </div>

            <!-- 右侧:序列化结果 -->
            <div class="card">
                <h2>数据序列化结果</h2>
                <div class="format-tabs">
                    <button class="format-tab active" data-format="json" onclick="switchFormat(this)">JSON</button>
                    <button class="format-tab" data-format="formdata" onclick="switchFormat(this)">FormData</button>
                    <button class="format-tab" data-format="urlencoded" onclick="switchFormat(this)">URL编码</button>
                    <button class="format-tab" data-format="querystring" onclick="switchFormat(this)">查询字符串</button>
                </div>
                <div class="serialized-data" id="serializedOutput">等待输入...</div>

                <h2 style="margin-top:20px;">验证规则说明</h2>
                <div style="font-size:13px; color:#666; line-height:1.8;">
                    <p><strong>required</strong> - 必填字段</p>
                    <p><strong>minLength:N</strong> - 最小长度</p>
                    <p><strong>maxLength:N</strong> - 最大长度</p>
                    <p><strong>min:N</strong> - 最小值</p>
                    <p><strong>max:N</strong> - 最大值</p>
                    <p><strong>pattern:REGEX</strong> - 正则匹配</p>
                    <p><strong>email</strong> - 邮箱格式</p>
                    <p><strong>match:FIELD</strong> - 与另一字段匹配</p>
                </div>
            </div>
        </div>
    </div>

    <script>
        // ===== 表单验证器 =====
        class FormValidator {
            constructor(form) {
                this.form = form;
                this.errors = {};
                this.init();
            }

            init() {
                // 实时验证
                this.form.querySelectorAll('input[data-rules]').forEach(input => {
                    input.addEventListener('input', () => {
                        this.validateField(input);
                        this.updateSerializedOutput();
                        this.updateSubmitButton();
                    });
                    input.addEventListener('blur', () => {
                        this.validateField(input);
                    });
                });

                this.form.addEventListener('submit', (e) => {
                    e.preventDefault();
                    if (this.validateAll()) {
                        this.showSummary('验证通过!准备提交数据...', 'success');
                        this.submitForm();
                    } else {
                        this.showSummary('请修正以上错误后重试', 'error');
                    }
                });
            }

            validateField(input) {
                const rules = input.dataset.rules.split('|');
                const value = input.value;
                const name = input.name;
                let errorMsg = '';

                for (const rule of rules) {
                    const [ruleName, ruleValue] = rule.split(':');
                    const result = this.checkRule(ruleName, ruleValue, value, name);
                    if (result) {
                        errorMsg = result;
                        break;
                    }
                }

                this.showFieldError(input, errorMsg);
                this.errors[name] = errorMsg;
                return !errorMsg;
            }

            checkRule(ruleName, ruleValue, value, fieldName) {
                switch (ruleName) {
                    case 'required':
                        return value.trim() ? '' : '此字段为必填项';
                    case 'minLength':
                        return value.length >= parseInt(ruleValue) ? '' : `最少${ruleValue}个字符`;
                    case 'maxLength':
                        return value.length <= parseInt(ruleValue) ? '' : `最多${ruleValue}个字符`;
                    case 'min':
                        return parseInt(value) >= parseInt(ruleValue) ? '' : `最小值为${ruleValue}`;
                    case 'max':
                        return parseInt(value) <= parseInt(ruleValue) ? '' : `最大值为${ruleValue}`;
                    case 'email':
                        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? '' : '请输入有效的邮箱地址';
                    case 'pattern':
                        return new RegExp(ruleValue).test(value) ? '' : '格式不正确';
                    case 'match':
                        const matchValue = this.form.querySelector(`[name="${ruleValue}"]`).value;
                        return value === matchValue ? '' : '两次输入不一致';
                    default:
                        return '';
                }
            }

            showFieldError(input, errorMsg) {
                const group = input.closest('.form-group');
                const errorEl = group.querySelector('.error-msg');
                const successIcon = group.querySelector('.success-icon');
                const errorIcon = group.querySelector('.error-icon');

                if (errorMsg) {
                    input.classList.add('invalid');
                    input.classList.remove('valid');
                    errorEl.textContent = errorMsg;
                    errorEl.classList.add('show');
                    successIcon.classList.remove('show');
                    errorIcon.classList.add('show');
                } else if (input.value) {
                    input.classList.add('valid');
                    input.classList.remove('invalid');
                    errorEl.classList.remove('show');
                    successIcon.classList.add('show');
                    errorIcon.classList.remove('show');
                } else {
                    input.classList.remove('valid', 'invalid');
                    errorEl.classList.remove('show');
                    successIcon.classList.remove('show');
                    errorIcon.classList.remove('show');
                }
            }

            validateAll() {
                let isValid = true;
                this.form.querySelectorAll('input[data-rules]').forEach(input => {
                    if (!this.validateField(input)) {
                        isValid = false;
                    }
                });
                return isValid;
            }

            updateSubmitButton() {
                const hasErrors = Object.values(this.errors).some(e => e);
                const hasEmpty = Array.from(this.form.querySelectorAll('input[data-rules*="required"]'))
                    .some(input => !input.value.trim());
                document.getElementById('submitBtn').disabled = hasErrors || hasEmpty;
            }

            showSummary(msg, type) {
                const summary = document.getElementById('validationSummary');
                summary.textContent = msg;
                summary.className = 'validation-summary show ' + type;
            }

            getFormData() {
                const data = {};
                const formData = new FormData(this.form);
                for (const [key, value] of formData.entries()) {
                    if (key !== 'confirmPassword') {
                        data[key] = value;
                    }
                }
                return data;
            }

            async submitForm() {
                const data = this.getFormData();
                try {
                    const response = await fetch('https://httpbin.org/post', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(data)
                    });
                    const result = await response.json();
                    console.log('提交成功:', result);
                } catch (error) {
                    console.error('提交失败:', error);
                }
            }
        }

        // ===== 数据序列化 =====
        let currentFormat = 'json';

        function updateSerializedOutput() {
            const form = document.getElementById('validatedForm');
            const output = document.getElementById('serializedOutput');
            const formData = new FormData(form);

            let result = '';
            switch (currentFormat) {
                case 'json': {
                    const obj = {};
                    for (const [key, value] of formData.entries()) {
                        if (key !== 'confirmPassword') {
                            obj[key] = value;
                        }
                    }
                    result = JSON.stringify(obj, null, 2);
                    break;
                }
                case 'formdata': {
                    const entries = [];
                    for (const [key, value] of formData.entries()) {
                        if (key !== 'confirmPassword') {
                            entries.push(`  "${key}": "${value}"`);
                        }
                    }
                    result = `FormData {\n${entries.join(',\n')}\n}`;
                    break;
                }
                case 'urlencoded': {
                    const params = new URLSearchParams();
                    for (const [key, value] of formData.entries()) {
                        if (key !== 'confirmPassword') {
                            params.append(key, value);
                        }
                    }
                    result = params.toString();
                    break;
                }
                case 'querystring': {
                    const parts = [];
                    for (const [key, value] of formData.entries()) {
                        if (key !== 'confirmPassword') {
                            parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
                        }
                    }
                    result = '?' + parts.join('&');
                    break;
                }
            }

            output.textContent = result || '等待输入...';
        }

        function switchFormat(el) {
            document.querySelectorAll('.format-tab').forEach(t => t.classList.remove('active'));
            el.classList.add('active');
            currentFormat = el.dataset.format;
            updateSerializedOutput();
        }

        // 初始化验证器
        const validator = new FormValidator(document.getElementById('validatedForm'));
    </script>
</body>
</html>

示例5:多步骤表单与数据管理

代码示例

<!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>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #a29bfe 0%, #6c5ce7 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .wizard {
            background: white;
            border-radius: 20px;
            width: 100%;
            max-width: 600px;
            overflow: hidden;
            box-shadow: 0 25px 80px rgba(0,0,0,0.2);
        }
        .progress-header {
            background: #f8f9fa;
            padding: 24px 30px;
            border-bottom: 1px solid #eee;
        }
        .steps-indicator {
            display: flex;
            justify-content: space-between;
            position: relative;
        }
        .steps-indicator::before {
            content: '';
            position: absolute;
            top: 20px;
            left: 30px;
            right: 30px;
            height: 2px;
            background: #e0e0e0;
        }
        .step-dot {
            display: flex;
            flex-direction: column;
            align-items: center;
            position: relative;
            z-index: 1;
        }
        .step-circle {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            background: #e0e0e0;
            color: #999;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: 600;
            font-size: 14px;
            transition: all 0.3s;
        }
        .step-dot.active .step-circle {
            background: #6c5ce7;
            color: white;
            transform: scale(1.1);
        }
        .step-dot.completed .step-circle {
            background: #00b894;
            color: white;
        }
        .step-label {
            font-size: 12px;
            color: #999;
            margin-top: 8px;
            font-weight: 500;
        }
        .step-dot.active .step-label { color: #6c5ce7; }
        .step-dot.completed .step-label { color: #00b894; }
        .step-content {
            padding: 30px;
            display: none;
        }
        .step-content.active { display: block; }
        .form-group {
            margin-bottom: 18px;
        }
        label {
            display: block;
            font-size: 14px;
            color: #555;
            margin-bottom: 6px;
            font-weight: 500;
        }
        input, select, textarea {
            width: 100%;
            padding: 12px 14px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            outline: none;
            transition: border-color 0.3s;
        }
        input:focus, select:focus, textarea:focus {
            border-color: #6c5ce7;
        }
        .checkbox-group {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }
        .checkbox-item {
            display: flex;
            align-items: center;
            gap: 6px;
            padding: 8px 14px;
            background: #f8f9fa;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.3s;
            font-size: 14px;
        }
        .checkbox-item.selected {
            background: #6c5ce7;
            color: white;
            border-color: #6c5ce7;
        }
        .checkbox-item input { display: none; }
        .actions {
            display: flex;
            justify-content: space-between;
            padding: 20px 30px;
            border-top: 1px solid #eee;
        }
        .btn {
            padding: 10px 24px;
            border: none;
            border-radius: 8px;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s;
        }
        .btn-prev {
            background: #f0f0f0;
            color: #666;
        }
        .btn-prev:hover { background: #e0e0e0; }
        .btn-next {
            background: #6c5ce7;
            color: white;
        }
        .btn-next:hover { background: #5a4bd1; }
        .btn-submit {
            background: #00b894;
            color: white;
        }
        .btn-submit:hover { background: #00a381; }
        .review-section {
            background: #f8f9fa;
            border-radius: 8px;
            padding: 16px;
            margin-bottom: 12px;
        }
        .review-section h3 {
            font-size: 14px;
            color: #6c5ce7;
            margin-bottom: 10px;
        }
        .review-item {
            display: flex;
            justify-content: space-between;
            padding: 6px 0;
            font-size: 13px;
            border-bottom: 1px solid #eee;
        }
        .review-item:last-child { border-bottom: none; }
        .review-label { color: #888; }
        .review-value { color: #333; font-weight: 500; }
        .success-overlay {
            display: none;
            text-align: center;
            padding: 60px 30px;
        }
        .success-overlay.show { display: block; }
        .success-icon {
            width: 80px;
            height: 80px;
            background: #00b894;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 0 auto 20px;
            font-size: 40px;
            color: white;
            animation: scaleIn 0.5s ease;
        }
        @keyframes scaleIn {
            from { transform: scale(0); }
            to { transform: scale(1); }
        }
        .success-overlay h2 {
            color: #333;
            margin-bottom: 10px;
        }
        .success-overlay p {
            color: #888;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="wizard">
        <div class="progress-header">
            <div class="steps-indicator">
                <div class="step-dot active" data-step="1">
                    <div class="step-circle">1</div>
                    <span class="step-label">基本信息</span>
                </div>
                <div class="step-dot" data-step="2">
                    <div class="step-circle">2</div>
                    <span class="step-label">联系方式</span>
                </div>
                <div class="step-dot" data-step="3">
                    <div class="step-circle">3</div>
                    <span class="step-label">偏好设置</span>
                </div>
                <div class="step-dot" data-step="4">
                    <div class="step-circle">4</div>
                    <span class="step-label">确认提交</span>
                </div>
            </div>
        </div>

        <!-- 步骤1:基本信息 -->
        <div class="step-content active" data-step="1">
            <h2 style="margin-bottom:20px; color:#333;">基本信息</h2>
            <div class="form-group">
                <label>姓名 *</label>
                <input type="text" name="name" placeholder="请输入姓名" required>
            </div>
            <div class="form-group">
                <label>性别</label>
                <select name="gender">
                    <option value="">请选择</option>
                    <option value="male">男</option>
                    <option value="female">女</option>
                </select>
            </div>
            <div class="form-group">
                <label>出生日期</label>
                <input type="date" name="birthday">
            </div>
        </div>

        <!-- 步骤2:联系方式 -->
        <div class="step-content" data-step="2">
            <h2 style="margin-bottom:20px; color:#333;">联系方式</h2>
            <div class="form-group">
                <label>邮箱 *</label>
                <input type="email" name="email" placeholder="请输入邮箱" required>
            </div>
            <div class="form-group">
                <label>手机号</label>
                <input type="tel" name="phone" placeholder="请输入手机号">
            </div>
            <div class="form-group">
                <label>地址</label>
                <textarea name="address" rows="2" placeholder="请输入详细地址"></textarea>
            </div>
        </div>

        <!-- 步骤3:偏好设置 -->
        <div class="step-content" data-step="3">
            <h2 style="margin-bottom:20px; color:#333;">偏好设置</h2>
            <div class="form-group">
                <label>兴趣领域</label>
                <div class="checkbox-group" id="interestsGroup">
                    <label class="checkbox-item">
                        <input type="checkbox" name="interests" value="tech"> 科技
                    </label>
                    <label class="checkbox-item">
                        <input type="checkbox" name="interests" value="sports"> 运动
                    </label>
                    <label class="checkbox-item">
                        <input type="checkbox" name="interests" value="music"> 音乐
                    </label>
                    <label class="checkbox-item">
                        <input type="checkbox" name="interests" value="travel"> 旅行
                    </label>
                    <label class="checkbox-item">
                        <input type="checkbox" name="interests" value="food"> 美食
                    </label>
                    <label class="checkbox-item">
                        <input type="checkbox" name="interests" value="reading"> 阅读
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label>通知方式</label>
                <select name="notification">
                    <option value="email">邮件通知</option>
                    <option value="sms">短信通知</option>
                    <option value="push">推送通知</option>
                    <option value="none">不通知</option>
                </select>
            </div>
        </div>

        <!-- 步骤4:确认提交 -->
        <div class="step-content" data-step="4">
            <h2 style="margin-bottom:20px; color:#333;">确认信息</h2>
            <div class="review-section">
                <h3>基本信息</h3>
                <div class="review-item">
                    <span class="review-label">姓名</span>
                    <span class="review-value" id="review-name">-</span>
                </div>
                <div class="review-item">
                    <span class="review-label">性别</span>
                    <span class="review-value" id="review-gender">-</span>
                </div>
                <div class="review-item">
                    <span class="review-label">出生日期</span>
                    <span class="review-value" id="review-birthday">-</span>
                </div>
            </div>
            <div class="review-section">
                <h3>联系方式</h3>
                <div class="review-item">
                    <span class="review-label">邮箱</span>
                    <span class="review-value" id="review-email">-</span>
                </div>
                <div class="review-item">
                    <span class="review-label">手机号</span>
                    <span class="review-value" id="review-phone">-</span>
                </div>
                <div class="review-item">
                    <span class="review-label">地址</span>
                    <span class="review-value" id="review-address">-</span>
                </div>
            </div>
            <div class="review-section">
                <h3>偏好设置</h3>
                <div class="review-item">
                    <span class="review-label">兴趣领域</span>
                    <span class="review-value" id="review-interests">-</span>
                </div>
                <div class="review-item">
                    <span class="review-label">通知方式</span>
                    <span class="review-value" id="review-notification">-</span>
                </div>
            </div>
        </div>

        <!-- 成功页面 -->
        <div class="success-overlay" id="successOverlay">
            <div class="success-icon">✓</div>
            <h2>提交成功!</h2>
            <p>您的信息已成功提交,我们会尽快与您联系。</p>
        </div>

        <div class="actions" id="actions">
            <button class="btn btn-prev" id="prevBtn" style="visibility:hidden" onclick="prevStep()">上一步</button>
            <button class="btn btn-next" id="nextBtn" onclick="nextStep()">下一步</button>
        </div>
    </div>

    <script>
        let currentStep = 1;
        const totalSteps = 4;
        const formData = {};

        // 复选框样式切换
        document.querySelectorAll('.checkbox-item').forEach(item => {
            item.addEventListener('click', function() {
                const checkbox = this.querySelector('input');
                checkbox.checked = !checkbox.checked;
                this.classList.toggle('selected', checkbox.checked);
            });
        });

        function collectStepData(step) {
            const content = document.querySelector(`.step-content[data-step="${step}"]`);
            content.querySelectorAll('input, select, textarea').forEach(el => {
                if (el.type === 'checkbox') {
                    if (!formData[el.name]) formData[el.name] = [];
                    if (el.checked) {
                        if (!formData[el.name].includes(el.value)) {
                            formData[el.name].push(el.value);
                        }
                    } else {
                        formData[el.name] = formData[el.name].filter(v => v !== el.value);
                    }
                } else {
                    formData[el.name] = el.value;
                }
            });
        }

        function validateStep(step) {
            const content = document.querySelector(`.step-content[data-step="${step}"]`);
            const requiredFields = content.querySelectorAll('[required]');
            let valid = true;

            requiredFields.forEach(field => {
                if (!field.value.trim()) {
                    field.style.borderColor = '#e74c3c';
                    valid = false;
                } else {
                    field.style.borderColor = '#e0e0e0';
                }
            });

            return valid;
        }

        function updateReview() {
            const genderMap = { male: '男', female: '女', '': '未选择' };
            const notifMap = { email: '邮件', sms: '短信', push: '推送', none: '不通知' };

            document.getElementById('review-name').textContent = formData.name || '-';
            document.getElementById('review-gender').textContent = genderMap[formData.gender] || '-';
            document.getElementById('review-birthday').textContent = formData.birthday || '-';
            document.getElementById('review-email').textContent = formData.email || '-';
            document.getElementById('review-phone').textContent = formData.phone || '-';
            document.getElementById('review-address').textContent = formData.address || '-';
            document.getElementById('review-interests').textContent =
                (formData.interests && formData.interests.length > 0) ? formData.interests.join(', ') : '-';
            document.getElementById('review-notification').textContent = notifMap[formData.notification] || '-';
        }

        function updateUI() {
            // 更新步骤指示器
            document.querySelectorAll('.step-dot').forEach(dot => {
                const step = parseInt(dot.dataset.step);
                dot.classList.remove('active', 'completed');
                if (step === currentStep) dot.classList.add('active');
                if (step < currentStep) dot.classList.add('completed');
            });

            // 更新步骤内容
            document.querySelectorAll('.step-content').forEach(content => {
                const step = parseInt(content.dataset.step);
                content.classList.toggle('active', step === currentStep);
            });

            // 更新按钮
            document.getElementById('prevBtn').style.visibility = currentStep === 1 ? 'hidden' : 'visible';

            const nextBtn = document.getElementById('nextBtn');
            if (currentStep === totalSteps) {
                nextBtn.textContent = '确认提交';
                nextBtn.className = 'btn btn-submit';
                updateReview();
            } else {
                nextBtn.textContent = '下一步';
                nextBtn.className = 'btn btn-next';
            }
        }

        function nextStep() {
            // 收集当前步骤数据
            collectStepData(currentStep);

            // 验证当前步骤
            if (!validateStep(currentStep)) return;

            if (currentStep === totalSteps) {
                // 最终提交
                submitAllData();
                return;
            }

            currentStep++;
            updateUI();
        }

        function prevStep() {
            collectStepData(currentStep);
            currentStep--;
            updateUI();
        }

        async function submitAllData() {
            try {
                const response = await fetch('https://httpbin.org/post', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(formData)
                });

                if (response.ok) {
                    // 显示成功页面
                    document.querySelector('.progress-header').style.display = 'none';
                    document.querySelectorAll('.step-content').forEach(c => c.classList.remove('active'));
                    document.getElementById('actions').style.display = 'none';
                    document.getElementById('successOverlay').classList.add('show');
                }
            } catch (error) {
                alert('提交失败: ' + error.message);
            }
        }
    </script>
</body>
</html>

示例6:表单数据编码详解

代码示例

<!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>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: #1a1a2e;
            color: #e0e0e0;
            min-height: 100vh;
            padding: 30px 20px;
        }
        .container { max-width: 900px; margin: 0 auto; }
        h1 { text-align: center; margin-bottom: 30px; color: #48dbfb; }
        .encoder-card {
            background: #16213e;
            border-radius: 12px;
            padding: 24px;
            margin-bottom: 20px;
            border: 1px solid #2d2d44;
        }
        .encoder-card h2 {
            font-size: 16px;
            margin-bottom: 16px;
            color: #feca57;
        }
        .input-area {
            width: 100%;
            min-height: 80px;
            padding: 12px;
            background: #0f3460;
            border: 1px solid #2d2d44;
            border-radius: 8px;
            color: #e0e0e0;
            font-family: 'Courier New', monospace;
            font-size: 14px;
            resize: vertical;
            outline: none;
        }
        .input-area:focus { border-color: #48dbfb; }
        .btn-row {
            display: flex;
            gap: 8px;
            margin: 12px 0;
            flex-wrap: wrap;
        }
        .enc-btn {
            padding: 8px 16px;
            border: 1px solid #2d2d44;
            border-radius: 6px;
            background: transparent;
            color: #e0e0e0;
            font-size: 13px;
            cursor: pointer;
            transition: all 0.3s;
        }
        .enc-btn:hover { background: #48dbfb; color: #0f0f23; border-color: #48dbfb; }
        .output-area {
            width: 100%;
            min-height: 80px;
            padding: 12px;
            background: #0a0a1a;
            border: 1px solid #2d2d44;
            border-radius: 8px;
            color: #00b894;
            font-family: 'Courier New', monospace;
            font-size: 14px;
            white-space: pre-wrap;
            word-break: break-all;
            overflow-y: auto;
        }
        .comparison-table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 12px;
        }
        .comparison-table th, .comparison-table td {
            padding: 10px 12px;
            border: 1px solid #2d2d44;
            text-align: left;
            font-size: 13px;
        }
        .comparison-table th {
            background: #0f3460;
            color: #48dbfb;
        }
        .comparison-table td {
            font-family: 'Courier New', monospace;
            color: #feca57;
        }
        .note {
            font-size: 12px;
            color: #888;
            margin-top: 8px;
            line-height: 1.6;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>表单数据编码详解</h1>

        <!-- URL编码 -->
        <div class="encoder-card">
            <h2>URL编码 (encodeURIComponent / decodeURIComponent)</h2>
            <textarea class="input-area" id="urlInput" placeholder="输入需要编码的文本,如:name=张三&age=25">name=张三&age=25&city=北京</textarea>
            <div class="btn-row">
                <button class="enc-btn" onclick="urlEncode()">编码 (encode)</button>
                <button class="enc-btn" onclick="urlDecode()">解码 (decode)</button>
                <button class="enc-btn" onclick="urlEncodeComponent()">组件编码</button>
                <button class="enc-btn" onclick="urlDecodeComponent()">组件解码</button>
            </div>
            <div class="output-area" id="urlOutput">点击按钮查看编码结果</div>
            <p class="note">
                encodeURI:编码完整URI,保留:/?#[]@!$&'()*+,;=等URI语法字符<br>
                encodeURIComponent:编码URI组件,编码所有特殊字符(包括/?:等),适合编码查询参数值<br>
                两者的区别在于编码范围不同,encodeURIComponent编码更彻底
            </p>
        </div>

        <!-- Base64编码 -->
        <div class="encoder-card">
            <h2>Base64编码 (btoa / atob)</h2>
            <textarea class="input-area" id="base64Input" placeholder="输入需要编码的文本">Hello, 世界!</textarea>
            <div class="btn-row">
                <button class="enc-btn" onclick="base64Encode()">Base64编码</button>
                <button class="enc-btn" onclick="base64Decode()">Base64解码</button>
                <button class="enc-btn" onclick="base64EncodeUnicode()">Unicode安全编码</button>
                <button class="enc-btn" onclick="base64DecodeUnicode()">Unicode安全解码</button>
            </div>
            <div class="output-area" id="base64Output">点击按钮查看编码结果</div>
            <p class="note">
                btoa():将字符串编码为Base64,仅支持Latin1字符<br>
                atob():将Base64字符串解码为原始字符串<br>
                对于Unicode字符(如中文),需要先使用encodeURIComponent转换,再btoa编码<br>
                解码时先atob解码,再decodeURIComponent还原
            </p>
        </div>

        <!-- 表单编码格式对比 -->
        <div class="encoder-card">
            <h2>三种编码格式对比</h2>
            <div style="margin-bottom:16px;">
                <label style="font-size:13px; color:#888; display:block; margin-bottom:6px;">输入测试数据(JSON格式):</label>
                <textarea class="input-area" id="compareInput" rows="4">{
    "username": "张三",
    "email": "zhangsan@example.com",
    "tags": ["前端", "React"],
    "message": "Hello & Goodbye!"
}</textarea>
            </div>
            <div class="btn-row">
                <button class="enc-btn" onclick="compareEncodings()">对比三种编码</button>
            </div>
            <table class="comparison-table">
                <thead>
                    <tr>
                        <th>编码格式</th>
                        <th>Content-Type</th>
                        <th>编码结果</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>URL编码</td>
                        <td>application/x-www-form-urlencoded</td>
                        <td id="compare-url" style="font-size:11px; word-break:break-all;">-</td>
                    </tr>
                    <tr>
                        <td>FormData</td>
                        <td>multipart/form-data</td>
                        <td id="compare-formdata" style="font-size:11px; word-break:break-all;">-</td>
                    </tr>
                    <tr>
                        <td>JSON</td>
                        <td>application/json</td>
                        <td id="compare-json" style="font-size:11px; word-break:break-all;">-</td>
                    </tr>
                </tbody>
            </table>
        </div>

        <!-- FormData操作 -->
        <div class="encoder-card">
            <h2>FormData API 操作演示</h2>
            <div class="output-area" id="formDataOutput" style="min-height:200px;">等待操作...</div>
            <div class="btn-row">
                <button class="enc-btn" onclick="demoFormData()">运行FormData演示</button>
            </div>
        </div>
    </div>

    <script>
        // ===== URL编码 =====
        function urlEncode() {
            const input = document.getElementById('urlInput').value;
            document.getElementById('urlOutput').textContent =
                'encodeURI:\n' + encodeURI(input) +
                '\n\n注意:encodeURI保留URI语法字符(:/?&=等)';
        }

        function urlDecode() {
            const input = document.getElementById('urlInput').value;
            try {
                document.getElementById('urlOutput').textContent =
                    'decodeURI:\n' + decodeURI(input);
            } catch (e) {
                document.getElementById('urlOutput').textContent = '解码错误: ' + e.message;
            }
        }

        function urlEncodeComponent() {
            const input = document.getElementById('urlInput').value;
            document.getElementById('urlOutput').textContent =
                'encodeURIComponent:\n' + encodeURIComponent(input) +
                '\n\n注意:encodeURIComponent编码所有特殊字符';
        }

        function urlDecodeComponent() {
            const input = document.getElementById('urlInput').value;
            try {
                document.getElementById('urlOutput').textContent =
                    'decodeURIComponent:\n' + decodeURIComponent(input);
            } catch (e) {
                document.getElementById('urlOutput').textContent = '解码错误: ' + e.message;
            }
        }

        // ===== Base64编码 =====
        function base64Encode() {
            const input = document.getElementById('base64Input').value;
            try {
                document.getElementById('base64Output').textContent =
                    'btoa():\n' + btoa(input);
            } catch (e) {
                document.getElementById('base64Output').textContent =
                    'btoa() 错误: ' + e.message +
                    '\n\n提示:btoa()不支持Unicode字符,请使用Unicode安全编码';
            }
        }

        function base64Decode() {
            const input = document.getElementById('base64Input').value;
            try {
                document.getElementById('base64Output').textContent =
                    'atob():\n' + atob(input);
            } catch (e) {
                document.getElementById('base64Output').textContent = '解码错误: ' + e.message;
            }
        }

        function base64EncodeUnicode() {
            const input = document.getElementById('base64Input').value;
            const encoded = btoa(encodeURIComponent(input).replace(/%([0-9A-F]{2})/g,
                function(match, p1) {
                    return String.fromCharCode('0x' + p1);
                }
            ));
            document.getElementById('base64Output').textContent =
                'Unicode安全编码:\n' + encoded +
                '\n\n原理:encodeURIComponent → 替换%XX → btoa';
        }

        function base64DecodeUnicode() {
            const input = document.getElementById('base64Input').value;
            try {
                const decoded = decodeURIComponent(
                    atob(input).split('').map(function(c) {
                        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
                    }).join('')
                );
                document.getElementById('base64Output').textContent =
                    'Unicode安全解码:\n' + decoded;
            } catch (e) {
                document.getElementById('base64Output').textContent = '解码错误: ' + e.message;
            }
        }

        // ===== 编码格式对比 =====
        function compareEncodings() {
            try {
                const data = JSON.parse(document.getElementById('compareInput').value);

                // URL编码
                const params = new URLSearchParams();
                for (const [key, value] of Object.entries(data)) {
                    if (Array.isArray(value)) {
                        value.forEach(v => params.append(key, v));
                    } else {
                        params.append(key, value);
                    }
                }
                document.getElementById('compare-url').textContent = params.toString();

                // FormData(模拟)
                let formDataStr = '------WebKitFormBoundary\n';
                for (const [key, value] of Object.entries(data)) {
                    if (Array.isArray(value)) {
                        value.forEach(v => {
                            formDataStr += `Content-Disposition: form-data; name="${key}"\n\n${v}\n------WebKitFormBoundary\n`;
                        });
                    } else {
                        formDataStr += `Content-Disposition: form-data; name="${key}"\n\n${value}\n------WebKitFormBoundary\n`;
                    }
                }
                formDataStr += '--';
                document.getElementById('compare-formdata').textContent = formDataStr;

                // JSON
                document.getElementById('compare-json').textContent = JSON.stringify(data);
            } catch (e) {
                alert('JSON格式错误: ' + e.message);
            }
        }

        // ===== FormData演示 =====
        function demoFormData() {
            const output = document.getElementById('formDataOutput');
            let log = '';

            // 创建FormData
            const fd = new FormData();
            log += '=== 创建FormData ===\n';
            log += 'const fd = new FormData();\n\n';

            // append vs set
            log += '=== append vs set ===\n';
            fd.append('hobby', 'reading');
            fd.append('hobby', 'coding');
            log += `fd.append('hobby', 'reading')\n`;
            log += `fd.append('hobby', 'coding')\n`;
            log += `fd.getAll('hobby') → [${fd.getAll('hobby').map(v => `'${v}'`).join(', ')}]\n\n`;

            fd.set('hobby', 'gaming');
            log += `fd.set('hobby', 'gaming')  // 覆盖所有同名值\n`;
            log += `fd.getAll('hobby') → ['${fd.getAll('hobby')[0]}']\n\n`;

            // 基本操作
            log += '=== 基本操作 ===\n';
            fd.set('username', 'zhangsan');
            fd.set('email', 'zhangsan@example.com');
            log += `fd.set('username', 'zhangsan')\n`;
            log += `fd.set('email', 'zhangsan@example.com')\n`;
            log += `fd.get('username') → '${fd.get('username')}'\n`;
            log += `fd.has('email') → ${fd.has('email')}\n`;
            log += `fd.has('phone') → ${fd.has('phone')}\n\n`;

            // 遍历
            log += '=== 遍历 ===\n';
            log += 'for (let [key, value] of fd.entries()) {\n';
            log += '    console.log(key, value);\n';
            log += '}\n';
            log += '结果:\n';
            for (let [key, value] of fd.entries()) {
                log += `  ${key}: ${value}\n`;
            }
            log += '\n';

            // 删除
            log += '=== 删除 ===\n';
            fd.delete('email');
            log += `fd.delete('email')\n`;
            log += `fd.has('email') → ${fd.has('email')}\n\n`;

            // 转换为对象
            log += '=== 转换为普通对象 ===\n';
            const obj = Object.fromEntries(fd);
            log += `Object.fromEntries(fd) → ${JSON.stringify(obj, null, 2)}\n\n`;

            // 转换为URLSearchParams
            log += '=== 转换为URLSearchParams ===\n';
            const urlParams = new URLSearchParams(fd);
            log += `new URLSearchParams(fd).toString()\n→ ${urlParams.toString()}\n`;

            output.textContent = log;
        }
    </script>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge IE
FormData API 7+ 4+ 5+ 12+ 10+
FormData.get() 42+ 39+ 10.1+ 12+ 不支持
FormData.getAll() 42+ 39+ 10.1+ 12+ 不支持
FormData.set() 42+ 39+ 10.1+ 12+ 不支持
FormData.delete() 42+ 39+ 10.1+ 12+ 不支持
URLSearchParams 49+ 29+ 10.1+ 12+ 不支持
fetch API 42+ 39+ 10.1+ 14+ 不支持
XMLHttpRequest 1+ 1+ 1+ 12+ 7+
btoa/atob 4+ 2+ 3+ 12+ 10+
form novalidate 10+ 4+ 5+ 12+ 10+
Constraint Validation API 10+ 4+ 5+ 12+ 10+

注意事项与最佳实践

1. 选择正确的编码格式

代码示例

// 文本字段 → URL编码或JSON
// 包含文件 → 必须使用FormData (multipart/form-data)
// API交互 → JSON格式
// 传统表单 → URL编码

// 错误示例:使用JSON发送文件
const wrongWay = {
    file: base64EncodedFileString  // 体积增加约33%,浪费带宽
};

// 正确示例:使用FormData发送文件
const formData = new FormData();
formData.append('file', fileInput.files[0]);  // 原始二进制传输,效率高

2. FormData使用注意事项

代码示例

// 注意1:使用FormData时不要手动设置Content-Type
const formData = new FormData();

// 错误 - 手动设置Content-Type会缺少boundary
fetch('/api/upload', {
    method: 'POST',
    headers: {
        'Content-Type': 'multipart/form-data'  // 缺少boundary!
    },
    body: formData
});

// 正确 - 让浏览器自动设置Content-Type
fetch('/api/upload', {
    method: 'POST',
    body: formData  // 浏览器自动设置正确的Content-Type和boundary
});

// 注意2:FormData的append和set的区别
const fd = new FormData();
fd.append('tag', 'js');
fd.append('tag', 'css');
// fd.getAll('tag') → ['js', 'css']

fd.set('tag', 'html');
// fd.getAll('tag') → ['html']  set会覆盖所有同名值

3. 安全性考虑

代码示例

// 1. 始终在服务端验证数据,不要信任前端验证
// 2. 对敏感数据使用HTTPS传输
// 3. 防止CSRF攻击 - 使用CSRF Token
const form = document.getElementById('secureForm');
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/submit', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken  // 添加CSRF令牌
    },
    body: JSON.stringify(data)
});

// 4. 对用户输入进行适当的编码和转义,防止XSS
function sanitizeInput(input) {
    const div = document.createElement('div');
    div.textContent = input;
    return div.innerHTML;
}

4. 表单提交的防抖处理

代码示例

// 防止重复提交
let isSubmitting = false;

async function submitForm(e) {
    e.preventDefault();

    if (isSubmitting) return;  // 防止重复提交
    isSubmitting = true;

    const submitBtn = e.target.querySelector('button[type="submit"]');
    submitBtn.disabled = true;
    submitBtn.textContent = '提交中...';

    try {
        const response = await fetch('/api/submit', {
            method: 'POST',
            body: new FormData(e.target)
        });
        // 处理响应...
    } catch (error) {
        console.error('提交失败:', error);
    } finally {
        isSubmitting = false;
        submitBtn.disabled = false;
        submitBtn.textContent = '提交';
    }
}

代码规范示例

规范的表单提交封装

代码示例

/**
 * 表单提交工具类
 * 封装了多种数据格式的表单提交方法
 */
class FormSubmitter {
    /**
     * @param {Object} config - 配置项
     * @param {string} config.url - 提交URL
     * @param {string} config.method - HTTP方法
     * @param {string} config.format - 数据格式 (json|formdata|urlencoded)
     * @param {Object} config.headers - 额外请求头
     * @param {Function} config.onBeforeSubmit - 提交前回调
     * @param {Function} config.onSuccess - 成功回调
     * @param {Function} config.onError - 错误回调
     * @param {Function} config.onComplete - 完成回调
     */
    constructor(config = {}) {
        this.url = config.url || '';
        this.method = config.method || 'POST';
        this.format = config.format || 'json';
        this.headers = config.headers || {};
        this.onBeforeSubmit = config.onBeforeSubmit || null;
        this.onSuccess = config.onSuccess || null;
        this.onError = config.onError || null;
        this.onComplete = config.onComplete || null;
        this.isSubmitting = false;
    }

    /**
     * 提交表单
     * @param {HTMLFormElement|Object} formOrData - 表单元素或数据对象
     * @returns {Promise<Object>} 响应数据
     */
    async submit(formOrData) {
        if (this.isSubmitting) {
            throw new Error('表单正在提交中,请勿重复操作');
        }

        // 提交前回调
        if (this.onBeforeSubmit) {
            const shouldContinue = this.onBeforeSubmit(formOrData);
            if (shouldContinue === false) return;
        }

        this.isSubmitting = true;

        try {
            const { body, headers } = this._prepareRequest(formOrData);

            const response = await fetch(this.url, {
                method: this.method,
                headers: { ...headers, ...this.headers },
                body: body
            });

            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }

            const data = await response.json();

            if (this.onSuccess) this.onSuccess(data);
            return data;
        } catch (error) {
            if (this.onError) this.onError(error);
            throw error;
        } finally {
            this.isSubmitting = false;
            if (this.onComplete) this.onComplete();
        }
    }

    /**
     * 准备请求数据
     * @private
     */
    _prepareRequest(formOrData) {
        let body;
        let headers = {};

        switch (this.format) {
            case 'formdata':
                if (formOrData instanceof HTMLFormElement) {
                    body = new FormData(formOrData);
                } else {
                    body = new FormData();
                    Object.entries(formOrData).forEach(([key, value]) => {
                        if (Array.isArray(value)) {
                            value.forEach(v => body.append(key, v));
                        } else {
                            body.append(key, value);
                        }
                    });
                }
                // 不设置Content-Type,让浏览器自动设置
                break;

            case 'urlencoded':
                const params = new URLSearchParams();
                const data = formOrData instanceof HTMLFormElement
                    ? Object.fromEntries(new FormData(formOrData))
                    : formOrData;
                Object.entries(data).forEach(([key, value]) => {
                    params.append(key, value);
                });
                body = params.toString();
                headers['Content-Type'] = 'application/x-www-form-urlencoded';
                break;

            case 'json':
            default:
                const jsonData = formOrData instanceof HTMLFormElement
                    ? Object.fromEntries(new FormData(formOrData))
                    : formOrData;
                body = JSON.stringify(jsonData);
                headers['Content-Type'] = 'application/json';
                break;
        }

        return { body, headers };
    }
}

// 使用示例
const submitter = new FormSubmitter({
    url: '/api/users',
    method: 'POST',
    format: 'json',
    onBeforeSubmit: (data) => {
        console.log('准备提交:', data);
        return true;  // 返回false可取消提交
    },
    onSuccess: (data) => {
        console.log('提交成功:', data);
        alert('提交成功!');
    },
    onError: (error) => {
        console.error('提交失败:', error);
        alert('提交失败: ' + error.message);
    },
    onComplete: () => {
        console.log('提交完成');
    }
});

// 提交表单
document.getElementById('myForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    await submitter.submit(e.target);
});

常见问题与解决方案

问题1:FormData无法打印/查看内容

代码示例

// 问题:直接console.log(FormData)看不到内容
const fd = new FormData();
fd.append('name', 'test');
console.log(fd);  // FormData {}  看不到内容

// 解决方案1:遍历entries
for (let [key, value] of fd.entries()) {
    console.log(key, value);
}

// 解决方案2:转换为对象
console.log('FormData内容:', Object.fromEntries(fd));

// 解决方案3:自定义日志函数
function logFormData(formData) {
    console.group('FormData内容');
    for (let [key, value] of formData.entries()) {
        if (value instanceof File) {
            console.log(`${key}: [File] ${value.name} (${value.size} bytes)`);
        } else {
            console.log(`${key}: ${value}`);
        }
    }
    console.groupEnd();
}

问题2:JSON提交时后端无法接收

代码示例

// 问题:后端使用body-parser解析JSON,但收到的数据为空

// 原因1:忘记设置Content-Type
fetch('/api/data', {
    method: 'POST',
    body: JSON.stringify(data)  // 缺少Content-Type头
});

// 解决:设置正确的Content-Type
fetch('/api/data', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'  // 必须设置
    },
    body: JSON.stringify(data)
});

// 原因2:数据未序列化
fetch('/api/data', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: data  // 直接传对象,浏览器会调用toString()
});

// 解决:使用JSON.stringify
fetch('/api/data', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)  // 必须序列化
});

问题3:表单提交后页面刷新

代码示例

// 问题:使用Ajax提交表单,但页面仍然刷新

// 原因:没有阻止表单默认提交行为
form.addEventListener('submit', function() {
    fetch('/api/submit', { method: 'POST', body: new FormData(form) });
    // 缺少e.preventDefault()
});

// 解决方案1:preventDefault
form.addEventListener('submit', function(e) {
    e.preventDefault();  // 阻止默认提交
    fetch('/api/submit', { method: 'POST', body: new FormData(form) });
});

// 解决方案2:使用button的click事件代替form的submit
submitBtn.addEventListener('click', function() {
    fetch('/api/submit', { method: 'POST', body: new FormData(form) });
});

// 解决方案3:return false(不推荐)
form.onsubmit = function() {
    fetch('/api/submit', { method: 'POST', body: new FormData(form) });
    return false;
};

问题4:GET请求中FormData的序列化

代码示例

// 问题:GET请求需要将FormData转为查询字符串

// 解决方案1:使用URLSearchParams
function formDataToQueryString(formData) {
    return new URLSearchParams(formData).toString();
}

// 解决方案2:手动构建
function formDataToQueryString2(formData) {
    const parts = [];
    for (const [key, value] of formData.entries()) {
        parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
    }
    return parts.join('&');
}

// 使用示例
const formData = new FormData(form);
const queryString = formDataToQueryString(formData);
fetch(`/api/search?${queryString}`)
    .then(response => response.json())
    .then(data => console.log(data));

问题5:大文件上传进度监控

代码示例

// 使用XMLHttpRequest监控上传进度
function uploadWithProgress(file, onProgress) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        const formData = new FormData();
        formData.append('file', file);

        xhr.upload.onprogress = function(e) {
            if (e.lengthComputable) {
                const percent = (e.loaded / e.total) * 100;
                onProgress(percent);
            }
        };

        xhr.onload = function() {
            if (xhr.status === 200) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject(new Error(`上传失败: ${xhr.status}`));
            }
        };

        xhr.onerror = function() {
            reject(new Error('网络错误'));
        };

        xhr.open('POST', '/api/upload');
        xhr.send(formData);
    });
}

// 使用
uploadWithProgress(file, (percent) => {
    console.log(`上传进度: ${percent.toFixed(1)}%`);
    progressBar.style.width = percent + '%';
}).then(data => {
    console.log('上传成功:', data);
}).catch(error => {
    console.error('上传失败:', error);
});

总结

表单数据提交是前端与后端通信的基础能力,本教程涵盖了以下核心内容:

  1. 传统表单提交:了解HTML表单的原生提交流程,包括action、method、enctype等关键属性,以及使用iframe接收响应避免页面跳转的技巧。

  2. Ajax表单提交:掌握使用XMLHttpRequest进行异步表单提交,包括URL编码、FormData和JSON三种数据格式的发送方式,以及上传进度监控。

  3. Fetch API提交:学习使用现代Fetch API进行表单提交,理解不同编码格式的特点和适用场景,以及请求构建的最佳实践。

  4. 表单验证:实现前端实时验证,包括必填检查、格式验证、长度限制、正则匹配等规则,以及验证结果的视觉反馈。

  5. 数据序列化:深入理解URL编码、Base64编码、FormData编码的原理和差异,掌握各种编码转换方法。

  6. 多步骤表单:学会管理复杂的多步骤表单流程,包括步骤间的数据收集、验证和最终的统一提交。

  7. 最佳实践:选择正确的编码格式、防止重复提交、CSRF防护、输入安全处理等生产环境必备知识。

掌握这些内容后,你将能够根据不同的业务场景选择最合适的表单提交方式,构建安全、高效、用户友好的表单交互体验。

常见问题

什么是14. 表单数据提交?

14. 表单数据提交是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。

如何学习14. 表单数据提交的实际应用?

教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。

14. 表单数据提交有哪些注意事项?

常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。

14. 表单数据提交适合初学者吗?

本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。

14. 表单数据提交的核心要点是什么?

核心要点包括教程简介等内容,建议按教程顺序逐步学习。

标签: Promise HTTP头部 表单提交 响应码 网络安全 键盘输入 async await 场景管理

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 后端通信:SSE服务器推送 - 从入门到实践详解 下一篇: 后端通信:15. 文件上传与下载 - 从入门到实践详解

poll相关推荐