简介:VirtualKeyboard是一款基于jQuery UI的可定制虚拟键盘插件,适合需要统一桌面端与移动端输入体验的前端开发者,也适用于密码输入等对安全性要求较高的表单场景。它通过模拟实体键盘布局,降低键盘记录类恶意软件窃取敏感信息的风险,并解决了触摸屏设备上的输入效率问题。该插件以zip压缩包提供,体积约1.27MB,内含JS与CSS文件,可直接引入现有jQuery项目;目前已有296人学习/下载。借助这套插件,开发者可以快速为input元素挂载虚拟键盘,通过layout、usePreview等选项或自定义JSON按键布局来适配不同业务需求,并使用open、close、show、hide等API灵活控制键盘行为,提升表单交互质量与整体可用性。
1. 虚拟键盘插件:触屏设备表单输入的最后一公里,jQueryUI 也能做出超炫效果
第一次看到这个叫 VirtualKeyboard 的插件时,我以为又是一堆 Canvas 画的烂大街软键盘。直到我把它的源码拆开,才发现它完全跑在 jQueryUI 之上——连自定义事件和按钮状态管理都复用了 Widget Factory 的那套标准生命周期。这个插件解决的是很多开发者都遇到过的问题:在触屏设备、一体机或者没有实体键盘的嵌入式终端上,如何让用户在一个<input>或<textarea>里顺畅地完成输入,同时让界面看起来有质感、不像是临时拼凑的控件。
它最适合三类人:做后台管理系统但偶尔需要触屏适配的前端、在工控或自助终端上做表单录入的开发者、以及想把 jQueryUI 现有交互组件玩出花样的 JavaScript 学习者。整个插件不依赖任何构建工具,下载后直接引用<script>标签就能跑,核心代码用一个独立的键盘渲染引擎承载布局和交互,业务侧只需要关心「按了什么键、值回填到哪里」。懂 jQuery 基础就能改,甚至不必熟读 jQueryUI 源码。
2. 架构与依赖:jQueryUI Widget Factory 之上的键位状态机
2.1 为什么选 jQueryUI 而不是从头造轮子
VirtualKeyboard 里最让我觉得聪明的选择,是没有把键盘的每个按键当成独立 DOM 来管理,而是把整块键盘看成 jQueryUI 的一个标准 Widget。它调用$.widget("ui.virtualKeyboard", ...)注册组件,利用 Widget Factory 自带的_create、_init、_destroy生命周期管理初始化、销毁和重绘。这意味着你可以在一个页面里同时创建多个键盘实例,每个实例拥有独立的options和element,互不干扰。
用 jQueryUI 而不是从零写 Class 的好处很直接:options的默认值合并、事件触发的规范写法、以及_on统一绑定和解绑事件这些能力都是现成的,我不用手写一堆 addEventListener 再去记挂清理逻辑。插件内部的按键映射其实是一个状态机——从「默认布局」切换到「大写锁定布局」再切换到「符号布局」,本质上就是状态流转触发布局重绘。这个状态机就埋在_renderKeys和_setLayout两个私有方法里。
2.2 插件目录结构与核心文件职责
我拆源码的时候习惯先把文件铺开看归属。这个 VirtualKeyboard 插件解开压缩包之后是这样一个结构:
virtual-keyboard/ ├── css/ │ ├── keyboard.css # 基础布局:键帽大小、行列间距 │ └── keyboard-skin-dark.css # 深色皮肤,覆盖默认配色变量 ├── js/ │ ├── jquery.virtual-keyboard.js # 插件主体,约 800 行,核心逻辑全部在这 │ └── i18n/ │ └── keyboard-zh-CN.js # 中文按键文本与提示气泡文案 ├── demo/ │ ├── index.html # 基础用法示例 │ └── custom-layout.html # 自定义布局示例 └── README.mdjquery.virtual-keyboard.js是唯一值得逐行读的文件。它内部划分了五个逻辑块:默认配置、布局定义、渲染函数、事件绑定、输入回填。其中布局定义是一个二维数组结构,每一个子数组对应一行键盘,每个元素代表一个按键的标签、值和类型(普通键、功能键、占位键)。这种数据结构和渲染逻辑彻底分离的写法,让我在改键位顺序时完全不用碰渲染代码。
2.3 数据驱动渲染:布局即数据,数据即UI
插件里最核心的设计就是把键盘布局抽象成了可序列化的 JavaScript 数组。常见的做法是定义一个layouts对象,默认导出normal、shift和symbol三套布局,切换状态时渲染函数读取对应布局重新生成按键 DOM。这样做的好处是,如果你要做一个法语键盘或者一个小键盘区域,不需要复制整个插件文件,只需要往layouts里追加一个新的布局对象,然后通过options.layoutName指过去就行。
// 这是插件内部的布局定义片段,不是完整的 104 键 layouts: { normal: [ ['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='], [{ value: 'q', type: 'letter' }, 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], ['caps', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'", 'enter'], ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 'shift-right'], ['space'] ] }这段代码定义了最基础的五行键盘。每个字符串默认渲染成一个普通按键,带type字段的对象则进入_createKey里的逻辑判断:当type为'letter'时表明这是一个字母键,'caps'是大小写锁定键,'shift'是临时上档键,'space'则单独渲染成一个宽空格键。自定义布局时,照着这个数组格式追加内容就能用,这是整个插件二次开发成本最低的入口点。
3. 核心实现拆解:一个按键从按下到字符上屏的完整链路
3.1_createKey:按键 DOM 的生成与交互状态注入
每个按键都是一个<button>元素,插件在_createKey里做了三件事:设置>_createKey: function (keyConfig, layoutName) { var key = $('<button>', { 'class': 'vk-key', 'type': 'button', 'data-key': typeof keyConfig === 'string' ? keyConfig : keyConfig.value, 'data-type': typeof keyConfig === 'string' ? 'char' : keyConfig.type }); if (typeof keyConfig === 'object' && keyConfig.type === 'space') { key.addClass('vk-key-space'); } key.text(this._getKeyLabel(keyConfig, layoutName)); return key; }
_getKeyLabel会先检查options.keys里有没有用户覆盖的文本,如果没有,则回退到i18n[layoutName]中的翻译文案。这样键盘上显示的文字和实际输出的值可以不一样——比如中文环境中回车键显示「换行」,但>_on: function () { this._on(this.element, { 'click .vk-key': '_handleClick', 'mousedown .vk-key': '_startPressFeedback', 'mouseup .vk-key': '_endPressFeedback' }); }, _handleClick: function (event) { var key = $(event.target).closest('.vk-key'); var keyValue = key.data('key'); var keyType = key.data('type'); if (keyType === 'char' || keyType === 'letter') { this._insertCharacter(keyValue); } else { this._handleFunctionKey(keyValue); } }
分发逻辑保持在最小可执行状态:字符类按键直接插值,功能类按键走_handleFunctionKey继续判断。长按重复是压感键盘的常见体验,插件里用mousedown时启动一个setInterval,如果 500ms 内没有松开且是字符键,就每 80ms 再插入一次字符;mouseup或mouseleave时清掉定时器。这套逻辑单独看很简单,但和事件委托配合的时候,定时器引用的this必须提前用箭头函数或_bind固定,否则很容易在键盘失去焦点时留下一个永远清不掉的幽灵定时器。
3.3 输入回填:插入光标位置、选中替换与撤销栈
键盘按出来了,字符落进输入框才是关键。插件回填字符时没有简单地在<input>末尾拼字符串,而是用getSelectionStart和setSelectionRange维护光标位置。每次_insertCharacter执行以下四步:记录当前光标位置、删除可能存在的选中文本、在光标处插入新字符、更新光标位置到插入字符之后。
_insertCharacter: function (char) { var input = this.$input[0]; var start = input.selectionStart || 0; var end = input.selectionEnd || 0; var value = this.$input.val(); var newValue = value.substring(0, start) + char + value.substring(end); this.$input.val(newValue); var newPosition = start + char.length; this._setCaretPosition(newPosition); this._pushUndoStack(value); this._trigger('change', null, { value: newValue }); }_setCaretPosition内部处理了两种兼容情况:如果浏览器支持setSelectionRange就直接调用,否则用createTextRange回退。_pushUndoStack则是把每次插入前的完整输入框值压入一个数组,最多保留 20 条,配合插件暴露的undo()方法实现撤销功能。值得注意的是,这里在每次插入后手动触发了_trigger('change'),而不是依赖 input 元素的原生事件,目的是让键盘的输入能进入 jQueryUI 的事件体系,方便上游业务监听统一的回调。
3.4 皮肤机制:CSS 变量驱动换肤,不重启也能切风格
键盘的「超炫」观感主要来自 CSS 和交互反馈的配合,而不是复杂的图形绘制。插件在keyboard.css中定义了键帽的渐变、阴影、按下时的内阴影位移,以及键帽之间 4px 的间隙。深色皮肤通过覆盖 CSS 变量实现,keyboard-skin-dark.css里重定义了--vk-key-bg、--vk-key-border、--vk-key-text等十余个变量。
.vk-key { border: 1px solid var(--vk-key-border, #ccc); border-radius: 5px; background: var(--vk-key-bg, linear-gradient(#fff, #e4e4e4)); box-shadow: var(--vk-key-shadow, 0 2px 2px rgba(0,0,0,0.12)); font-size: var(--vk-key-font-size, 14px); min-width: var(--vk-key-min-width, 36px); height: var(--vk-key-height, 40px); }默认皮肤在无变量定义时回退到#fff和浅灰渐变,主题色全部由变量接管。插件运行时会扫描当前键盘容器上是否设置了><!DOCTYPE html> <html> <head> <link rel="stylesheet" href="css/keyboard.css"> <link rel="stylesheet" href="css/keyboard-skin-dark.css"> <script src="js/jquery.min.js"></script> <script src="js/jquery-ui.min.js"></script> <script src="js/jquery.virtual-keyboard.js"></script> </head> <body> <input type="text" id="demo" placeholder="点击唤起虚拟键盘"> <script> $('#demo').virtualKeyboard({ layoutName: 'normal', skin: 'dark', showKeyTips: true }); </script> </body> </html>
装好依赖打开页面,点击输入框,键盘就会以弹层形式出现在输入框下方。需要注意的初始化参数包括:layoutName指定首次渲染的键盘布局、skin指定皮肤名、showKeyTips控制在按键右上角显示当前字符的小气泡提示。如果键盘没有弹出来,先打开浏览器控制台看有没有报$.widget is not a function,有就说明 jQueryUI 没有被正确加载。
4.2 常用配置项一览与动态调整方法
插件对外暴露的配置项基本覆盖了真实场景里的需求。我通常在使用前先列一张表确认每个参数的行为,避免在事件回调里临时改配置发现不生效。
| 配置项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
layoutName | String | 'normal' | 初始渲染的布局名 |
skin | String | 'light' | 皮肤名,对应>$('#field-name').virtualKeyboard({ layoutName: 'normal', skin: 'dark' }); $('#field-code').virtualKeyboard({ layoutName: 'symbol', skin: 'light', showKeyTips: true });两个键盘实例在点击各自的输入框时分别弹出,互不干扰。但注意 4.4 自定义按键布局:改一个数组就能换一套键盘如果部署环境需要的是数字小键盘,而不是全键盘,最省力的做法不是删键而是新增布局。插件在初始化完成后,可以通过实例方法直接注册新布局,这样连插件的源文件都不需要改动。
4.5 事件回调:监听键盘输入与功能键动作插件提供的监听事件是开发时连接业务逻辑的主要路径。
改动之后,点击“查询”按键不会输入任何字符,而是触发一个自定义事件 这里唯一要保证的是 6.3 增强换肤:复用业务设计系统的颜色变量前面提到的皮肤机制是用 CSS 变量实现的,二次开发时可以让键盘样式跟随业务站点设计令牌,而不是复制一份新 CSS。做法是找到项目里定义设计变量的入口文件,把键盘的变量映射放进去。 映射完成之后,皮肤表里新增一行 拆完这个插件给我的最大感受是,好的组件设计不是功能堆得多炫,而是每一层边界收得干净——布局数据、渲染逻辑、事件回调三者互不越界。从那以后我每次接手表单类控件开发,都会先确认输入逻辑是否与 UI 表现解耦,如果发现改一个按键要动三个文件,就会强制自己停下来重新拆一层。希望这份拆解对你有帮助,照着跑一遍 demo,再按自己的业务场景改一个功能键试试,你会很快摸清它的脾气。 本文还有配套的精品资源,点击获取 |