ARTICLE DETAIL

资讯详情

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

前端实战:手写搜索页面,串联DOM操作与本地存储核心技能

前端实战:手写搜索页面,串联DOM操作与本地存储核心技能 1. 为什么第十五天该做Search页面从零基础开始学前端到第十五天这个节点其实处在一个很微妙的位置。语法基础基本过了一遍HTML和CSS的常用布局方式也上手了JavaScript的循环、数组操作、函数声明这些核心概念也都见过了。但这个阶段的通病是知道每个知识点是什么却不知道怎么把几个知识点拼成一样能用的东西。Search页面就是为这个阶段量身定制的练习项目。它看着简单——一个输入框、一个搜索结果列表最多加一个历史记录。但真正动手做的时候你会发现里面同时牵扯到DOM操作、事件监听、数组处理、数据筛选、状态同步、本地存储这些前端最基础的技能点。一个Search页面做完前面十五天学的东西基本全都能串起来。我选择在第十五天安排这个任务还有一个很现实的考虑它足够小一个晚上加一个上午就能出完整效果又足够完整做完之后你可以很自然往里面扩展筛选、排序、搜索历史、高亮关键词这些进阶功能。对小白来说没有比这种“跳一跳够得着”的项目更能建立信心了。Search页面在真实项目中也是无处不在的。淘宝的搜索框、掘金的文章搜索、GitHub的仓库搜索底层逻辑和我接下来要写的东西是同构的。只是真实场景下数据量更大、正则更复杂、还有后端的配合但前端这一层的基本骨架就是今天这套东西。你把这个页面做实了后面去改任何搜索场景的UI交互心里都会有一点底。今天这篇记录我不光写最终代码还会还原我踩坑的过程、当时卡住的地方以及为什么最后那样改。这样你看到的不只是答案还有思考路径自己写的时候会更少走弯路。1.1 学习阶段的定位第十四天结束时你该有的底子我特别想强调一下做Search页面之前你不需要等什么“时机成熟”。只要下面的东西你都见过了就可以直接开工。你需要在第十四天结束时能看着一个普通的静态页面说出每个标签是干嘛的会用Flexbox做横向排列和垂直居中已经背得下来getElementById和querySelector的差别写过至少一次数组的forEach或者filter遍历并且知道addEventListener总是比在HTML标签里写onclick更值得养成习惯。这个标准其实不高。如果你还没达到我建议先花一天把这几项补一补不用追求深入理解先做到“见过、用过、能认出来”就行。因为Search页面的实现过程会把这几样全部激活用一次比看十遍都管用。我当时就是带着这种半生不熟的状态开始的边做边查边改一天下来反而把之前模糊的概念全部打通了。1.2 Search页面的技术点拆解为什么它值得做一天一个完整的Search页面前端部分拆开来看涉及这么几块一个输入框需要监听用户的输入行为这里就牵扯到input事件和keyup事件的取舍以及防抖的概念——也就是用户停止输入多少毫秒后才真正发起筛选避免每敲一个字母就跑一遍逻辑。这在高频输入场景里是躲不掉的问题今天我会用一个极简版本让你感受一下。然后是数据筛选这一块需要把关键词转成可以匹配的规则用String的includes方法做包含判断或者用indexOf做基础检查再配合数组的filter方法把符合条件的结果捞出来。这个过程的从左到右思路是后面学习任何框架时都会反复遇到的。接着是页面更新查到的数据怎么渲染成真实可见的HTML。小白最顺手的是拼字符串往里塞但这种方式很快会因为转义问题翻车。我会演示一种更结构化的做法先建一个DocumentFragment或者容器节点循环把新元素插进去这样更干净。再往下就是状态管理的小启蒙。关键词、筛选结果、历史记录这些数据并不是散落的变量它们之间有一条清晰的关系链输入值决定筛选结果筛选结果决定页面显示历史记录决定底部那块列表。你把自己代入“页面里所有的数据都是有来源的”这种思维方式后面学Vue或React的时候会觉得顺很多。最后还有一个升级选项把搜索历史写进localStorage这样刷新页面之后还能恢复。这一步看上去只是多点了几下API但它背后关联的是“持久化”这个概念属于前端开发里比较基础也比较重要的一块内容。2. 从设计到代码Search页面的完整搭建过程先说清楚我这里要搭的不是复制粘贴就能跑的玩具页面而是一个你可以反复折腾、继续扩展的骨架。整页我会按两个文件来组织index.html负责骨架和布局lut.js负责查数据和更新页面。如果你还没接触过模块化或者工程化先别急单文件一样能跑重点是先把逻辑跑通。2.1 页面的功能需求梳理动手敲代码之前我先给自己列了一份需求清单。清单不一定越长越好但一定要清晰因为后面每一行代码都是围绕这里面的某一项展开的。页面顶部需要一个输入框和搜索按钮输入框要支持按回车就搜索同时还要在旁边放一个清空按钮用来快速重置页面状态。中间是搜索结果展示区默认状态下显示占位提示查询到结果后实时渲染成列表如果没有结果则显示“未找到相关内容”的提示。底部是搜索历史区用标签块的形式展示最近8条记录点击标签可以快速回填关键词并触发搜索历史记录要存储到浏览器本地关闭页面后再打开依然能恢复。这份清单看起来简单但每条都带有前端交互中常见的标准问题。比如按回车触发搜索就牵扯到keydown事件和防止默认行为实时渲染要考虑性能历史记录回填要处理输入框值和搜索逻辑的一致性。我会在实现过程中逐一说明为什么这样处理。2.2 HTML骨架的搭建我会先把页面的语义结构定下来。注意这里用了很多语义化标签而不是一把div到底。原因有两层一是有利于读代码的人快速理解每个区域是干什么的二是有利于搜索引擎和屏幕阅读器识别页面层次。对小白来说从第一天就养成这个习惯后面写复杂页面会省很多事。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleSearch 页面练习/title link relstylesheet hrefstyle.css /head body main classsearch-container h1Search 练习/h1 !-- 搜索输入区 -- section classsearch-box input typetext idsearchInput placeholder输入关键词例如JS autocompleteoff button idsearchBtn搜索/button button idclearBtn清空/button /section !-- 搜索结果区 -- section classresult-section ul idresultList/ul p idemptyTip输入关键词开始搜索/p /section !-- 历史记录区 -- section classhistory-section h2搜索历史/h2 div idhistoryTags/div /section /main script srclut.js/script /body /html这段结构里你看不到任何跟数据相关的硬编码因为数据来源在后续章节会单独设计。这里唯一需要留意的点就是button元素如果放在form外面默认不会触发表单提交这反而让逻辑更好控制。2.3 CSS样式先做整体再做细节写CSS我习惯分三步第一步重置默认样式并设置整体布局第二步把主要区块的框架样式定下来比如容器的宽度、卡片投影、间距第三步做交互反馈比如悬停效果、聚焦状态、过渡动画。* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, PingFang SC, Microsoft YaHei, sans-serif; background: #f5f6fa; min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px; } .search-container { width: 100%; max-width: 640px; background: #fff; border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06); padding: 32px; } h1 { font-size: 28px; font-weight: 700; color: #1a1a2e; margin-bottom: 24px; } .search-box { display: flex; gap: 12px; margin-bottom: 32px; } .search-box input { flex: 1; height: 44px; padding: 0 16px; font-size: 16px; border: 2px solid #e2e2ea; border-radius: 10px; outline: none; transition: border-color 0.2s; } .search-box input:focus { border-color: #4f46e5; } .search-box button { padding: 0 20px; font-size: 15px; border: none; border-radius: 10px; cursor: pointer; background: #4f46e5; color: #fff; transition: opacity 0.2s; } .search-box button:hover { opacity: 0.8; } .result-section { min-height: 160px; border-top: 1px solid #eee; padding-top: 16px; } .result-section ul { list-style: none; } .result-section li { padding: 10px 12px; margin-bottom: 4px; border-radius: 8px; cursor: pointer; transition: background 0.15s; } .result-section li:hover { background: #f0f0f4; } .history-section { margin-top: 32px; border-top: 1px solid #eee; padding-top: 20px; } .history-section h2 { font-size: 18px; color: #333; margin-bottom: 12px; } #historyTags { display: flex; flex-wrap: wrap; gap: 8px; } .history-tag { background: #f1f2f7; border-radius: 20px; padding: 6px 14px; font-size: 14px; color: #4f46e5; cursor: pointer; transition: background 0.15s; } .history-tag:hover { background: #e2e2ea; }你可能已经注意到我的历史标签、输入框聚焦、列表项悬停都做了明显的交互反馈这对小白来说很重要。前端页面不是“摆上去就行”你能不能感知到“这个元素可以点”“那个区域有反应”直接决定页面是不是显得专业。2.4 JS逻辑让页面活起来接下来是今天的主体——JavaSript部分。我会用lut.js这个名字说明这段代码的核心逻辑是通过输入关键词在预置数据里进行查找匹配然后把结果展示到页面上。先设计数据源这里用到一个数组每一项包含标题和分类。真实项目的搜索往往是请求后端接口但前端练习阶段重点在交互层用本地数组足以模拟。// 预置的数据源模拟后端返回的搜索结果 const searchData [ { title: JavaScript 基础语法指南, category: 教程 }, { title: 前端面试题JS 原型与闭包, category: 面试 }, { title: 用 CSS 实现一个响应式布局, category: 教程 }, { title: React 入门到放弃组件生命周期, category: 框架 }, { title: Vue 3 实战搜索框防抖处理, category: 框架 }, { title: Webpack 打包优化之路, category: 工程化 }, { title: 浏览器缓存机制详解, category: 原理 }, { title: Git 团队协作常用命令, category: 工具 }, { title: TypeScript 类型体操入门, category: 教程 }, { title: 性能优化如何减少 DOM 操作, category: 优化 } ];然后是一组基础元素引用也就是把之前HTML里的几个关键节点用querySelector拿过来方便后续操作。这里的命名我倾向于用语义化前缀比如$input表示输入框、$list表示结果列表这是很多前端开发者更习惯的风格事实上并非强制但保持一致性对阅读很有帮助。const $input document.querySelector(#searchInput); const $btn document.querySelector(#searchBtn); const $clearBtn document.querySelector(#clearBtn); const $list document.querySelector(#resultList); const $emptyTip document.querySelector(#emptyTip); const $historyTags document.querySelector(#historyTags);接下来我会一步步写核心逻辑每个函数的职责尽量单一这样调试的时候定位问题更快。3. 核心交互逻辑让页面不算难用页面光能渲染数据是不够的真正决定它好不好用的是交互细节。我在这里把所有交互拆成四个模块监听输入、触发搜索、更新列表、管理历史。每块单独说清楚你自己写的时候可以照着一个个去实现。3.1 监听输入与回车触发Search页面的使用场景决定了用户有两种触发方式一种是点击搜索按钮另一种是输入完直接按回车。这两种模式我都要兼容因此分离事件绑定会让代码更清晰也方便后续扩展防抖或自动完成功能。// 处理搜索逻辑的函数 function handleSearch() { const keyword $input.value.trim(); if (!keyword) { $list.innerHTML ; $emptyTip.textContent 请输入搜索关键词; $emptyTip.style.display block; return; } // 执行筛选 const result searchData.filter(item item.title.toLowerCase().includes(keyword.toLowerCase()) || item.category.includes(keyword) ); // 渲染结果 renderResult(result, keyword); // 记录历史 addHistory(keyword); } // 点击搜索按钮 $btn.addEventListener(click, handleSearch); // 按回车触发 $input.addEventListener(keydown, event { if (event.key Enter) { handleSearch(); } });这段代码里值得展开讲的地方有四个。第一是trim()的使用。用户在手机输入法或中文输入法下很容易在首尾带上空格如果不去掉 JS就搜不到关键词为JS的结果这种问题排查起来非常隐蔽。第二是大小写的统一。我把关键词和标题都转成了小写再比较这样用户输入js能搜到JavaScript输入GIT也能搜到Git相关的内容。这个细节在实际项目中几乎每次都会遇到数据库搜索就已经考虑了大小写但前端筛选必须自己处理。第三是空关键词的保护。如果输入框是空的还往下走结果列表自然什么都查不到而且还会把空关键词写进历史记录。所以我在这里先判断并给出提示这属于“防御式编程”的思维实际上很多前端异常都来自输入边界没挡住。第四是事件参数里event.key的判断。我不用event.keyCode或event.which的原因很简单keyCode已经废弃了虽然老项目里很多还在用但新代码里不要再用。我用的是直观的字符串比较Enter可读性远好于13这种魔法数字。3.2 筛选逻辑与关键词高亮筛选操作本身用数组filter方法就能完成。但搜索结果还有一个很常见的交互需求关键词高亮。也就是用户输入“JS”那么所有包含“JS”的结果列表中这个关键词应该被突出显示。这一步需要操作字符串和DOM也是很多小白觉得不太好理解的地方。我的做法是把原文中的关键词用mark标签包起来。这个标签是HTML里专门用来高亮文本的浏览器默认给它加一个黄底样式。如果你用的是React或者Vue逻辑完全一样只是渲染方式换成框架的语法而已。function highlightKeyword(text, keyword) { const regex new RegExp((${keyword}), ig); return text.replace(regex, mark$1/mark); } function renderResult(result, keyword) { $emptyTip.style.display none; $list.innerHTML ; if (result.length 0) { $emptyTip.textContent 没有找到与“${keyword}”相关的内容; $emptyTip.style.display block; return; } result.forEach(item { const li document.createElement(li); li.innerHTML ${highlightKeyword(item.title, keyword)} span stylecolor: #888; font-size: 13px; margin-left: 8px;[${item.category}]/span; $list.appendChild(li); }); }这里有一个点我踩过坑必须提醒你我用的是innerHTML直接插字符串这意味着如果关键词包含HTML特殊字符——比如用户输入了b——它会被当成标签解析。所以在有用户输入参与的地方用innerHTML一定要先做转义处理。今天我演示的数据源是写死的不会有这个问题但真实开发中要非常小心。另一种方案是全程用textContent赋值再用replace配合createElement逐个处理高亮。这样安全但代码更长对小白来说也更绕。你可以先理解现在的写法等过段时间再回来处理“XSS注入”这个安全问题。3.3 历史记录从内存到本地存储历史记录是实现搜索功能时容易忽略但又非常重要的部分。好用的搜索引擎、电商页面都会保留你最近的搜索记录方便快速二次搜索。实现思路分三层先放到数组里再渲染到页面上最后存进浏览器的本地存储里。const HISTORY_KEY search_history; const MAX_HISTORY 8; function getHistory() { try { return JSON.parse(localStorage.getItem(HISTORY_KEY)) || []; } catch { return []; } } function addHistory(keyword) { let history getHistory(); history history.filter(item item ! keyword); history.unshift(keyword); history history.slice(0, MAX_HISTORY); localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); renderHistory(); } function renderHistory() { const history getHistory(); $historyTags.innerHTML ; if (history.length 0) { $historyTags.textContent 暂无搜索记录; return; } history.forEach(item { const tag document.createElement(span); tag.className history-tag; tag.textContent item; tag.addEventListener(click, function () { $input.value item; handleSearch(); }); $historyTags.appendChild(tag); }); } // 页面初始化时渲染历史 renderHistory();localStorage的使用体验很接近一个简单的数据库用字符串键值对存储数据不会因为页面刷新就消失。但需要注意的是它只能存字符串所以我用JSON.stringify把数组转成JSON字符串再存取出来时再用JSON.parse转回数组。我设置了最大8条记录的限制避免历史无限膨胀。每次添加新记录时先去掉重复项再把新的放最前最后裁掉尾部超出的部分。这三步走下来数据的顺序和唯一性都保证了。点击历史标签是可以回填搜索的也就是$input.value item之后再调用handleSearch()这比直接把历史记录当纯文本展示有用得多用户点一下就能重搜。这里还要注意一点tag.addEventListener是在renderHistory里注册的每次重新渲染历史时旧的绑定会被覆盖不会有事件堆积的问题因为每次我都是先用innerHTML 清空容器再重新创建节点。3.4 清空与恢复初始状态清空按钮负责把搜索状态归零清输入框、清结果列表、恢复默认提示。这个操作看起来简单但滴水不漏地实现也有几个注意点。$clearBtn.addEventListener(click, function () { $input.value ; $list.innerHTML ; $emptyTip.textContent 输入关键词开始搜索; $emptyTip.style.display block; });这里不需要清掉历史记录因为历史记录属于用户浏览数据清空输入不应该连带抹掉。当然如果你做的是“清除所有数据”按钮那就是另一套逻辑了。清空之后焦点最好也回到输入框上这样用户可以直接开始下一轮输入。这个细节我是在实际测试时才发现的不把焦点拉回来用户每次清空后都要再点一下输入框才能打字体验上很别扭。4. 防抖优化与搜索实时性代码更符合真实场景如果你按上面的代码写完点击按钮搜索或回车搜索已经是完整可用的Search页面了。但真实产品的搜索框通常多一个更自然的动作不按任何按钮输入即触发搜索。比如掘金、GitHub的搜索框你输入到一半结果就已经实时出现在下方了。这个需求并不复杂监听input事件就行。但如果不做处理每敲一个字母、每删一个字符事件都会触发一次紧跟着筛选函数跑一遍、渲染函数跑一遍。如果数据量小还好一旦数据源是几万条页面就会明显卡顿甚至出现输入跟不上手速的情况。防抖就是用来解决这个问题的。4.1 防抖的原理与最小实现防抖的核心思想是当一个高频事件连续触发时并不每次立即执行回调而是等事件停止触发之后过指定的时间间隔再执行一次。就像电梯门每进一个人就重新计时人齐了隔几秒关门。如果一直有人进门就一直不关。实现一个防抖函数不需要任何库几行代码就能完成。function debounce(fn, delay 300) { let timer null; return function(...args) { if (timer) { clearTimeout(timer); } timer setTimeout(() { fn.apply(this, args); timer null; }, delay); }; }这里需要注意闭包的运用。timer变量不会因为函数执行完就被回收因为返回的新函数一直持有对它的引用。这就是JavaScript闭包的一个典型应用场景。以后你在Vue、React的项目里看到各种debounce封装它们背后的原理都是一样的。4.2 在Search页面接入实时搜索把防抖函数和输入事件组合起来// 输入事件防抖200ms 内停止输入后执行搜索 $input.addEventListener(input, debounce(function () { handleSearch(); }, 200));这样每停止输入200毫秒后自动执行搜索逻辑。输入过程中的反复触发会被持续清理掉最后一次停止后才真正干活。接入之后你会明显感觉到输入过程变得“丝滑”连续打“前端面试”并不会在“前”“前端”“前端面”这些中间状态上卡住只在停下来的那一刻显示“前端面试”相关结果。这个体验上的差异就是防抖带来的。可能有人会问为什么用200毫秒而不是0毫秒或500毫秒这个值没有标准答案取决于搜索的成本。如果每次搜索要请求后端网络耗时高防抖时间可以放大到400~500毫秒如果只是本地数据筛选150~300毫秒的体验最跟手。你可以自己调节这个值感受不同延迟带来的差异。4.3 防抖 回车 点击的关系有了防抖实时搜索之后回车和点击按钮还需要保留吗我建议保留。原因有两点第一防抖只在用户输入文本时触发如果用户输入完等了一会儿再去点击按钮此时防抖早就执行完了点击会再做一次搜索数据没变但用户获得了明确的操作反馈。第二很多用户使用搜索框时有敲回车确认的习惯这是一种“完成”的心理暗示。即使实时搜索结果已经出来了回车也应该无缝执行一次完整的搜索流程。这三个触发路径——输入防抖、回车事件、点击按钮——最终都汇到同一个handleSearch函数上。这样设计的好处是统一出口后续要改搜索逻辑只需要改一处。但你要注意一个边界情况防抖重新触发会不会造成历史记录里出现很多半截关键词比如用户想搜“JavaScript”先输入“Java”停了300毫秒历史会记录“Java”然后再输入“Script”又会记录“JavaScript”。这在技术上没错但确实可能污染历史列表。常见的处理方式是防抖触发的自动搜索不写入历史只有回车和手动点击按钮才记录历史。我建议你把这个逻辑加进去。function handleSearch({ saveHistory true } {}) { // 其他逻辑不变 if (saveHistory) { addHistory(keyword); } } // 防抖触发时不存历史 const debouncedSearch debounce(function () { handleSearch({ saveHistory: false }); }, 200);这样一个简单的参数扩展就解决了历史污染的问题。5. 常见问题与踩坑实录每次写技术分享我都习惯把自己真实遇到的问题列出来。有些问题你上网搜不到满意的答案就是因为太细了踩过的人不一定愿意分享。这里我挑五个自己卡过、琢磨过、解决掉的问题直接给你结论。5.1 中文输入法导致的高频多余搜索这是中文用户做搜索框一定会遇到的问题。使用拼音输入法时用户敲字母的过程会触发input事件但此时输入框里的值是拼音字母。比如用户想搜“前端”他打的是qianduan这个过程中防抖函数会不断触发搜索“q”“qi”“qia”……不仅浪费性能还会给用户展示一堆莫名其妙的结果。解决思路是监听compositionstart和compositionend事件判断用户是否处于中文组合状态。当compositionstart触发时说明正在拼音组合期间不应触发搜索等compositionend触发时才执行一次搜索。看一个简洁的方案let isComposing false; $input.addEventListener(compositionstart, () { isComposing true; }); $input.addEventListener(compositionend, function () { isComposing false; handleSearch(); }); $input.addEventListener(input, debounce(function () { if (!isComposing) { handleSearch({ saveHistory: false }); } }, 200));这段代码的要点在于input事件仍然会被触发但isComposing为true时不处理。等中文输入结束后compositionend里直接手动调用一次搜索补上最后一次组合结果。如果你不做这个处理中文搜索场景下防抖的价值就打了对折。5.2 filter 结果为空时的页面空白我第一次做搜索结果渲染时只处理了“有结果”的情况。输入一个毫无匹配的关键词结果列表清空了但页面上什么都没有用户不知道是自己搜错了还是页面坏了。这个问题看着小对体验的伤害却很大。现在的代码里已经在renderResult中做了判断结果为空时显示“没有找到与xx相关内容”的提示。这里建议把关键词也一并展示让用户确认不是系统漏了关键字。这个思路也延伸到搜索按钮前如果输入框为空提示“请输入关键词”不要直接不管。5.3 localStorage 数据格式损坏JSON.parse解析已经损坏的JSON字符串时会直接抛错这是我在测试时故意写坏数据发现的。也许用户开了多个标签页同时操作或者缓存数据被人手动改坏了都会导致整个脚本崩溃。所以我在getHistory里用try...catch兜底解析失败就返回空数组。这就是“防御式编程”的价值——不能假设外部数据永远是正确的。function getHistory() { try { return JSON.parse(localStorage.getItem(HISTORY_KEY)) || []; } catch { return []; } }这一行改动成本极低收益却很大。你写任何本地存储相关的代码时都应该带上这个习惯。5.4 渲染大量结果时页面卡顿如果你的数据源是几百条直接appendChild无所谓。但如果数据源上升到几千条一次性创建几千个DOM节点会让页面出现明显的卡顿感尤其是在低端手机上。这就是为什么真实项目里搜索分页、懒加载成为标配。作为一个过渡方案你可以只渲染前20条结果并在底部加一个“加载更多”的按钮。二十条数据用户一目了然体验并不会差多少但渲染压力比一次性全量渲染小得多。5.5 高亮标签与输入内容的冲突在3.2节我提到了innerHTML拼接高亮内容的安全问题。这里再展开一下因为很多人会忽略它。如果你输入的关键词里本身就含或比如搜索“div”高亮逻辑会把所有“div”替换成markdiv/mark正常。但如果用户直接输入script且数据源里碰巧包含“script”字样替换出来的字符串就可能被浏览器当作标签执行。安全写法是先转义再高亮。转义函数也不长function escapeHTML(str) { return str.replace(/[]/g, function (match) { const map { : amp;, : lt;, : gt;, : quot;, : #039; }; return map[match]; }); }在高亮之前先对企业内容转义再对关键词做高亮替换这样输出到innerHTML就安全了。等你后面学到XSS攻击相关知识时会发现这个问题在真实项目里有多重要。6. 拆分与扩展把Search页面做得更像真实项目完成了今天的版本你已经拥有一套完整的Search页面基础能力。如果你的目标是求职或做作品集还可以在现有代码上继续扩展几个方向不用重写只需要叠加。6.1 搜索结果的分组与筛选条件现在的结果是一股脑排成一个列表只有标题匹配。真实场景里搜索常常需要按类型过滤。你可以给数据源增加一个type字段然后在页面顶部放一排可选筛选项比如“全部”“教程”“面试”“工具”。点击筛选时保留关键词筛选条件同时再套一层类型过滤。这一步实际操作下来就是filter里多加一个判断条件const result searchData.filter(item { const isKeywordMatch item.title.includes(keyword) || item.category.includes(keyword); const isTypeMatch currentType all || item.category currentType; return isKeywordMatch isTypeMatch; });这个“多条件叠加”的思路在实际开发中非常高频。商品列表按价格区间筛选、勿扰模式下按分类筛选全是同一个套路。6.2 空数据时的兜底推荐当搜索结果为空与其只显示一句“没有找到”不如推荐几个热门关键词给用户点击。你可以预置一个热门词数组空结果时渲染成标签形式。这会让你的Search页面显得很“聪明”也体现出对用户下一步操作的引导设计。6.3 请求后端接口的思考如果你已经学了fetch可以把searchData替换成异步请求逻辑。比如async function fetchSearchResult(keyword) { const res await fetch(/api/search?q${encodeURIComponent(keyword)}); const data await res.json(); return data.list; }注意这里用encodeURIComponent对关键词做编码防止中文和特殊字符在URL里出错。这个习惯要尽早养成不然一到真实接口就容易踩中文参数乱码的坑。6.4 无障碍功能的补充在主流前端团队的验收清单里无障碍属于必不可少的一环。你可以给输入框加aria-label给搜索结果列表加aria-live区域这样屏幕阅读器能够朗读搜索状态变化。项目展示或面试时点出这一点远比只会写业务代码更有竞争力。7. 项目复盘与经验总结Search页面做下来我很想分享几个体会。第一项目不需要大但一定要完整。我见过很多学习前端的初学者一开始就想着要做一个“论坛系统”“电商平台”结果做了一半陷入技术债务不能自拔。Search页面的体量刚刚好能触发你处理数据、交互、状态、持久化这些核心问题又不会让你因为工程复杂度而放弃。第二拆解需求再动手比撸起袖子就写高效得多。我在动手前把功能点列成了清单写到哪里都不会跑偏。中途想加个“防抖”也能很自然地插在交互逻辑这一层不会牵一发动全身。第三写代码时要时刻想着“别人会怎么读这段代码”。变量命名清晰、函数职责单一、关键地方加注释这些东西虽然是“软素质”但越是早养成后面的项目就会越受益。第四从第十五天这个进度来看今天涉及的内容并不超纲。你以前用过的forEach、addEventListener、querySelector今天以新的方式组合在一起就变成了一个能用的产品。这就是前端有意思的地方——它的创造力不在于写出多罕见的语法而在于把常见的基础零件拼成有用的东西。Search页面做完之后的下一步我建议把这份代码翻新一遍尝试把渲染逻辑和筛选逻辑拆到两个独立函数文件里再尝试把数据源换成调用一个真实的公开API比如GitHub的搜索接口、辞典API等。当你发现不用改HTML、不用改CSS只是换数据源就能让页面拥有完全不同的内容时你就摸到了前后端分离的入门门槛。把今天的代码存好后面学框架的时候你可以试着用Vue或React重新实现一遍这个Search页面。到那时你会意识到虽然框架的写法完全不同但数据驱动页面的思路没有变今天打下的基础没有白费。
返回列表