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相关推荐