尝试用 JS 给浏览器写个 Vim 按键模拟,好像没大毛病

陪她去流浪 桃子 2024年03月31日 阅读次数:147

我在浏览器上面也安装了 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,不知道如何操作。

开源地址:https://github.com/movsb/web-extension-vim

这篇文章的内容已被作者标记为“过时”/“需要更新”/“不具参考意义”。