我在浏览器上面也安装了 Vim 按键模拟扩展,Vimium - The Hacker's Browser,已经使用多年。
怎么说呢,确实是个“大而全”的 Vim 按键模拟。
但是,这么多年下来,我发现我几乎只需要使用其中的 gg
, G
, x
, r
, t
等非常少数的几个。
而 Bugs 呢,却发现了不少。最明显的例子,有几类页面可能不能正常工作:不是用标准 <input>
/<textarea>
作为输入框的、用 <div contenteditible>
来输入的)不能正确使用。所以当访问类似 jslinux 的页面时,要么:
- 禁用掉整个扩展(麻烦!);
- 按
i
进入Insert
模式(all commands will be ignored until you hit Esc to exit); - 把网页添加到 Vimium 的白名单(还要写正则/通配符,没有测试!);
总之就是,奇奇怪怪,影响编辑/使用体验。再加上此扩展本身体积很大(几百KB)、还申请了很多的隐私🔏类权限等等。 所以,我就想,写一个类似的东西到底有多难?
如何模拟?
我没参考任何已有代码,只是按照我的想法尝试做了以下的步骤:
- 在页面的根元素上监听按键(KeyPress)事件;
- 同时会接受到来自其它子元素的事件,因为事件会冒泡,所以要判断事件是来自
body
本身的; - 判断事件来自
body
来自也就不会来自<input> / <textarea> / <div contenteditible>
等元素。 - 由于大概率是冒泡来的事件,如果页面本身没处理,我
body
处理一下应该没啥问题吧?
- 同时会接受到来自其它子元素的事件,因为事件会冒泡,所以要判断事件是来自
- 把事件中的按键字符加入到一个按键栈里面;
- 检查按键栈是否属于某个已绑定的操作
- 如果是,执行对应的操作;
- 如果不是,则清空栈。
写点儿代码?
好像上面描述的步骤就足够了,然后就尝试写了点儿,两个函数,100+ 行代码。
用了几天,好像没啥毛病🤔。但是 Vimium 可是有 ~5000 的 Git 提交量。我这 100+ 行代码能不能换像它一样的 22K Stars? 代码不多,我全部贴出来吧(我 JS 并不熟悉,写得只是刚好能工作的程度,香草味的):
// 简易的 Vim 按键模拟。
class __Vim {
constructor() {
this.maps = {}; // 已知的按键绑定映射
this.tree = {}; // TRIE 树,用于搜索
this.stack = []; // 按键栈
this.timer = null; // 定时清理掉无效的按键
this.init();
}
init() {
document.body.addEventListener('keypress', (function (e) {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
if (e.target.tagName != 'BODY') {
this.stack = [];
return;
}
this.stack.push(e.key);
this.trigger();
if (this.stack.length) {
this.timer = setInterval(() => {
if (this.stack.length > 0) {
this.stack = [];
console.log('key stack cleared');
}
}, 1000);
}
}).bind(this));
this.maps = {
gg: function() {
window.scrollTo({left: 0, top: 0, behavior: 'smooth'});
},
G: function() {
window.scrollTo({left: 0, top: document.body.scrollHeight, behavior: 'smooth'});
},
j: function() {
window.scrollBy({left: 0, top: +150, behavior: 'smooth'});
},
k: function() {
window.scrollBy({left: 0, top: -150, behavior: 'smooth'});
},
f: function() {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.documentElement.requestFullscreen();
}
},
r: function() {
location.reload();
},
b: function() {
location.pathname = '/';
},
'?': function() {
console.log('Vim Help');
console.table({
gg: '回到页首',
G: '回到页尾',
j: '向下滚动',
k: '向上滚动',
f: '进入全屏',
r: '刷新',
b: '回到首页',
});
},
};
for (let key in this.maps) {
let node = this.tree;
for (let i in key) {
if (!node[key[i]]) {
node[key[i]] = {};
}
node = node[key[i]];
}
node.__handler = this.maps[key];
}
}
trigger() {
let node = this.tree;
console.log('stack:', this.stack);
// 遍历树以寻找匹配按键序列的按键映射/绑定。
for (let i = 0; i < this.stack.length; i++) {
let child = node[this.stack[i]];
if (!child) {
node = null;
break;
}
node = child;
}
// 说明根本没有这个按键映射,
// 属于无效的按键映射,清空。
if (!node) {
console.log('no such key binding:', this.stack);
this.stack = [];
return;
}
// 按键组合还没有到达最后一个按键。
if (!node.__handler) {
return;
}
// console.log(node);
console.log('triggering:', node);
node.__handler.call(this);
this.stack = [];
}
}
let vim = new __Vim();
最快速的体验方式:复制以上代码,打开浏览器的控制台,粘贴并执行!然后就可以在任何页面上体验了。 当然,我的博客 fork 了一份相同的代码,不管你愿不愿意,已经是被迫尝试了🐶。
预定义的按键可以在页面上按 ?
输出到控制台里面:
(索引) | 值 |
---|---|
gg | 回到页首 |
G | 回到页尾 |
j | 向下滚动 |
k | 向上滚动 |
f | 进入全屏 |
r | 刷新 |
b | 回到首页 |
作为扩展/Web Extension
根据 Mozilla 的 Web Extension 教程,加个 manifest.json
就可以作为 Web Extension 来使用了,简直简单到另人发指,连打包都不用:
{
"manifest_version": 2,
"name": "Vim",
"version": "0.0.0",
"description": "Minimal Vim emulation.",
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["vim.js"]
}
]
}
用火狐打开的 about:debugging
页面,点击“加载临时组件”,选中 manifest.json
所在目录即可加载。
我没用 Chrome 和 Safari,不知道如何操作。
正式版
尝试上架到火狐官方扩展商店,竟然很容易地成功了。可以直接点击下面的链接安装:
虽然权限一栏提示说会“Access your data for all websites”,但是实际上并没有访问任何数据。 只是我不知道怎么消除这个提示。
亲,给个好评!