最简单有效的是用 localStorage 存搜索词数组,但必须处理去重、顺序、XSS 和空值四个卡点:先移除已有项再插开头并限长;用 trim() 和 length 过滤空值;读取时 JSON.parse 并加 try...catch;渲染用 textContent 防 XSS;确保 DOM 加载完成后再读取并触发 input 事件。

直接用 localStorage 存搜索词数组是最简单有效的做法,但必须处理去重、顺序、XSS 和空值这四个实际卡点,否则历史列表会错乱、重复、执行脚本或根本渲染不出来。
怎么存搜索词才不重复、不乱序
每次提交搜索后,不能直接 push(),否则最新词在末尾;也不能无脑 unshift() 后不截断,否则数组越攒越多。关键逻辑是:先移除已有项(保证唯一),再插到开头,最后限长。
- 用
trim()去首尾空格,再判断value.length === 0过滤空输入 - 读取时用
JSON.parse(localStorage.getItem('searchHistory') || '[]'),避免null导致解析失败 - 检查是否已存在,推荐用
historyArray.findIndex(item => item.toLowerCase() === term.toLowerCase()),统一大小写防“React”和“react”并存 - 存在则
splice(index, 1)删除,再unshift(term);不存在直接unshift() - 最后统一
historyArray.splice(10)截取前 10 条,别用slice(0, 10)赋值——那是新建数组,原引用没变
怎么渲染才不被 XSS 攻击
用户搜 <script>alert(1)</script> 是真实风险,innerHTML = 会直接执行。必须杜绝任何未转义的 HTML 插入。
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
- 生成历史项时,用
document.createElement('button')+textContent设置文字,不是innerHTML - 如果要用
<datalist>配合<input list="history">,选项内容也得走option.textContent = term,不能option.innerHTML - 点击回填搜索框时,直接赋值
input.value = clickedTerm,别拼 HTML 字符串再插入 - 清空按钮调用
localStorage.removeItem('searchHistory'),不要用clear()——它会删掉你存的其他键(比如草稿、分类)
为什么页面刷新后历史没显示出来
八成是读取时机错了:DOM 还没加载完就去取 localStorage,或者 JS 执行顺序被框架/异步逻辑打乱。
立即学习“前端免费学习笔记(深入)”;
- 确保读取逻辑放在
DOMContentLoaded事件里,或 script 标签放在</body>前 - 如果编辑器是 Vue/React 渲染的,必须等组件挂载后再读,比如 Vue 的
mounted()或 React 的useEffect(() => {}, []) - 读取后要手动触发一次搜索框的
input事件:input.dispatchEvent(new Event('input', { bubbles: true })),否则绑定的搜索逻辑不会响应 - 检查控制台是否有
SyntaxError: Unexpected token,那是之前存了非法 JSON(比如手动改过 localStorage 值),加try...catch包裹JSON.parse()
最易忽略的是大小写去重和 DOM 加载时机——前者让“前端”和“Frontend”共存,后者让历史列表永远为空白。这两处不处理,功能就算写全了也等于没做。


















