简介VirtualKeyboard是一款基于jQuery UI的虚拟键盘插件面向需要优化网页表单输入体验的Web开发者。它模拟真实键盘提供字母、数字、符号等布局支持jQuery UI多主题切换并针对触摸屏操作做了专门优化在密码输入等敏感场景下可借助虚拟键盘规避键盘记录器窃取信息提升应用安全性。插件依赖jQuery与jQuery UI主流浏览器及移动端均可使用集成时只需引入CSS/JS文件为目标input添加特定class并调用初始化方法即可。开发者还能自定义按键布局、监听键入删除等事件或通过.open/.close、.option等方法动态控制键盘行为满足多种业务定制。整个资源压缩包约1.27MB适合为现有Web项目快速补充安全输入方案目前已有296人浏览学习对需要兼顾交互体验与安全防护的前端技术人员而言是一份即取即用的参考实现。1. 从工控机到网页登录框VirtualKeyboard 解决的是什么场景有一次我面对一台触摸屏工控机操作员戴着手套系统要求输入工号才能登录。物理键盘放在一米开外系统自带的软键盘又和网页样式完全脱节用户每次要低头找键再抬头核对屏幕效率低到没法看。这时候在页面上挂一层基于 jQuery UI 的虚拟键盘组件——就是标题里说的 VirtualKeyboard 这类实现——能把输入这件事完全留在网页里点击屏幕上的按键往输入框里填字符视觉风格跟着页面主题走不受操作系统输入法干扰。它适合的场景很集中触摸屏交互、公共查询机、密码输入、以及不想让系统软键盘弹出来的内嵌页面。真正用起来之后你会发现最大的坑不在键盘渲染而在键盘与输入框之间的焦点和事件转发。2. 为什么基于 jQuery UI 而不是从零手写组件骨架与事件模型的四个理由2.1 三种实现路线与选型边界原生手写、现成插件、jQuery UI 扩展做虚拟键盘第一条岔路就是选实现路线。常见做法有三种原生 JavaScript 从零写、直接找一个现成 jQuery 插件、基于 jQuery UI 的 widget factory 扩展一个组件。三者的差别不在代码量而在后面接需求时的成本。实现路线优点代价适合场景原生 JS 手写无依赖、体积最小事件管理、销毁、主题全部自己造只有一两个输入框、样式要求低现成 jQuery 插件开箱即用、功能全行为固定、二次定制要改源码需求标准、无特殊交互基于 jQuery UI 扩展有 widget 规范、主题体系、按钮组件依赖 jQuery jQuery UI项目本来就在用 jQuery UI需要长期维护如果页面里已经引着 jQuery UI那我基本不做第二选择。原因有四widget factory 替你处理了组件生命周期从创建到销毁都有固定钩子主题机制和页面其他控件能保持一套皮肤不用单独维护键盘的配色变量按钮、对话框这类现成组件可以直接复用省掉重复造轮子的时间事件绑定方式统一团队接手时心智负担小。这个判断放到今天依然成立——项目技术栈是 jQuery 全家桶时再引入一套现代组件库反而是负担虚拟键盘本身不重重的是它和现有页面的融合成本。2.2 用 widget factory 搭建可复用组件骨架一个最小组件长什么样基于 jQuery UI 做插件第一步不是写按键而是先定义一个 widget。jQuery UI 的 widget factory 会帮我们把 options 合并、链式调用、实例销毁这些重复劳动处理掉。我一般先搭一个最小骨架确认组件能创建、能销毁再往里填键盘逻辑$.widget(ui.virtualKeyboard, { // options 里放所有可配置项用户传入的值会与默认值合并 options: { layout: qwerty, // 键位布局名 theme: default, // 皮肤名对应一组 CSS class appendTo: body, // 键盘容器挂到哪个节点下 visible: false, // 初始化后是否直接显示 animationSpeed: 150 // 显示/隐藏动画时长单位 ms }, // _create 只在组件创建时执行一次负责搭 DOM、绑事件 _create: function () { this.panel $(div).addClass(vk-panel vk-theme- this.options.theme); this._renderLayout(); this._bindEvents(); this._setOption(visible, this.options.visible); }, // 销毁时把生成的面板和事件都清掉避免重复初始化时 DOM 堆积 _destroy: function () { this.panel.remove(); } });创建实例的方式是$(#wrapper).virtualKeyboard({ layout: myLayout })之后可以随时用$(#wrapper).virtualKeyboard(option, animationSpeed, 200)改参数或者$(#wrapper).virtualKeyboard(destroy)把组件拆掉。这段代码里有几个值得注意的参数。appendTo决定键盘挂载位置默认放 body 是为了避免被输入框父级的 overflow:hidden 裁剪visible如果默认 true页面加载时会立刻弹键盘遮挡首屏animationSpeed不是越快越好移动端触摸响应本来就有延迟太快反而显得按键没有确认感。widget factory 还帮我们保证重复调用virtualKeyboard()不会创建两个实例这在单页应用里很关键。2.3 为什么按键用 mousedown 而不是 click焦点、延迟与事件委托按键交互的事件选择是这个组件的第一个大坑。很多人第一反应是绑 click但在虚拟键盘场景里 click 有三个问题。移动端浏览器对 click 有约 300ms 延迟触摸结束才触发用户体验上像「按了没反应」鼠标点击键盘时mousedown 阶段的默认行为会把焦点从当前输入框移走等 click 触发时输入框已经失焦后面所有光标操作全乱。我一般会这样绑事件_bindEvents: function () { var self this; // 用事件委托监听面板内所有 .vk-key后续动态添加按键也无需重新绑定 this.panel.on(mousedown touchstart, .vk-key, function (e) { e.preventDefault(); // 阻止默认行为保住输入框焦点 var keyCode $(this).data(code); self._trigger(keypress, null, { code: keyCode }); self._pressKey(keyCode); }); // 触摸设备上 touchstart 和 mousedown 都会触发加个标记做防抖 this.panel.on(touchstart, .vk-key, function () { self._touchFired true; }); this.panel.on(mousedown, .vk-key, function () { if (self._touchFired) { self._touchFired false; return; } // 鼠标路径继续处理 }); }事件委托的写法把监听挂在面板容器上子节点用.vk-key做选择器过滤后面用布局数组渲染出新的按键不用再单独on()一次。参数上>var qwertyLayout [ [, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, -, , { key: backspace, label: ⌫, cls: vk-wide }], [{ key: tab, label: Tab, cls: vk-fn }, q, w, e, r, t, y, u, i, o, p, [, ], { key: enter, label: ⏎, cls: vk-fn }], [caps, a, s, d, f, g, h, j, k, l, ;, , { key: shift, label: ⇧, cls: vk-fn }], [Ctrl, Win, Alt, space, Alt, Win, menu, Ctrl] ]; function renderLayout(layout) { return $.map(layout, function (row) { var $row $(div).addClass(vk-row); $.each(row, function (idx, keyDef) { // 字符串键直接使用自身作为编码和显示文本 var def typeof keyDef string ? { key: keyDef, label: keyDef } : keyDef; $(span) .addClass(vk-key (def.cls || )) .attr(data-code, def.key) .text(def.label) .appendTo($row); }); return $row; }); }这段渲染逻辑里>function insertTextAtCursor(input, text) { var start input.selectionStart; var end input.selectionEnd; var value input.value; // 直接操作 DOM 的 value 属性绕开 jQuery 的缓存机制 input.value value.slice(0, start) text value.slice(end); // 光标放在插入文本之后方便连续输入 input.selectionStart input.selectionEnd start text.length; // 手动触发 input 事件让依赖 v-model 或 change 的逻辑能感知 $(input).trigger(input); } function deleteAtCursor(input) { var start input.selectionStart; var end input.selectionEnd; // 未选中文本时默认删除光标前一个字符 if (start end start 0) { start - 1; } input.value input.value.slice(0, start) input.value.slice(end); input.selectionStart input.selectionEnd start; $(input).trigger(input); }两个函数的关键点是一致的先用原生selectionStart和selectionEnd记录光标改完值以后按同样规则写回去再触发一次input事件。为什么不用$(input).val()因为 jQuery 的 val() 在读写时会走它自己的值缓存某些情况下会把光标位置重置到末尾。虚拟键盘这种高频操作对光标位置极其敏感一次错位用户立刻就能察觉。手动触发input事件也很重要——很多前端框架对输入框的响应依赖这个事件不触发的话你这边值改了页面上绑定的数据流还没反应过来。如果你要兼容很老的浏览器document.execCommand(insertText)可以作为兜底方案但它已经被标记为废弃新代码不建议押注。3.3 与系统输入法共存readonly 切换与输入法之争虚拟键盘和系统输入法的关系是接入时绕不开的第二个大坑。逻辑上只有两种情况用虚拟键盘就不让系统软键盘弹出来让系统键盘接管就关闭虚拟键盘。两者不能同时作用在同一个输入框上否则按下物理键盘的字符和点击虚拟键盘的字符会互相干扰。我一般这样实现切换_vkShow: function () { // readonly 可以阻止系统软键盘弹出但不会让输入框失焦 this.target.prop(readonly, true); this._moveCursorToEnd(); this.panel.show(); }, _vkHide: function () { this.panel.hide(); // 恢复可写同时把焦点还给输入框 this.target.prop(readonly, false); this.target.trigger(focus); }readonly这个属性是关键。它和disabled不一样disabled会让输入框完全不可交互focus 都拿不到readonly只是不让键盘往里输入焦点、光标、选中状态都还在。配合_moveCursorToEnd把光标挪到末尾用户从系统键盘切到虚拟键盘时体验是连续的。有的实现会直接用blur()把焦点弹走再让虚拟键盘接管输入但那样做会让页面上其它依赖 focus 的逻辑全部失联排查起来很麻烦我一般不建议。这里还要看一个边界虚拟键盘只适合接管「字符输入」这件事。如果输入框需要配合中文输入法打拼音又需要虚拟键盘做辅助正确做法不是硬拼而是提供显式切换——虚拟键盘开启时把输入框设为 readonly用户点输入框时先弹系统输入法确认要用手写键位时再切回来。两个输入体系做好互斥比试图让它们共存要省心得多。4. 接入避坑VirtualKeyboard 高频翻车点与排查思路4.1 按键没反应事件绑定在哪儿、被谁卸载了现象页面跑起来键盘显示正常点任何键都没有反应控制台没报错。这时候先别怀疑渲染问题大概率出在事件绑定上。原因最常见的有三个键盘面板被创建了两次后一次覆盖了前一次的事件某个页面脚本在初始化时调用了off()把面板上的委托事件一并解掉事件绑定在了$(#container)上但渲染出来的面板挂到了body下两者根本不是同一个节点。排查时我会直接打开控制台看事件绑定详情// 查看面板节点上实际绑定的事件确认委托监听是否还在 console.log($._data($(.vk-panel)[0], events)); // 如果输出 undefined 或空对象说明事件在某个环节被解绑了解决方法是统一事件的绑定时机和挂载点。把_bindEvents放到_create里执行保证面板生成后立刻绑定挂载点固定使用options.appendTo指定的容器。如果项目里有人做全局事件清理要叮嘱他先查一下事件命名空间jQuery UI 的 widget 事件通常带有.virtualKeyboard这样的命名空间清理时用带命名空间的off()才安全。还有一种隐蔽情况面板的 HTML 被整体替换过比如$(#wrapper).html(...)把面板连同事件一起换了页面重新virtualKeyboard()初始化后要确认_create里没有残留旧面板的引用。4.2 点一下光标就丢焦点转移与 selection 错乱现象鼠标点击虚拟键盘按键时目标输入框的蓝色光标立刻消失输入几个字符后内容顺序是乱的。原因是点击事件发生在输入框之外mousedown 的默认行为把焦点从输入框转移到了别处后续你在代码里读写selectionStart拿到的都是 0。这个问题的解法在 2.3 节已经埋下mousedown 里要e.preventDefault()。如果漏了这一步返工成本很高。this.panel.on(mousedown, .vk-key, function (e) { e.preventDefault(); // 阻止焦点转移输入框始终保持 focus // 后续操作 selectionStart 才能拿到正确的位置 insertTextAtCursor(self.target[0], code); });如果已经出现光标丢失应急办法是每次按键后重新 focus 并恢复光标位置self.target.trigger(focus)然后把selectionStart手动设置为记录值。说到底虚拟键盘所有输入操作的前提是目标输入框不能失焦这个前提一旦破坏后面所有逻辑都是空中楼阁。我见过有人为了处理失焦写了很复杂的光标记录系统其实最开始挡住 mousedown 默认行为就没事了。4.3 中文输入法弹出组合拼音直接漏进输入框现象在触摸屏的浏览器里打开页面虚拟键盘显示正常但用户一点输入框系统自带的输入法也跟着弹出来屏幕上两个键盘打架。更糟的情况是中文输入法的拼音组合串直接进了输入框还没选字光标位置已经乱了。根因就是第 3.3 节说的互斥没做好——输入框保持可编辑状态系统自然认为你可以接收物理输入。解决的核心动作是在虚拟键盘唤起时把输入框设为 readonly并且要在用户点击输入框之前就设置好等系统输入法弹出来再改就晚了。另外如果页面同时有物理键盘用户得加一个判断检测到keydown事件时自动收起虚拟键盘把输入权交还给物理键盘。这套策略做到了中文输入法和虚拟键盘就能各自工作而不互相干扰。4.4 换肤后尺寸错乱CSS 盒模型和主题样式打架现象默认皮肤下键盘整整齐齐切换主题之后按键重叠、键盘宽度溢出、高度挤成一团。换个主题只是换颜色正常不该动布局所以问题一定出在 CSS 结构上。最常见的元凶是两套样式同时定义了按键的宽度和 padding主题自带的按钮样式给.vk-key加了内边距你自己写的布局样式又给同一个元素设置了固定宽度或 height两套盒模型叠加后实际占位完全失控。解决方式是先统一盒模型再做尺寸分层.vk-panel, .vk-panel * { box-sizing: border-box; } .vk-row { display: flex; margin: 2px 0; } .vk-key { flex: 1 1 auto; padding: 6px 8px; /* 视觉内边距不影响布局宽度 */ } .vk-key.vk-wide { flex: 1.5 1 auto; /* 退格、空格这类键按比例加宽 */ }注意vk-wide和固定宽度的区别它控制的是 flex 伸缩比例不是固定像素这样键盘整体宽度变化时按键能成比例缩放。主题 CSS 里的颜色和阴影样式放在另一层不碰尺寸。这样换肤时布局样式保持稳定主题只管视觉表现问题自然消散。还有一个细节是字体大小也属于尺寸换肤后字体变大、按键没跟着变看起来像整体缩放错位所以我习惯把字体大小也写到布局层主题层只改颜色和背景。4.5 快速连点重复输入双事件触发与动画堆积现象用户快速连续点击同一个键有时出现两个字符有时只出现一个但键盘有明显的卡顿动画堆积。这两个副作用分别来自两个根因。一个是触屏设备上 mousedown 和 touchstart 都被绑定了或者绑定了 click 和 mousedown一次触摸触发了两次输入。另一个是视觉上的按下的动画还没执行完下一次点击又触发新动画jQuery 的animate()把两次动画排队了。处理方法是加防抖标志位动画用 CSS class 切换_pressKey: function (code, event) { var self this; // 一次物理点击只允许一次输入用标志位防止双事件 if (this._busy) { return; } this._busy true; setTimeout(function () { self._busy false; }, 120); // 动画用 class 切换不用 animate()避免排队堆积 this.panel.find(.vk-key.active).removeClass(active); $(event.target).addClass(active); insertTextAtCursor(this.target[0], this._getText(code)); }_busy标志位是最简单有效的防抖方案时间窗口设为 120ms 不会让人觉得按键失灵也能覆盖一次物理点击的双事件间隔。按键回弹动画不要用 jQuery 的animate()去做直接切换 CSS class 配合transition: transform 80ms性能更好也不存在排队问题。另外注意animationSpeed和 CSS transition 时长是两个不同参数前者管键盘弹出收起后者管按键回弹搞混了会出现弹出很快、按键回弹很慢的割裂感。排查这类问题先抓事件是否双触发再抓动画是否堆积别一上来就改全局配置。5. 进阶让 VirtualKeyboard 从「能用」变成「好用」的三个方向5.1 安全键盘随机布局与输入结束不留痕普通键盘满足功能安全键盘满足的是「防窥探」。常见做法是每次显示键盘前把字母键和数字键的位置随机打乱一次防的就是屏幕录制和肩部偷看。实现时不要直接在布局数组上原地打乱把字母数字抽出来洗牌后重新生成布局再触发一次渲染这样每次弹出的键盘位置都不同。配合输入结束后清空输入框值和 value 缓存能降低密码类输入被还原的风险。随机布局对用户的影响是肌肉记忆失效所以只建议在密码输入、支付确认这类短输入场景开启长文本输入不要用。5.2 密码框回显只显示最后一位的方案密码输入场景有个体验技巧用户按下一个键时密码框只短暂显示最后一位然后变回圆点让用户确认自己按的是对的。实现时在按键按下后把 input 的 type 从password临时切换为text80ms 后再切回去。注意切换 type 本身可能影响光标位置所以要在这两步中间把selectionStart重新设置好。这个交互在密码键盘上基本是必须项否则用户盲打输错一位只能全部删掉重来体验很差。5.3 上线前自查清单与我踩过的第一个坑接完 VirtualKeyboard 后我习惯按清单过一遍触摸屏真机点一遍每个功能键外接物理键盘输入时虚拟键盘要自动收起输入框被 readonly 后光标仍在原地换肤后键盘布局不打乱快速连点 20 次同一个键不出现重复字符。最后一条最有感触我第一次做虚拟键盘时图省事全用 click交付后在真机上被现场反馈手指点快了就丢字查下来就是两个原因叠加click 延迟和 touch/mouse 双事件触发。那次之后我养成一个习惯——接虚拟键盘先查 mousedown 的默认行为有没有被阻止再查事件有没有被双绑最后查动画是不是走了 jQuery 队列三件事查完八成问题都定位了。希望这些经验能帮到你接入虚拟键盘时少走这一段弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?