用户在填写表单时,经常会遇到意外关闭页面或浏览器崩溃的情况,导致长时间的输入成果丢失。通过实现表单自动保存功能,可以在用户输入时实时保存数据到本地存储,当用户意外离开后重新进入页面时,能够恢复之前填写的内容,大大提升用户体验。这种机制特别适用于注册表单、问卷调查、订单填写等需要用户投入时间的场景。本文将介绍如何使用 HTML、CSS 和 JavaScript 实现表单自动保存功能。效果演示
当用户在表单中进行输入操作时,系统会在用户停止输入 1 秒后自动将当前填写的数据保存到浏览器的本地存储中。页面右上角会显示一个"已自动保存"的提示信息,提醒用户数据已保存。当用户刷新页面或重新访问该页面时,之前填写的数据会自动恢复到相应的表单控件中。即使用户意外关闭浏览器后再次打开,数据依然存在。用户可以通过提交按钮提交表单,或使用清空按钮清除表单内容和本地存储。
页面结构
表单主体区域
包含各种类型的表单控件,如文本输入框、日期选择器、单选按钮组、复选框组、下拉选择器和文本域。<div class="form-group"> <label for="name">姓名</label> <input type="text" id="name" name="name" placeholder="请输入您的姓名"></div><div class="form-group"> <label for="birthdate">出生日期</label> <input type="date" id="birthdate" name="birthdate"></div>
操作按钮区域
<div class="buttons"> <button type="submit" class="btn-primary">提交表单</button> <button type="button" class="btn-secondary" onclick="clearForm()">清空表单</button></div>
提示信息区域
<div id="saveIndicator" class="save-indicator">已自动保存</div>
核心功能实现
自动保存类初始化
创建 AutoSaveForm 类,用于管理表单的自动保存功能。在构造函数中初始化必要的 DOM 元素引用,并在 init 方法中设置事件监听器。主要监听 input 和 change 事件来检测用户输入,同时监听页面可见性变化以在用户切换标签页时保存数据。class AutoSaveForm { constructor(formId, storageKey = 'autoSaveForm') { this.form = document.getElementById(formId); this.storageKey = storageKey; this.saveIndicator = document.getElementById('saveIndicator'); this.status = document.getElementById('status'); this.saveTimeout = null; this.init(); }
init() { this.loadSavedData(); this.form.addEventListener('input', (e) => this.handleInput(e)); this.form.addEventListener('change', (e) => this.handleInput(e)); this.form.addEventListener('submit', (e) => this.handleSubmit(e)); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') this.saveData(); }); }}
输入事件处理与延迟保存
当用户输入时,需要避免频繁保存造成性能问题。通过 handleInput 方法实现防抖机制,每次输入后设置 1 秒的延迟,如果在延迟期间又有新的输入,则清除之前的定时器并重新设置,确保只在用户停止输入后才执行保存操作。handleInput(e) { if (this.saveTimeout) clearTimeout(this.saveTimeout); this.saveTimeout = setTimeout(() => this.saveData(), 1000);}
数据收集与本地存储
saveData 方法负责收集表单中的所有数据并保存到 localStorage。对于复选框组,需要特殊处理以正确收集所有选中的值。同时处理未选中的复选框,确保它们的值也被正确记录。saveData() { const data = {}; const formData = new FormData(this.form); for (let [key, value] of formData.entries()) { const elements = this.form.querySelectorAll(`input[name="${key}"][type="checkbox"]`); if (elements.length > 1) { if (data[key]) { if (!Array.isArray(data[key])) data[key] = [data[key]]; data[key].push(value); } else { data[key] = [value]; } } else { data[key] = value; } } this.form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { if (!formData.has(checkbox.name)) { const groupCheckboxes = this.form.querySelectorAll(`input[type="checkbox"][name="${checkbox.name}"]`); if (groupCheckboxes.length > 1) { if (!data[checkbox.name]) data[checkbox.name] = []; } else { data[checkbox.name] = false; } } }); localStorage.setItem(this.storageKey, JSON.stringify(data)); this.showSaveIndicator();}
数据恢复与状态显示
loadSavedData 方法从本地存储中读取之前保存的数据,并将其恢复到相应的表单控件中。针对不同类型的表单元素(文本框、单选框、复选框)使用不同的恢复逻辑。showSaveIndicator 方法控制保存状态提示信息的显示和隐藏。loadSavedData() { const savedData = localStorage.getItem(this.storageKey); if (savedData) { const data = JSON.parse(savedData); Object.keys(data).forEach(key => { const elements = this.form.elements[key]; if (elements) { if (elements.type === 'checkbox' || elements[0]?.type === 'checkbox') { this.setCheckboxValues(elements, data[key]); } else if (elements.type === 'radio' || elements[0]?.type === 'radio') { this.setRadioValues(elements, data[key]); } else if (Array.isArray(data[key]) && elements.length) { Array.from(elements).forEach(el => el.checked = data[key].includes(el.value)); } else { elements.value = data[key]; } } }); }}
完整代码
git地址:https://gitee.com/ironpro/hjdemo/blob/master/form-autosave/index.html<!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 { background: #f5f5f5; padding: 20px; min-height: 100vh; color: #333; } .container { background: white; padding: 20px; width: 800px; margin: 0 auto; box-shadow: 0 2px 10px rgba(0,0,0,0.05); } h1 { color: #2c3e50; font-size: 22px; margin-bottom: 25px; text-align: center; border-bottom: 1px solid #eee; padding-bottom: 15px; } .form-group { margin-bottom: 20px; display: flex; flex-direction: row; align-items: flex-start; } .form-group.checkbox-single { align-items: center; } label { display: inline-block; margin-bottom: 0; color: #374151; font-size: 14px; font-weight: 500; width: 120px; flex-shrink: 0; } .form-group.checkbox-single label { width: auto; margin-left: 8px; } input[type="text"], input[type="email"], input[type="number"], input[type="date"], textarea, select { width: calc(100% - 130px); padding: 10px 14px; border: 1px solid #d1d5db; font-size: 14px; transition: border-color 0.2s; background: white; } .form-group.checkbox-single input[type="checkbox"] { width: auto; } input[type="text"]:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="date"]:focus, textarea:focus, select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59, 130, 246, .1); } textarea { resize: vertical; min-height: 100px; } .radio-group, .checkbox-group, .checkbox-grid { display: flex; flex-wrap: wrap; gap: 16px; width: calc(100% - 130px); } .radio-item, .checkbox-item { display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 6px 0; flex-shrink: 0; } input[type="radio"], input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: #3b82f6; } .save-indicator { position: fixed; top: 16px; right: 16px; background: #10b981; color: white; padding: 8px 16px; border-radius: 4px; font-size: 14px; opacity: 0; transform: translateY(-10px); transition: all 0.2s ease; pointer-events: none; box-shadow: 0 2px 4px rgba(0, 0, 0, .1); } .save-indicator.show { opacity: 1; transform: translateY(0); } .buttons { display: flex; gap: 14px; margin-top: 24px; } button { padding: 10px 20px; border: 1px solid transparent; font-size: 14px; cursor: pointer; transition: all 0.2s; font-weight: 500; } .btn-primary { background: #3b82f6; color: white; } .btn-primary:hover { background: #2563eb; } .btn-secondary { background: white; color: #374151; border: 1px solid #d1d5db; } .btn-secondary:hover { background: #f3f4f6; } </style></head><body><div class="container"> <h1>表单自动保存</h1> <form id="autoSaveForm"> <div class="form-group"> <label for="name">姓名</label> <input type="text" id="name" name="name" placeholder="请输入您的姓名"> </div> <div class="form-group"> <label for="birthdate">出生日期</label> <input type="date" id="birthdate" name="birthdate"> </div> <div class="form-group"> <label>性别</label> <div class="radio-group"> <div class="radio-item"> <input type="radio" id="male" name="gender" value="male"> <label for="male">男</label> </div> <div class="radio-item"> <input type="radio" id="female" name="gender" value="female"> <label for="female">女</label> </div> </div> </div> <div class="form-group"> <label>兴趣爱好</label> <div class="checkbox-grid"> <div class="checkbox-item"> <input type="checkbox" id="reading" name="hobbies" value="reading"> <label for="reading">阅读</label> </div> <div class="checkbox-item"> <input type="checkbox" id="sports" name="hobbies" value="sports"> <label for="sports">运动</label> </div> <div class="checkbox-item"> <input type="checkbox" id="music" name="hobbies" value="music"> <label for="music">音乐</label> </div> <div class="checkbox-item"> <input type="checkbox" id="travel" name="hobbies" value="travel"> <label for="travel">旅行</label> </div> <div class="checkbox-item"> <input type="checkbox" id="cooking" name="hobbies" value="cooking"> <label for="cooking">烹饪</label> </div> </div> </div> <div class="form-group"> <label>技能水平</label> <div class="radio-group"> <div class="radio-item"> <input type="radio" id="beginner" name="skill" value="beginner"> <label for="beginner">初学者</label> </div> <div class="radio-item"> <input type="radio" id="intermediate" name="skill" value="intermediate"> <label for="intermediate">中级</label> </div> <div class="radio-item"> <input type="radio" id="advanced" name="skill" value="advanced"> <label for="advanced">高级</label> </div> </div> </div> <div class="form-group"> <label for="city">城市</label> <select id="city" name="city"> <option value="">请选择城市</option> <option value="beijing">北京</option> <option value="shanghai">上海</option> <option value="guangzhou">广州</option> <option value="shenzhen">深圳</option> <option value="hangzhou">杭州</option> <option value="chengdu">成都</option> </select> </div> <div class="form-group"> <label>通知设置</label> <div class="checkbox-group"> <div class="checkbox-item"> <input type="checkbox" id="emailNotify" name="notifications" value="email"> <label for="emailNotify">邮件通知</label> </div> <div class="checkbox-item"> <input type="checkbox" id="smsNotify" name="notifications" value="sms"> <label for="smsNotify">短信通知</label> </div> <div class="checkbox-item"> <input type="checkbox" id="pushNotify" name="notifications" value="push"> <label for="pushNotify">推送通知</label> </div> </div> </div> <div class="form-group"> <label for="message">留言</label> <textarea id="message" name="message" placeholder="请输入您的留言"></textarea> </div> <div class="form-group checkbox-single"> <input type="checkbox" id="agree" name="agree"> <label for="agree">我已阅读并同意服务条款</label> </div> <div class="buttons"> <button type="submit" class="btn-primary">提交表单</button> <button type="button" class="btn-secondary" onclick="clearForm()">清空表单</button> </div> </form></div>
<div id="saveIndicator" class="save-indicator">已自动保存</div>
<script> class AutoSaveForm { constructor(formId, storageKey = 'autoSaveForm') { this.form = document.getElementById(formId); this.storageKey = storageKey; this.saveIndicator = document.getElementById('saveIndicator'); this.status = document.getElementById('status'); this.saveTimeout = null; this.init(); }
init() { this.loadSavedData(); this.form.addEventListener('input', (e) => this.handleInput(e)); this.form.addEventListener('change', (e) => this.handleInput(e)); this.form.addEventListener('submit', (e) => this.handleSubmit(e)); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') this.saveData(); }); }
handleInput(e) { if (this.saveTimeout) clearTimeout(this.saveTimeout); this.saveTimeout = setTimeout(() => this.saveData(), 1000); }
saveData() { const data = {}; const formData = new FormData(this.form); for (let [key, value] of formData.entries()) { const elements = this.form.querySelectorAll(`input[name="${key}"][type="checkbox"]`); if (elements.length > 1) { if (data[key]) { if (!Array.isArray(data[key])) data[key] = [data[key]]; data[key].push(value); } else { data[key] = [value]; } } else { data[key] = value; } } this.form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { if (!formData.has(checkbox.name)) { const groupCheckboxes = this.form.querySelectorAll(`input[type="checkbox"][name="${checkbox.name}"]`); if (groupCheckboxes.length > 1) { if (!data[checkbox.name]) data[checkbox.name] = []; } else { data[checkbox.name] = false; } } }); localStorage.setItem(this.storageKey, JSON.stringify(data)); this.showSaveIndicator(); }
loadSavedData() { const savedData = localStorage.getItem(this.storageKey); if (savedData) { const data = JSON.parse(savedData); Object.keys(data).forEach(key => { const elements = this.form.elements[key]; if (elements) { if (elements.type === 'checkbox' || elements[0]?.type === 'checkbox') { this.setCheckboxValues(elements, data[key]); } else if (elements.type === 'radio' || elements[0]?.type === 'radio') { this.setRadioValues(elements, data[key]); } else if (Array.isArray(data[key]) && elements.length) { Array.from(elements).forEach(el => el.checked = data[key].includes(el.value)); } else { elements.value = data[key]; } } }); } }
setCheckboxValues(elements, savedValue) { if (elements.length) { Array.from(elements).forEach(checkbox => { if (Array.isArray(savedValue)) checkbox.checked = savedValue.includes(checkbox.value); else checkbox.checked = savedValue === true; }); } else { elements.checked = savedValue === true || elements.value === savedValue; } }
setRadioValues(elements, savedValue) { if (elements.length) Array.from(elements).forEach(radio => radio.checked = radio.value === savedValue); else elements.checked = elements.value === savedValue; }
showSaveIndicator() { this.saveIndicator.classList.add('show'); setTimeout(() => this.saveIndicator.classList.remove('show'), 2000); }
handleSubmit(e) { e.preventDefault(); this.saveData(); setTimeout(() => { if (confirm('是否清空已保存的数据?')) { this.clearSavedData(); this.form.reset(); } }, 1000); }
clearSavedData() { localStorage.removeItem(this.storageKey); } }
const autoSaveForm = new AutoSaveForm('autoSaveForm');
function clearForm() { if (confirm('确定要清空表单吗?')) { autoSaveForm.clearSavedData(); document.getElementById('autoSaveForm').reset(); } }</script></body></html>
阅读原文:https://mp.weixin.qq.com/s/Am8HSmh3yo6X3_RBw50E9g
该文章在 2026/1/5 15:58:08 编辑过