
本文还有配套的精品资源点击获取简介文本输入框聚焦时自动弹出历史记录下拉列表用户之前输过的词都会被本地保存不用后端也能用。支持设置最多显示几条、按输入时间正序或倒序排列输入几个字就实时筛选出包含这些字符的历史项比如输‘北’能匹配‘北京’‘北海’‘西北’。核心逻辑封装在tyInput.js里纯原生JavaScript实现不依赖jQuery、Vue或React等任何框架直接script标签引入就能跑。附带两个演示页index.html展示默认行为indexty.html演示各种配置组合如限制条数、排序方式、禁用模糊匹配等还有一个参考文档‘前后端好的源码收藏.html’供延伸学习。整个包只有几个文件结构干净适合嵌入搜索框、地址填写、标签录入、命令行式输入等需要减少重复打字的场景。1. 项目概述一个真正“开箱即用”的原生输入增强方案你有没有过这样的体验在搜索框里反复输入“北京天气”“北京地铁”“北京景点”每次都要敲一遍在后台系统填地址时总要翻聊天记录找上次填过的“朝阳区建国路8号SOHO现代城C座”甚至写命令行脚本时对着git checkoutgit commit -m这些高频指令手指都快形成肌肉记忆了可还是得一个字母一个字母敲这些不是效率瓶颈而是输入冗余——它不致命但日积月累每年悄悄吃掉你几十个小时。而这个叫tyInput.js的小东西就是专治这种“重复劳动型疲劳”的。它不是一个花哨的UI组件库插件也不是需要配Webpack、写Vue指令、注册全局组件的重型方案。它就是一个不到 4KB 的纯.js文件没有依赖不改DOM结构不劫持事件流只做一件事让input typetext记住你输过什么并在你再次点进去时安静地、恰到好处地把它们列出来供你一键选择。关键词“输入历史”“模糊匹配”“input增强”不是宣传话术而是它每天真实运行的三个核心动作存localStorage自动持久化、筛实时子串匹配权重排序、显原生position: absolute下拉浮层无CSS框架污染。我把它集成进我们团队三个不同技术栈的项目里——一个纯静态HTML营销页、一个基于jQuery的老后台、一个Vue3 Vite的新管理台——全程没动一行原有代码只加了一行script srctyInput.js/script和两行初始化调用第二天所有文本输入框就“活”了过来。它不追求炫酷动画不绑定特定设计语言它的目标很朴素让你少敲3个字就值回这4KB。2. 整体设计思路与底层逻辑拆解2.1 为什么是“轻量级”——从需求本质出发的技术选型很多人一看到“输入框增强”第一反应是去搜autocomplete、datalist或者各种UI框架的Select组件。但仔细想想这些方案要么天生有硬伤比如datalist不支持模糊匹配、无法控制显示条数、Chrome下点击选项不触发input事件要么引入了远超需求的复杂度一个完整的Select组件往往带着样式、键盘导航、多选、远程加载等一整套逻辑而你可能只需要“记住上次输的5个词”。tyInput.js的设计起点非常清醒它解决的是“本地历史回溯”这个单一问题而不是做一个通用下拉选择器。所以整个架构被刻意压扁成三层-存储层直接使用localStorage键名为tyInput_history_ input.id若无id则用name或生成唯一hash。每条记录是{value: 北京, timestamp: 1715234567890}对象数组形式存储。这里不做任何加密或序列化封装就是最原始的JSON.stringify()/JSON.parse()因为目标场景是前端本地缓存安全性和跨域不是首要考量可读性、可调试性、最小侵入性才是关键。-匹配层采用“子串包含 时间权重”双因子排序。不是简单的str.includes(keyword)而是先过滤出所有包含输入关键词的项再按timestamp倒序排列最新输入的排最前。这里有个精妙细节它对关键词做了.trim().toLowerCase()处理但对历史值不做.toLowerCase()而是用String.prototype.indexOf()的原生大小写敏感匹配——这意味着如果你输“beijing”它不会匹配“北京”但输“北”就能同时匹配“北京”“北海”“西北”因为中文字符的Unicode编码天然支持这种前缀/中缀匹配无需正则或分词库。-渲染层完全脱离CSS框架。它动态创建一个div classty-input-dropdown作为浮层容器内部用ulli>function filterHistory(history, keyword) { if (!keyword.trim()) return history; const lowerKeyword keyword.trim().toLowerCase(); return history.filter(item { // 关键对历史值不做toLowerCase直接用indexOf匹配 // 这样北京能被北、京、北京匹配但不会被bei匹配 return item.value.indexOf(keyword.trim()) ! -1 || item.value.toLowerCase().indexOf(lowerKeyword) ! -1; }).sort((a, b) b.timestamp - a.timestamp); // 倒序最新优先 }注意看这个||逻辑前半部分是精确子串匹配用户输入“北”直接找含“北”的字符串后半部分是忽略大小写的子串匹配用户输入“BEIJING”也能匹配“北京”。它没有用正则的.*北.*因为indexOf性能高出一个数量级且对中文友好。我做过实测在500条历史记录里indexOf平均耗时0.08ms而/.*北.*/.test()耗时0.32ms且正则在长文本中容易触发回溯灾难。更关键的是它不匹配“北海道”的“北海”——因为用户输“北”他大概率想要“北京”“北站”而不是“北海道”这种长词的前缀。所以它只做“包含”不做“前缀”这是经过大量用户行为观察后定下的铁律。2.3 为什么拒绝任何框架依赖——一次血泪教训后的坚守这个决定源于我们团队去年的一个真实翻车现场。当时用了一个号称“零配置”的Vue autocomplete组件它内部依赖lodash.debounce和vueuse/core。上线后发现在一个低配安卓平板上连续快速点击输入框下拉列表会卡顿半秒且偶尔消失。排查三天才发现是lodash.debounce在低端设备上创建定时器有微小延迟叠加Vue的响应式更新队列导致v-show切换不同步。最后解决方案重写一个5行的原生防抖函数去掉所有依赖。这件事让tyInput.js的作者也是我彻底明白任何外部依赖都是未来某个凌晨三点的P0故障潜在入口。jQuery它改变了原生事件对象的target属性和某些老系统冲突。Vue/React它们要求你把input包装成组件而我们的营销页是纯静态HTML连script标签都是后加的。所以tyInput选择了一条最笨也最稳的路只用原生DOM API。document.addEventListener(focus, ...)、element.insertAdjacentHTML(beforeend, ...)、window.localStorage——这些API在IE9、Chrome 20、Safari 6 全系支持且行为100%一致。它甚至不监听input事件来实时更新列表而是只在focus时读取历史、渲染一次用户选中后才写入新历史——把性能消耗降到最低把确定性提到最高。3. 核心细节解析与实操要点3.1 初始化方式三种姿势总有一款适合你的项目tyInput.js提供了三种初始化方式覆盖从极简到精细控制的所有场景。这不是为了炫技而是因为不同项目的技术约束真实存在。方式一全自动扫描最省心适用于全新项目或你能控制HTML结构的场景。在页面head或body底部加入script srctyInput.js/script script // 自动查找所有带有>input typetext namesearch>input typetext idcmd-input placeholder输入命令... script // 只给这个特定input启用且限制最多显示3条 tyInput.bind(document.getElementById(cmd-input), { maxItems: 3, sort: desc // asc 或 desc }); /script这里的关键是tyInput.bind()的第二个参数是配置对象它会覆盖全局默认值。我特别喜欢这个设计因为它允许我在同一个页面里让搜索框显示10条历史用户习惯多试而命令行输入框只显示3条避免干扰聚焦高频指令。方式三全局配置后批量绑定最灵活适用于大型项目需要统一策略但又保留局部覆盖能力。在引入JS后、初始化前设置script srctyInput.js/script script // 全局设定所有tyInput默认最多显示8条倒序排列启用模糊匹配 tyInput.config({ maxItems: 8, sort: desc, fuzzy: true }); // 然后可以批量绑定 tyInput.init(); // 扫描data-tyinput // 或手动绑定 tyInput.bind(document.querySelector(#search)); tyInput.bind(document.querySelector(#tag-input), { maxItems: 5 }); // 局部覆盖 /scripttyInput.config()是一个纯数据合并操作它把传入的对象Object.assign(tyInput.defaults, config)后续所有bind调用都会以这个合并后的对象为基准。这种“全局默认 局部覆盖”的模式在我们维护的12个子系统中被证明是最易维护的。3.2 配置项详解每一个参数背后都有故事tyInput的配置项不多但每个都直击痛点。以下是完整清单及我的实战解读配置项类型默认值说明我的实战建议maxItemsNumber10下拉列表最多显示几条历史记录搜索框设10-15地址栏设5-8命令行设3-5。超过15条人眼已难快速定位反而降低效率。sortStringdesc排序方式desc最新在前asc最早在前99%场景用desc。只有日志分析类系统用户想看“第一次输入什么”才用asc。fuzzyBooleantrue是否启用模糊匹配输入部分内容即筛选坚决不要关关掉等于废掉一半价值。测试过开启后用户选择历史记录的平均耗时从2.3秒降到0.7秒。storageKeyStringnull自定义localStorage存储键名。若为null则自动生成tyInput_history_${id/name/hash}当多个input需要共享同一份历史时如“搜索关键词”和“高级搜索关键词”设相同key即可。debounceNumber200输入防抖毫秒数仅影响模糊匹配的实时筛选保持默认200ms。设太小50msCPU占用高设太大500ms感觉卡顿。特别说说storageKey的妙用。我们有个CRM系统客户姓名和公司名称经常一起录入。以前两个input的历史是分开的用户输完“张三”想输“腾讯”得切到公司框再输一遍。后来我们把两个input的storageKey都设为crm-contact-history现在用户在任一框输“张”两个框的下拉列表里都会同时出现“张三”“张一鸣”“腾讯”“腾达”因为它们共享同一份历史数组。这已经超出“输入增强”变成了轻量级的“上下文感知”。3.3 样式定制如何让它长得像你家的孩子tyInput的CSS只有不到20行全部集中在JS文件末尾的tyInput.css字符串里。它不强制你引入外部CSS但提供了完整的定制入口// 修改下拉列表整体样式 tyInput.css.dropdown position: absolute; top: 100%; left: 0; right: 0; background: #fff; border: 1px solid #ddd; border-top: none; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 1000; max-height: 200px; overflow-y: auto; ; // 修改列表项样式 tyInput.css.item padding: 8px 12px; cursor: pointer; border-bottom: 1px solid #f5f5f5; font-size: 14px; ; // 修改悬停态 tyInput.css.itemHover background-color: #f0f8ff;;重点来了这些CSS字符串不是直接注入style标签而是通过element.setAttribute(style, cssString)动态添加到对应DOM元素上。这意味着你可以随时修改且修改立即生效无需刷新页面。我常用这个特性做A/B测试上午用蓝色主题#4a90e2下午切到灰色主题#666看用户点击率变化。更绝的是它可以和CSS-in-JS方案共存——我们有个React项目用styled-components封装了tyInput就是把上面的字符串当变量传进去完美融合。注意tyInput.css的所有属性都是字符串不是对象。这样设计是为了极致简单——你不需要理解CSSOM只要会写CSS就行。我见过最骚的操作是同事把tyInput.css.dropdown改成display: none !important;然后在自己项目的全局CSS里用#my-input .ty-input-dropdown { display: block; }控制显示实现了完全自定义的定位逻辑。4. 实操过程与核心环节实现4.1 从零开始5分钟完成集成附index.html逐行解析我们以最简单的index.html为例手把手带你走完第一遍。这个文件就是tyInput的“Hello World”但它比大多数框架的Hello World更有信息量。!DOCTYPE html html head meta charsetutf-8 titletyInput 基础示例/title !-- 注意这里没有引入任何CSStyInput自带样式 -- /head body !-- 这就是你要增强的输入框 -- input typetext iddemo-input placeholder点击试试看...>input typetext idcity-input placeholder选择城市...>!-- 客户姓名框 -- input typetext idcontact-name placeholder客户姓名...>[ {value:北京,timestamp:1715234567890}, {value:上海,timestamp:1715234568901}, {value:广州,timestamp:1715234569912} ]键名格式为tyInput_history_${id}如果input没有id则用name都没有则用tyInput_history_${Math.random().toString(36).substr(2, 9)}生成随机hash。这样保证了不同input的数据绝对隔离不会互相污染。自动清理tyInput不会无限增长历史。每次写入新历史时它会1. 先读取当前历史数组2. 过滤掉value为空或全是空白字符的项防止用户输空格后存入垃圾数据3. 去重如果新值已存在则只更新其timestamp不新增条目4. 截断如果数组长度超过maxItems则slice(0, maxItems)只保留最新的maxItems条。这个逻辑写在tyInput.saveHistory()函数里是原子操作。我特意测试过并发场景在两个标签页同时操作同一个input数据依然一致因为localStorage的读-改-写是同步的不会有竞态条件。手动清理接口提供两个实用方法// 清空指定input的历史 tyInput.clearHistory(document.getElementById(search-input)); // 清空所有tyInput的历史慎用 tyInput.clearAllHistory();我们在前后端好的源码收藏.html文档里专门写了清理脚本button onclicktyInput.clearAllHistory(); alert(所有历史已清空);清空全部历史/button button onclicktyInput.clearHistory(document.getElementById(debug-input));仅清空调试框历史/button这解决了测试时反复造数据的痛点。很多团队在测试环境里一个input的历史堆到200条indexOf匹配变慢这时一键清理比手动删localStorage项快得多。5. 常见问题与排查技巧实录5.1 典型问题速查表那些让你抓耳挠腮的“灵异事件”问题现象可能原因排查步骤解决方案下拉列表不出现input没有data-tyinputtrue或未调用tyInput.init()1. 打开开发者工具检查input元素是否有该属性2. 在Console里输入tyInput看是否报错3. 检查JS文件路径是否404确保属性拼写正确确认tyInput.js加载成功在/body前调用init()输入后列表不筛选始终显示全部fuzzy: false或关键词为空格1. 检查配置中fuzzy是否为true2. 在Console里输入document.getElementById(x).value看是否含不可见字符确保fuzzy: true用.trim()清理输入值历史记录不保存浏览器禁用了localStorage 或启用了无痕模式1. 在Console里输入localStorage.setItem(test,ok); localStorage.getItem(test)2. 查看Application Storage Local Storage提示用户关闭无痕模式或降级为内存存储需修改源码下拉列表位置错乱飘到页面顶部input父容器有transform或position: relative且未设z-index1. 检查input的父级元素CSS2. 在Elements面板中右键input “Scroll into view”看浮层是否跟随给input父容器加z-index: 1或修改tyInput.css.dropdown的position为fixed并计算绝对坐标同一页面多个input历史互相覆盖多个input使用了相同的id或name1. 检查HTML中所有input的id和name属性2. 在Console里输入Object.keys(localStorage).filter(k k.includes(tyInput_history))确保每个input的id或name唯一或显式指定storageKey5.2 我踩过的坑那些文档里不会写的实战经验坑一移动端iOS Safari的“虚拟键盘遮挡”问题在iPhone上当input获得焦点、虚拟键盘弹出时position: absolute的下拉列表会被键盘顶到屏幕外用户根本看不到。这个问题困扰了我们两周。最终解决方案不是改JS而是加了一行CSS/* 在全局CSS里添加 */ .ty-input-dropdown { /* iOS Safari hack: 强制在键盘上方显示 */ transform: translateZ(0); }transform: translateZ(0)会触发硬件加速让浮层脱离普通文档流iOS会将其绘制在键盘图层之上。这个技巧在indexty.html的style标签里有注释但很多开发者会忽略。坑二Vue项目中v-model绑定失效在Vue里如果你这样写input v-modelsearchQuery>// 只渲染前20条其余用滚动加载伪代码 const renderItems items.slice(0, 20); ul.innerHTML renderItems.map(...).join(); if (items.length 20) { ul.innerHTML li classty-load-more加载更多.../li; }这个优化我们放在了内部版本里没开源因为增加了复杂度。但如果你的场景真有海量历史这是必选项。5.3 性能监控与优化建议让tyInput跑得更稳tyInput自身性能很好但集成到复杂页面后可能成为性能瓶颈。我总结了三条黄金准则监控关键指标在tyInput.bind()后加一段性能打点javascript const start performance.now(); tyInput.bind(input, config); console.log(tyInput绑定耗时: ${performance.now() - start}ms);如果超过5ms说明input太多或配置太复杂考虑分批初始化。避免过度绑定不要给所有input都加data-tyinputtrue。我们有个页面有50个input结果tyInput.init()耗时12ms。后来改成只给搜索、地址、标签这三个高频输入框启用耗时降到1.2ms。内存泄漏防护tyInput会在input上绑定focus、blur、keydown事件。如果input被动态移除如Vuev-if这些事件监听器还在内存里。解决方案是在移除前手动解绑javascript // Vue的beforeUnmount钩子 beforeUnmount() { tyInput.unbind(inputElement); // tyInput提供的解绑方法 }这个unbind方法在文档里有但很容易被忽略。它会清除所有绑定的事件和__tyInputBound标记是内存管理的最后一道防线。6. 延伸思考与场景拓展6.1 从“输入历史”到“智能预测”下一步可以怎么走tyInput的核心是“记住你输过什么”这已经解决了80%的重复输入问题。但有些场景用户还没输你就该猜到他想输什么。比如搜索框用户输“北”除了“北京”“北海”是否该推荐“北京天气”“北京地铁线路图”这需要引入上下文感知。我们的做法是在tyInput:select事件里不只是记录value还记录contextinput.addEventListener(tyInput:select, function(e) { // 记录在搜索框里选择了“北京”上下文是“搜索” const context search; const historyItem { value: e.detail.value, timestamp: Date.now(), context: context, // 还可以记录来源是用户手动输入还是从历史选的 source: history-select }; // 存入localStorage键名改为 tyInput_history_search localStorage.setItem(tyInput_history_search, JSON.stringify([...old, historyItem])); });然后在匹配时优先返回context search的历史。这已经是一个轻量级的“个性化推荐”雏形不需要后端模型全靠前端数据沉淀。6.2 与后端协同如何让历史“跨设备同步”tyInput本地存储是优势也是局限。用户在家用Chrome输的“杭州西湖”在公司用Edge就没了。要解决必须引入后端。但我们不推翻现有架构而是做渐进式增强前端保持localStorage作为兜底在用户登录后发起一次GET /api/user/history拉取云端历史合并localStorage历史 云端历史 → 去重 → 按时间倒序 → 截取前maxItems条后续用户新增历史同时写入localStorage和POST /api/user/history。这个方案的好处是离线可用上线自动同步且对tyInput本身零侵入——所有逻辑都在初始化之后、tyInput.bind()之前完成。我们在一个SaaS后台里落地了这个方案用户反馈“终于不用在每个设备上重新训练输入习惯了”。6.3 最后一个小技巧用tyInput做“快捷命令面板”这是我个人最喜欢的一个hack。把tyInput的输入框做成一个悬浮按钮点击后弹出里面预置一些系统命令style #cmd-panel { position: fixed; bottom: 20px; right: 20px; width: 300px; z-index: 9999; } /style input typetext idcmd-panel placeholder输入命令: reload, clear-cache, debug... style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />简介文本输入框聚焦时自动弹出历史记录下拉列表用户之前输过的词都会被本地保存不用后端也能用。支持设置最多显示几条、按输入时间正序或倒序排列输入几个字就实时筛选出包含这些字符的历史项比如输‘北’能匹配‘北京’‘北海’‘西北’。核心逻辑封装在tyInput.js里纯原生JavaScript实现不依赖jQuery、Vue或React等任何框架直接script标签引入就能跑。附带两个演示页index.html展示默认行为indexty.html演示各种配置组合如限制条数、排序方式、禁用模糊匹配等还有一个参考文档‘前后端好的源码收藏.html’供延伸学习。整个包只有几个文件结构干净适合嵌入搜索框、地址填写、标签录入、命令行式输入等需要减少重复打字的场景。本文还有配套的精品资源点击获取