简介:VirtualKeyboard是一款基于jQuery UI的虚拟键盘插件,面向需要提升网页表单输入体验的前端开发者,适用于桌面端与移动端的安全输入场景。它提供接近真实键盘的交互界面,支持字母、数字、符号等不同布局,可复用jQuery UI多主题样式,并为触摸屏设备做了优化,在密码等敏感输入场景下能有效防范键盘记录器窃取信息。兼容主流浏览器及IE9+,同时适配jQuery Mobile,可平滑运行于手机和平板上。压缩包约1.27MB,已有296人学习下载。资源内含插件完整文件与使用说明,集成时引入jQuery、jQuery UI后,为输入框添加指定class并调用$("#inputElement").virtualKeyboard()即可启动。插件支持自定义键盘布局、动态设置选项、监听键入手势,并开放.open()、.close()、.show()、.option()等API,便于按需扩展,是构建高可用表单交互的实用工具。
1. 虚拟键盘在表单输入控件里的真实位置:不是画键帽,是管焦点和输入语义
虚拟键盘在 JavaScript 开发里不算高频需求,但当它落到表单输入控件这个位置时,坑比看上去多得多。我第一次在触屏收银机上接 VirtualKeyboard(一款基于 jQuery UI 的插件),原以为只是把输入框换成软键盘,结果第一天就连续翻车:按键一点,输入框失焦,键盘跟着隐藏;输入法弹拼音,软键盘却拿不到字符。后来才想明白,这类插件真正难的不是画键帽,而是把“按键序列变成输入框 value”整条语义理顺。这篇文章直接复现一套能落地的方案:初始化、自定义中英文布局、五个必踩坑的绕法,以及最后把键盘接回表单验证的完整闭环。适合正在做自助终端、收银系统、车载大屏这类触屏表单的前端开发者。
2. 初始化与内置布局:五步把软键盘挂到表单上
2.1 插件架构:jQuery UI widget 机制与最小依赖
VirtualKeyboard 是基于 jQuery UI Widget Factory 构建的组件,注册的 widget 名是ui.virtualKeyboard。这意味着它的生命周期、事件命名、销毁逻辑都遵循 jQuery UI 那套约定:实例挂在 DOM 元素上,方法通过$(el).keyboard('methodName')调用,事件用keyboard + EventName命名。这样做的好处是,多个输入框可以同时存在各自的键盘实例,互不污染。
依赖方面,实际接项目时我一般只引入三样:jQuery 本体、jQuery UI 的 core 和 widget 模块。如果要用到键盘面板相对输入框自动定位,还会把 position 模块带上;如果要做气泡提示,tooltip 模块可按需引入。渲染方式上,插件会在初始化时往appendTo指定的容器里插入一层div.ui-keyboard,默认挂在 body 下,但多实例或弹窗场景下强烈建议指定容器。
最小化接入代码大概是这样:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="css/ui.keyboard.css"> </head> <body> <input id="nameInput" placeholder="触屏输入"> <script src="js/jquery.min.js"></script> <script src="js/jquery-ui.min.js"></script> <script src="js/jquery.keyboard.js"></script> <script> $(function () { $('#nameInput').keyboard(); }); </script> </body> </html>逻辑说明:插件初始化后会自动读取 input 的现有 value,并且监听物理键盘事件。代码里没有传任何选项,它就会用默认布局和默认行为,适合先验证资源能不能跑通。参数说明:css/ui.keyboard.css是该插件的核心皮肤样式,后面自定义按钮尺寸、配色都基于这层覆盖。
2.2 初始化与事件绑定:从一行代码到完整表单接入
真实项目里不能只调一个空构造。下面这组配置是我在收银类页面上常用的基底:
$('#orderInput').keyboard({ layout: 'qwerty', usePreview: false, autoAccept: false, appendTo: $('#keyboard-container'), initialFocus: true, repeat: true, repeatDelay: 300, repeatRate: 120, beforeClose: function (e, keyboard, el) { var val = $(el).val(); if (val.length < 4) { e.preventDefault(); $('#kb-error').text('金额至少 4 位'); } } });逻辑说明:beforeClose在键盘要隐藏前触发,这里拦截了一次非法关闭,避免用户没填完整就把键盘收起来。参数说明:layout指定了内置 qwerty 布局;usePreview: false表示不让插件在原输入框上方再叠加一个预览层,自助终端上多一层反而容易挡住按钮;appendTo把键盘固定在指定容器里,而不是默认挂在 body 尾部;repeat: true开启长按重复,配合repeatDelay和repeatRate控制首次延迟与后续重复频率,这两个值直接影响退格键的手感。
事件绑定是切入业务的关键。插件抛出的change事件会在键盘输入导致 value 变化时触发,但它不是原生的input事件:
$('#orderInput').on('keyboardchange', function (e, keyboard, el) { $('#displayVal').text($(el).val()); });逻辑说明:回调里的el才是真正被键盘写入的输入框,keyboard是插件实例,可以用来在后续调用getVal()。参数说明:事件名是keyboard前缀加上原生change,不要写成change,否则在虚拟键盘录入时压根不会触发。
2.3 内置布局与参数细节:layout、usePreview、repeat 怎么配合
插件内置了 qwerty、azerty、dvorak、alpha 和数字键盘等布局,layout参数值和布局名的映射直接写在插件源码里。qwerty 布局适合英文和大多数场景;数字键盘布局适合密码输入、单行手机号输入。选择依据很简单:只输数字就选numpad,会切英文就选qwerty,中文场景基本都从 qwerty 改。
usePreview 和 autoAccept 是容易混淆的一对。usePreview 控制「显示层」,autoAccept 控制「值何时写回原输入框」。当 usePreview 为 true、autoAccept 为 false 时,键盘上方会有一个独立展示区,输入的内容先存在展示区里,点击 accept 按钮后才真正写入原输入框。这个模式适合需要二次确认的支付密码场景。反过来,usePreview 为 false、autoAccept 为 true 时,每次按键都直接写回原输入框,更像普通键盘。
repeat 参数的坑在低端设备上尤其明显。repeatRate 单位是毫秒,表示重复触发间隔,数字越小重复越快。我在两台配置不一样的平板上对比过,repeatRate 从 80 调到 120,长按退格的掉帧感就没了,用户也不会觉得迟钝。
| 参数名 | 默认值 | 建议值(触屏收银场景) | 作用 |
|---|---|---|---|
| layout | qwerty | numpad或qwerty | 核心键盘布局 |
| appendTo | body | 指定容器 | 防止定位错乱 |
| usePreview | true | false | 是否显示预览层 |
| autoAccept | true | false | 是否自动写回原 input |
| initialFocus | true | true | 打开后是否聚焦输入框 |
| repeat | false | true | 长按重复 |
| repeatRate | 100 | 120 | 重复触发间隔 |
3. 自定义布局与中文输入:改 key 数组就能换皮肤
3.1 布局数据结构:customLayout 的 rows 与按键语义
真正要把软键盘用出生产力,就不能只用内置布局。插件的customLayout接受一个对象,对象里每个 key 代表一层布局,最常用的是default和shift两层:
$('#searchInput').keyboard({ layout: 'custom', customLayout: { 'default': [ '1 2 3 4 5 6 7 8 9 0 - = {b}', '{tab} q w e r t y u i o p [ ] \\', '{lock} a s d f g h j k l ; \' {enter}', '{shift} z x c v b n m , . / {shift}', '{cancel} {space} {accept}' ], 'shift': [ '! @ # $ % ^ & * ( ) _ + {b}', '{tab} Q W E R T Y U I O P { } |', '{lock} A S D F G H J K L : " {enter}', '{shift} Z X C V B N M < > ? {shift}', '{cancel} {space} {accept}' ] } });逻辑说明:每一行是字符串数组里的一个元素,键位用空格分隔,带花括号的是功能键。插件在渲染 shift 布局时,会同时读取 default 里同位置的普通键来做大小写切换展示。参数说明:{b}是退格,{tab}是制表,{lock}是大小写锁定,{shift}是临时切换,{cancel}和{accept}分别对应关闭和确定,{space}是空格。
如果某个键在 Shift 状态下需要输出不同字符,可以不用整层布局,只在这个键位放对象:
'default': [ '1 2 3 4 5 6 7 8 9 0 - = {b}', "{tab} q w e {k: '6', s: '⑥'} r t y u i o p [ ] \\" ]逻辑说明:k是键帽上显示的内容,s是 Shift 按下后输出的内容。这个写法适合处理各种特殊符号,不用为个别字符复制一整层布局。
3.2 中英文切换实现:给输入框塞字符串的两种方式
插件本身不带输入法引擎,它只会把按下的字符写进输入框。我一般的做法是先决定输入形态:如果只接受英文和数字,让 Shift 直接切换大小写就够了;如果接受中文,就自己加一个「中/英」切换键,配合预置候选或系统输入法。
常见做法是单独定义一套中文布局,把常用的中文标点、拼音字母放在键位上。点击中英切换按钮时,用插件实例的方法重新加载布局,而不是销毁重建:
var kbInstance = null; function buildKeyboard() { kbInstance = $('#nameInput').keyboard({ layout: 'custom', customLayout: getLayout('en'), change: function (e, keyboard, el) { $('#hiddenName').val($(el).val()); } }); } $('#langToggle').on('click', function () { var currentLang = $(this).data('lang'); var newLayout = getLayout(currentLang === 'en' ? 'zh' : 'en'); $('#nameInput').keyboard('destroy'); $('#nameInput').keyboard({ layout: 'custom', customLayout: newLayout }); $(this).data('lang', currentLang === 'en' ? 'zh' : 'en'); });逻辑说明:切换布局最简单的方式是先销毁再重建,因为布局结构直接决定渲染结果,动态改customLayout要重新触发布局解析,不如重建干净。参数说明:getLayout是一个返回布局对象的函数,实际项目里可以把中英文两套布局都放在独立 js 文件里,方便维护。
中文输入场景里,光标位置是另一个隐藏问题。虚拟键盘不像物理键盘有原生光标行为,直接往 value 尾部追加字符会打乱用户已经移动过的光标位置。下面这个函数处理了光标插入:
function insertCharAtCursor(input, char) { var el = $(input)[0]; var start = el.selectionStart; var end = el.selectionEnd; var old = el.value; el.value = old.substring(0, start) + char + old.substring(end, old.length); el.setSelectionRange(start + char.length, start + char.length); $(input).trigger('keyboardchange'); }逻辑说明:先把光标起止位置记下来,再把新字符拼进去,最后把光标移到插入点之后。参数说明:selectionStart和selectionEnd是原生 DOM 属性,在触屏软键盘场景里也有效;手动触发keyboardchange是为了让监听该事件的业务代码同步更新。
删键也不能简单地value = value.slice(0, -1),遇到 emoji 或生僻字会拆出半个字符:
function deleteCharAtCursor(input) { var el = $(input)[0]; var start = el.selectionStart; if (start <= 0) return; var old = el.value; var code = old.charCodeAt(start - 1); if (code >= 0xD800 && code <= 0xDBFF && start >= 2) { el.value = old.slice(0, start - 2) + old.slice(start); el.setSelectionRange(start - 2, start - 2); } else { el.value = old.slice(0, start - 1) + old.slice(start); el.setSelectionRange(start - 1, start - 1); } }逻辑说明:UTF-16 里 emoji 由两个码元组成,高位码元范围在 0xD800 到 0xDBFF,删除时往前多看一位。参数说明:charCodeAt返回前一个字符的码元值,命中代理对就一次删两位。
3.3 样式定制:CSS 变量与类名覆盖
插件自带主题里有一组以ui-keyboard-开头的类名,覆盖它们比直接改插件 css 文件更安全,因为升级版本时不会被冲掉。一般我会先把整体边框、圆角、阴影做一轮覆盖:
.ui-keyboard { font-family: "PingFang SC", "Microsoft YaHei", sans-serif; border-radius: 10px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); background: #f5f5f5; padding: 6px; } .ui-keyboard-button { border-radius: 6px; border: 1px solid #d0d0d0; background: linear-gradient(#fff, #f0f0f0); touch-action: manipulation; font-size: 16px; min-width: 42px; height: 44px; } .ui-keyboard-button.ui-keyboard-accept { background: #2d8f6f; color: #fff; border-color: #237054; } .ui-keyboard-button.ui-keyboard-cancel { background: #b34a4a; color: #fff; border-color: #8f3b3b; }逻辑说明:touch-action: manipulation能消掉移动端双击放大和部分 300ms 延迟。参数说明:min-width和height直接决定触摸命中区域,收银终端上小于 40px 高的键位误触率会明显上升。背景用线性渐变是模仿物理按键的立体感,追求扁平风格就去掉渐变只留纯色。
4. 排查与避坑:五个必踩的兼容与性能坑
4.1 坑一:点按钮输入框就失焦,键盘被自己关掉
现象:在触屏上一按键,输入框光标立刻消失,键盘面板跟着收起,连续输入无法进行。
原因:mousedown 触发时,输入框默认失去焦点,插件监听到 blur 后执行了关闭逻辑。而触屏设备上 mousedown 和 click 间隔很短,用户还没看到第二次按键,键盘已经关了。
解决:在键盘容器上阻止 mousedown 的默认行为,让焦点不转移:
$('#keyboard-container').on('mousedown touchstart', function (e) { e.preventDefault(); });逻辑说明:preventDefault只拦截焦点转移,不会阻止 click 事件,所以按键功能不受影响。参数说明:同时绑定touchstart是覆盖触屏设备的兼容写法,但不建议在里面直接用stopPropagation,那会影响按钮事件冒泡。
4.2 坑二:中文输入法状态下,keypress 只拿到字母,拿不到拼音
现象:原生输入法开启时,软键盘按下去,输入框里出现的是字母,而不是拼音组合后的候选字;或者同一个拼音被重复插入好几次。
原因:软键盘点击触发的字符写入走的是普通 input 流程,而输入法的 composition 事件流没有被插件接管。两者交错时,compositionend 还没结束,value 已经被插件改掉了。
解决:用 composition 状态锁住写入逻辑:
var isComposing = false; $('#nameInput').on('compositionstart', function () { isComposing = true; }); $('#nameInput').on('compositionend', function () { isComposing = false; }); $(document).on('keyboardchange', function (e, keyboard, el) { if (isComposing) return; $('#hiddenName').val($(el).val()); });逻辑说明:compositionstart 到 compositionend 之间是输入法独占的时间窗,这期间虚拟键盘的每次写入都先被挡掉。参数说明:isComposing是闭包变量,多个实例共用时请放各自的闭包作用域里,不要挂在 window 上。
4.3 坑三:长按退格键把低端机卡成幻灯片
现象:在低配平板上长按退格删除一串字符,界面明显掉帧,键盘按键闪烁。
原因:repeat 开启后,每次重复触发都会做一次完整的值读取、DOM 更新和事件广播,默认 repeatRate 过快时,短时间内的 DOM 操作量直接拉满。
解决:把重复间隔调宽,并减少 repeat 期间的事件广播:
$('#numberInput').keyboard({ repeat: true, repeatDelay: 350, repeatRate: 150, change: function (e, keyboard, el) { if (keyboard.repeating) return; $('#hiddenNumber').val($(el).val()); } });逻辑说明:keyboard.repeating是插件在 repeat 循环期间暴露的标志位,用它跳过不必要的业务同步。参数说明:repeatDelay 是长按多久后开始重复,repeatRate 是相邻两次重复的间隔,两个值相乘大约就是每秒重复次数,150ms 对应每秒六次多,删除效率能接受且低端机带得动。
4.4 坑四:多个实例互相抢事件,A 输入框被 B 的键盘改写
现象:页面有两个输入框和两个键盘,给第一个输入框输入时,第二个输入框的 value 也跟着变;或者一个键盘收起把另一个也带没了。
原因:事件绑在了 document 级联上,回调里没有判断触发源,任何键盘的 change 都广播给了所有监听器。
解决:每个实例自己绑定独立回调,并在回调里校验输入框身份:
$('#inputA').keyboard({ change: function (e, keyboard, el) { if (!$(el).is('#inputA')) return; $('#hiddenA').val($(el).val()); } }); $('#inputB').keyboard({ change: function (e, keyboard, el) { if (!$(el).is('#inputB')) return; $('#hiddenB').val($(el).val()); } });逻辑说明:插件实例的作用域本来就该是隔离的,问题出在外部监听器上。参数说明:$(el).is()是 jQuery 选择器匹配,比直接比对 id 字符串更稳,因为 el 在部分事件回调里已经是原生节点对象。
4.5 坑五:平板旋转后键盘错位,按钮被拉变形
现象:横竖屏切换后,键盘宽度没有跟随容器重排,右半边超出屏幕或者按钮被压成条状。
原因:键盘初始化的宽度是按打开那一刻的视口计算的,旋转后没有重新计算;如果 container 用了固定像素宽度,也会在旋转后溢出。
解决:监听 resize 和 orientationchange,强制重设键盘宽度:
function resizeKeyboard() { var containerWidth = $('#keyboard-container').width(); var keyboard = $('#keyboard-container .ui-keyboard'); keyboard.width(containerWidth - 12); } $(window).on('resize orientationchange', function () { resizeKeyboard(); });逻辑说明:.ui-keyboard是插件渲染出的根节点,直接改它的宽度触发浏览器重排,按钮宽度是按百分比继承的,会跟着自适应。参数说明:减去 12px 给容器留出左右两边的边距,避免键盘贴边。注意这个函数要在键盘已渲染后再调用,初始化前执行会拿到空集合。
5. 进阶验证:把软键盘接回表单校验与提交闭环
5.1 在 keyboardchange 里做即时校验,别等提交按钮
虚拟键盘的写入路径和原生 input 事件不完全重合,所以校验逻辑最好挂在keyboardchange上:
$('#passwordInput').keyboard({ layout: 'numpad', change: function (e, keyboard, el) { var v = $(el).val(); if (v.length < 6) { $('#kbPasswordError').text('密码至少 6 位').show(); $(el).addClass('is-invalid'); } else { $('#kbPasswordError').hide(); $(el).removeClass('is-invalid'); } } });逻辑说明:每次按键都会触发 change,校验状态实时更新,用户不用等到最后点提交才发现错误。参数说明:is-invalid是你自己项目里已有的校验类名,保持和物理键盘输入时的表现一致,不要为软键盘单独造一套样式。
5.2 提交前统一走 getVal,绕过 input.value 的隐藏坑
有些场景里 input 设置了 readonly 做展示,真实值存在插件内部的 preview 面板里,这时候直接读.val()拿到的是空串。提交前统一从实例取值:
$('#payForm').on('submit', function (e) { var kb = $('#amountInput').keyboard('getInstance'); var value = kb ? kb.getVal() : $('#amountInput').val(); $('#hiddenAmount').val(value); });逻辑说明:getInstance返回插件实例,getVal是插件提供的方法,它比直接读 input.value 更稳,会自动处理 preview 模式和 autoAccept 组合的各种分支。参数说明:这里的hiddenAmount才是真正随表单提交的字段,input 本身只负责展示和交互。
5.3 封装一个初始化函数,把 destroy、setVal、getVal 收口
项目里用到虚拟键盘的页面多了以后,每个页面复制一坨初始化脚本会让后续维护很痛苦。我一般封装成一个小函数:
window.createVirtualKeyboard = function (selector, options) { var $el = $(selector); $el.keyboard($.extend({ layout: 'custom', usePreview: false, autoAccept: true, appendTo: '#keyboard-container', change: function (e, keyboard, el) { var name = $el.attr('id'); var hidden = $('#hidden-' + name); if (hidden.length) hidden.val($(el).val()); } }, options || {})); return { instance: function () { return $el.keyboard('getInstance'); }, getVal: function () { return $el.keyboard('getInstance').getVal(); }, reset: function () { $el.keyboard('destroy').val('').keyboard(); } }; };逻辑说明:把通用参数内置好,业务只需要传特殊布局和额外回调,返回对象统一暴露三个方法,页面代码就不用直接碰插件 API 了。参数说明:reset里先keyboard('destroy')销毁旧实例再重新初始化,是处理键盘状态残留的常规动作。
从那以后,我每次接 VirtualKeyboard 都强制走一遍排查流程:先测按键焦点,再开输入法测 composition,再长按退格看卡顿,再旋转屏幕看布局,最后才放提交按钮。这套流程帮我挡掉了不少现场问题,希望帮到你。
本文还有配套的精品资源,点击获取