ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用 HTML+JavaScript 做一个自动保存便签:输入即记录,刷新可恢复|AI 编程第 5 课

用 HTML+JavaScript 做一个自动保存便签:输入即记录,刷新可恢复|AI 编程第 5 课 查资料时临时记下一条命令学习时写下还没弄懂的问题工作时随手记录接下来要处理的事情。这些内容往往很短却经常需要临时找地方放。这一课我们做一个浏览器便签打开网页就能写停止输入后自动保存再次打开同一个地址时恢复内容。完成后你会得到一个包含这些功能的小工具多行文本输入。停止输入约半秒后自动保存。页面重新打开时恢复上次保存的内容。显示字符数量和保存状态。保存失败时明确提示并支持手动重试。本篇提供独立完整代码没有看过前四课也可以直接运行。如果你一直在跟做前面的效率工具箱文末也会说明怎样把便签放回原来的项目。先看一个使用场景今天的学习记录 1. 弄懂 localStorage 的用途 2. 练习 input 事件 3. 给便签加上自动保存输入这些文字稍微停顿一下页面就会显示已保存到本浏览器 · 具体时间此时刷新页面刚才保存的文字就会重新出现在输入框中。这里的“保存到本浏览器”有明确范围它使用 localStorage 保存内容不会自动同步到另一台电脑。清理网站数据、使用不同浏览器或无痕窗口也可能影响内容保留。直接用file:///打开的 HTML其存储行为没有统一保证练习时建议使用固定的本地 HTTP 地址。参考MDN localStorage先把小工具做出来再拆开看它的实现。在项目文件夹中新建note.html将下面的代码完整复制进去。使用记事本保存时文件类型选择“所有文件”编码选择 UTF-8确认文件名没有变成note.html.txt。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title自动保存便签/title style * { box-sizing: border-box; } body { margin: 0; padding: 40px 16px; background: #f5f7fb; color: #1f2937; font-family: Microsoft YaHei, Arial, sans-serif; } .note-panel { width: 100%; max-width: 760px; margin: 0 auto; padding: 28px; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 18px; box-shadow: 0 12px 35px rgba(31, 41, 55, 0.06); } .note-panel h1, .note-panel h2 { margin: 0 0 12px; font-size: 26px; } .note-panel .note-description { margin: 0 0 24px; color: #6b7280; line-height: 1.8; } .note-panel label { display: block; margin-bottom: 10px; font-weight: bold; } .note-panel textarea { display: block; width: 100%; min-height: 300px; padding: 16px; border: 1px solid #d1d5db; border-radius: 12px; background: #fcfcff; color: #1f2937; font: inherit; font-size: 16px; line-height: 1.8; resize: vertical; } .note-panel textarea:focus { outline: 3px solid #ede9fe; border-color: #7c3aed; } .note-panel .note-actions { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-top: 16px; } .note-panel #note-count { color: #6b7280; font-size: 14px; } .note-panel button { padding: 10px 18px; border: 0; border-radius: 8px; background: #7c3aed; color: #ffffff; font: inherit; cursor: pointer; } .note-panel button:disabled { opacity: 0.5; cursor: not-allowed; } .note-panel #note-status { min-height: 24px; margin: 16px 0 0; font-size: 14px; line-height: 1.7; overflow-wrap: anywhere; } .note-panel [data-statesaved] { color: #15803d; } .note-panel [data-statepending] { color: #92400e; } .note-panel [data-stateerror] { color: #b91c1c; } .note-panel .note-tip { margin: 12px 0 0; color: #6b7280; font-size: 13px; line-height: 1.7; } /style /head body main classnote-panel h1 我的随手便签/h1 p classnote-description 记录想法、命令和学习笔记停止输入后自动保存。 /p label fornote-editor便签内容/label textarea idnote-editor placeholder例如今天要弄懂 JavaScript 的 input 事件…… aria-describedbynote-tip /textarea div classnote-actions span idnote-count0 个字符/span button idsave-note typebutton立即保存/button /div p idnote-status rolestatus/p p idnote-tip classnote-tip 内容保存在当前浏览器重要记录请另行备份。 /p /main script (() { const editor document.querySelector(#note-editor); const count document.querySelector(#note-count); const status document.querySelector(#note-status); const saveButton document.querySelector(#save-note); const KEY efficiency-toolbox.note.v1; let timer null; let dirty false; let composing false; let readable true; // 更新字符数量 function updateCount() { count.textContent Array.from(editor.value).length 个字符; } // 取消之前安排的保存 function cancelPendingSave() { clearTimeout(timer); timer null; } // 保存当前内容 function saveNote() { cancelPendingSave(); if (!readable || !dirty) return; try { localStorage.setItem(KEY, editor.value); dirty false; status.textContent 已保存到本浏览器 · new Date().toLocaleTimeString(); status.dataset.state saved; } catch (error) { status.textContent 保存失败请先复制内容备份 再点击立即保存重试。; status.dataset.state error; console.error(便签保存失败, error); } } // 内容改变后重新安排保存 function scheduleSave() { dirty true; updateCount(); cancelPendingSave(); status.textContent composing ? 输入中…… : 尚未保存停止输入后自动保存……; status.dataset.state pending; if (!composing) { timer setTimeout(saveNote, 500); } } // 页面打开时恢复内容 try { const saved localStorage.getItem(KEY); editor.value saved ?? ; status.textContent saved null ? 写点什么吧停止输入后会自动保存。 : 已恢复上次保存的便签。; status.dataset.state saved; } catch (error) { readable false; editor.readOnly true; saveButton.disabled true; status.textContent 无法读取本地便签已暂停编辑。 请检查浏览器存储权限后重试。; status.dataset.state error; console.error(便签读取失败, error); } updateCount(); // 中文输入法开始组词 editor.addEventListener(compositionstart, () { composing true; cancelPendingSave(); }); // 中文输入法结束组词 editor.addEventListener(compositionend, () { composing false; scheduleSave(); }); // 输入、粘贴、删除内容时安排保存 editor.addEventListener(input, scheduleSave); // 输入框失去焦点时尝试立即保存 editor.addEventListener(blur, saveNote); // 手动保存或失败后重试 saveButton.addEventListener(click, saveNote); // 页面进入后台时尝试保存待保存内容 document.addEventListener(visibilitychange, () { if (document.visibilityState hidden) { saveNote(); } }); // 离开页面时再尝试保存 window.addEventListener(pagehide, saveNote); })(); /script /body /html已经使用第四课本地服务器的同学保持服务器运行在同一端口打开note.html即可。如果电脑已经安装 Python 3也可以在存放该文件的文件夹中打开终端运行python -m http.server 8000 --bind 127.0.0.1然后访问打开本地便签页面后续继续使用同一个浏览器和地址进行测试。先输入两行文字等待页面显示“已保存”再刷新。确认内容恢复后我们来看自动保存究竟是怎样发生的。便签需要监听用户修改文本的动作因此代码使用了input事件editor.addEventListener(input, scheduleSave);它能响应用户输入、删除和粘贴等导致输入框内容变化的操作。相比只监听键盘按键这更贴合“文本内容改变了”这一需求。不过通过 JavaScript 直接设置value不会自动触发这个事件。参考MDN input 事件每次内容变化程序先做三件事dirty true; updateCount(); cancelPendingSave();dirty表示“当前内容有尚未保存的修改”。它不是在判断内容是否错误而是在记录页面里的内容已经变化需要重新保存。随后程序安排半秒后执行保存timer setTimeout(saveNote, 500);如果你在这半秒内继续输入之前的计时器就会被取消再重新安排一次。例如你连续输入三个字输入“学” → 安排保存 输入“习” → 取消上一次重新安排 输入“中” → 再次取消重新安排 停止输入 → 等待约半秒保存“学习中”这就是常见的防抖连续操作时不断推迟执行等操作停下来再处理。本例中的 500 毫秒是等待时间设置不是精确执行保证。浏览器忙碌时回调可能更晚执行是否保存成功要看页面实际显示的状态。对于这份小便签每输入一次就保存也能作为基础实现。这里增加防抖是为了练习如何合并连续操作。以后实现搜索建议、输入校验时也会遇到类似需求。接着看真正写入数据的位置localStorage.setItem(KEY, editor.value);上一课保存待办清单需要把数组转换成 JSON。这一课保存的是输入框里的纯文本它本来就是字符串所以可以直接保存。恢复时const saved localStorage.getItem(KEY); editor.value saved ?? ;??的含义是如果读取结果是null或undefined就使用右边的空字符串。第一次使用没有保存内容输入框显示为空之前保存过内容就显示之前的文本。删除全部文字也应该保存。假设便签原来写着明天下午整理学习笔记你已经完成这件事于是把它全部删掉。如果程序只在“有文字”时保存刷新后旧内容还会回来。因此这个便签允许保存空字符串。代码中没有这样的限制// 不要用这个条件阻止空内容保存 if (editor.value.trim()) { // 保存 }空内容同样代表用户最新的编辑结果。你可能还注意到程序没有对便签内容调用trim()。待办任务通常是一小段标题去掉首尾空格比较合理。但便签可能包含换行、缩进和代码片段。本例直接保存原始文本让这些格式保留下来。对于中文输入还有一个需要考虑的过程。使用拼音输入法时输入字母、选择候选词和最终确认文字并不一定是一次完成的。如果在组词过程中频繁安排保存可能记录到尚未确认的输入内容。因此我们监听输入法组合事件editor.addEventListener(compositionstart, () { composing true; cancelPendingSave(); });editor.addEventListener(compositionend, () { composing false; scheduleSave(); });组词期间暂停安排定时保存文字确认后重新安排。对于用户实际输入体验这类细节往往比增加一个新按钮更有用。再看保存状态。只有localStorage.setItem()成功执行后程序才会显示已保存到本浏览器如果写入失败程序保留“尚有修改”的状态并提示先复制备份再点击“立即保存”重试。因此“立即保存”按钮虽然不是每次都要点但在保存失败后仍然有用途。如果一开始就无法读取存储程序会暂时将输入框设为只读避免在不知道旧内容的情况下继续覆盖。检查浏览器权限、恢复正常访问后再重新打开页面。还有一个实际问题你刚输入完还没等到半秒就切换到其他页面了。本例增加了输入框失去焦点、页面进入后台和离开页面时的保存尝试。其中visibilitychange可以帮助我们在页面变为隐藏状态时处理待保存内容。参考MDN visibilitychange 事件不过这些事件不能保证在浏览器崩溃、设备断电等情况下都执行。重要内容编辑后确认页面显示“已保存”并保留额外备份。完成代码后可以按这个顺序验收测试操作应看到的结果输入一段文字后停顿状态从等待保存变为已保存显示已保存后刷新恢复刚才的文字连续快速输入停顿后保存最终内容粘贴多行文字换行保留内容可恢复删除全部文字保存后刷新输入框保持为空使用中文输入法确认文字确认后正常安排保存修改后点击立即保存立即尝试保存当前内容修改后切换标签页页面隐藏时尝试保存字符计数使用的是Array.from(editor.value).length这里按 Unicode 码点计数换行也会计入某些组合表情可能包含多个码点。所以它是一个简单的长度提示不是严格的中文字数统计。如果你想继续让 AI 改进这个工具可以把当前代码和下面的需求一起发给它请在现有便签基础上增加“导出 TXT”功能。导出当前输入框中的文字保留换行。导出操作不要清空便签也不要改变自动保存状态。请说明新增代码的位置并提供测试步骤。这样你是在一个已经能运行的版本上增加明确功能出了问题也更容易找到修改范围。一直跟做前面课程的同学可以把便签接回效率工具箱将原来的便签占位卡片替换为本例main内的内容外层使用article classcard note-panel标题改为h2。将以.note-panel开头的样式复制到工具箱原来的style中保留工具箱自身的页面布局。添加.card.note-panel { max-width: none; margin: 0; }让便签卡片跟随原有网格排列。删除原来绑定#note-preview按钮的占位事件再把本例script内的代码放进现有脚本。便签使用的存储名称是efficiency-toolbox.note.v1待办清单使用的是efficiency-toolbox.tasks.v1两个模块使用不同名称便签保存时不会覆盖待办任务。若独立便签与工具箱使用同一来源地址、同一个便签存储名称它们就能读取同一份便签内容。本例暂时按单个页面编辑设计。多个标签页同时修改同一份便签时后保存的内容可能覆盖先前内容这可以作为后续版本的改进方向。下一课我们将把番茄钟、待办清单和便签整理成一个完整工具箱补上统一布局和模块切换让它成为一个可以持续使用的小项目。如果跟做时遇到问题描述“做了什么、出现什么、原本希望什么”并附上控制台报错会比只写“运行不了”更容易定位原因。
返回列表