阅读 872

解决 Web 应用与浏览器快捷键冲突的一条野路子

先介绍下大背景,我们是 Ant Codespaces 团队,主要为蚂蚁的工程师们提供云上研发能力。我们对外透出的是一个典型的 B/S 应用,用户无需在本地下载任何软件,在浏览器中就能完成标准技术栈场景下的研发工作。

于此同时问题随之而来。

Web 应用 相比于 Native 应用 存在一个显著的缺点:键盘事件会冲突,现象是部分快捷键组合在 Web 应用中会 “失效”,举几个例子:如 Cmd + W Cmd + N Cmd + T 等等组合,这些事件不做额外处理是无法被 Web 应用所正常响应的。

究其原因,可以归于 “Web App 还未来得及处理 Keyborads Event,Browser 已经做出了对应的响应并产生了副作用”,比如当 Cmd + W 被浏览器响应后,其副作用为关闭当前 tab 页,页面都被关闭了,Web App 自然也就被关了。

Ant Codespaces 作为研发上云的重要组成部分,而快捷键又是工具产品提效的重要方式之一,因此在快捷键这件事上我们需要给用户提供不落后于本地 IDE 的按键组合功能与体验。

既然我们遇到了这个问题,那么友商是怎么做的?

Github Codespaces

image.png

Github Codespaces 巧妙避开了这部分冲突的快捷键,用户可以自己到相关配置界面中去设置自己想要的快捷键(但此时用户无法设置成与浏览器冲突的组合)

Theia

image.png

Theia 提供了 Alt + W 作为代替 Cmd + W 的默认组合

Coding

image.png

Coding 同上,也是避开冲突 + 自主设置

试用了一圈外部产品后,我枯了。

不出意外的发现友商们在解决此类问题时采用了一种通用的解决方案:避开与浏览器冲突的快捷键组合

这种方式虽然能解决问题,但代价是 需要改变本地用户的使用习惯。

功能上是有了,但体验上产生了差异。

随后到 Theia 老师处取了取经,也没有发现太好的办法:

image.png

快捷键背后的心智模型

既然业界已经有了通用的 “方案”,为何还要继续纠结这个问题?

从我切身体验来看,我在今年 6 月份来到黄龙,当我将研发活动从本地 VSCode 迁移到 Cloud IDE 时,最难受的是:部分高频快捷键操作与我脑海里的第一反应不一致,典型的如关闭文件 Cmd + W ,即便已经过了 100 多天,我依旧不想用非 Cmd + W 组合来关闭文件。

同时,纠结这一点的用户不止我一个,我们也收到了一些来自用户的反馈,期望能解决高频快捷键的一致性问题。

这是一个很合理的诉求,当我们在 macOS 上下载一个 Native App 时,自然而然的会认为 Cmd + W 是「关闭 XX」, Cmd + N 是「新建 XX」,这是一种约定俗成的 guide line。当然软件开发者们也可以不遵守这个,甚至是反其道行之,那相对应的就会增加用户使用成本,从而戴上真难用的帽子。

快捷键对工具类软件的重要性

在特定的场景下,如果操作鼠标或是操作键盘都能达成某一目的,那么大概率操作键盘会比纯操作鼠标速度来的更快。(当然还有一些场景可能是纯鼠标更快,比如 macOS 用触发角锁屏)

作为工程师,我们可以联想一些日常研发活动时的一些场景,比如跳行、搜索、关闭/新开文件.... 无论是用 Sublime 还是 VSCode ,这些功能都可以用键盘或是鼠标做到,但很多同学会选择使用键盘来唤醒对应的功能。

甚至很多效率工具的核心能力之一是将鼠标操作转用键盘操作来代替,比如 Alfred、Spectacle 等等等等。

image.png

同样我们也可以联想一下行业外的其他角色是如何工作的。比如照相馆老板使用 Photoshop,他们键盘上部分按键都快包浆了,可想而知他们有多依赖快捷键的能力,没有快捷键固然还能继续工作,但是效率上会降低很多,而时间就是金钱

因此可以草率的得出一条结论:遵循心智模型的快捷键组合能提升软件使用的效率。

为什么要用草率这个词,因为这句话是我自己编的 : )

所以我们不但要解决快捷键的 “功能有无” 问题,也要尽可能的解决快捷键 “体验一致” 问题。

所有冲突的快捷键都会失灵吗?

并不是

从现象上看,大体上可以分为两类快捷键:

  1. 能被 preventDefault
  2. 不能被 preventDefault

可以被 preventDefault 的这部分组合很好解,以 Cmd + S 举例

在一个没有额外处理键盘事件的 Web 应用里, Cmd + S 会触发浏览器的 网页保存,见:

save1.gif

当通过以下代码取消默认事件后,我们可以愉快的在 listener 中加入 callback

    document.addEventListener("keydown", (e) => {
      if (e.keyCode === 83 && e.metaKey) {
        e.preventDefault();
        alert("I AM CMD_S");
      }
    });
复制代码

看看效果:

cmd_s.gif

这背后发生的故事就不在本文展开了,相信各位前端老法师们比我更懂 Events

不能被 preventDefault 的快捷键该如何是好

观察上述行为后,可以发现当 CMD + S 未被取消默认事件时,会先执行 cb,随后再是执行默认事件(可以在此同款 demo 中感受一下):

当 preventDefault 后,自然就变成了:

那么,把事件 CMD + S 换为 CMD + W 会发生什么?

将上述 demo 改写后试下:

   document.addEventListener("keydown", (e) => {
      if (e.keyCode === 69 && e.metaKey) {
        e.preventDefault();
        alert("I AM CMD_W");
      }
    });
复制代码

cmd_w5.gif

直接翻车,说明 CMD + W 类的事件和 CMD + S 类的事件有着本质差别,我们可以在原有的流程图上继续做一个推测,当浏览器对这部分优先级更高的快捷键做出不可逆的副作用响应时,listener 的 cb 即便 preventDefault 也将变得无能为力,因为更高优先级的副作用已经产生了:

那!怎!么!办!啊!

问题不大,办法总归是有的,无非是权衡一下投入和产出。

从流程图上看这个问题的话,思路上大概是在 emit keybords eventsbrowser do sth. 之间加点骚东西,一是让不可逆的副作用不再产生,二是让 listener 的 cb 正常执行。 只要能满足这两点,这事情基本上就成了,剩下的就看研发成本与实现方式:

有了大致的思路后,继续寻找实现思路的方式,大概分为三个方向:

  1. 写一个 native bridge,用来代理系统级的事件,再通过某种方式与浏览器通信(类似 Postman Capture requests and cookies 的实现方式)
  2. 增加 Electron 版本的 IDE,理论上 VSC 的快捷键能力都能实现了
  3. 通过 Chrome Extension 来代理快捷键

方式

复杂度

优点

缺点

Native Bridge

⭐️ ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ ⭐️

  1. 系统级能力,想做的能力基本都能做,没有做不到只有想不到
  1. 需要用户额外安装 native bridge 与 chrome extension
  2. 需要考虑跨平台
  3. 复杂度较高

Electron App

⭐️ ⭐️ ⭐️

  1. 跨平台
  2. 在现有 Web 应用的基础上追加 Electron ,成本比楼上更小
  1. 需要用户额外在本地安装使用 Electron App,与现阶段 Cloud IDE 的定位与场景不符
  2. 开了 electron 的口子,后续的系统级 API 如果 runtime 未提供,则依旧无法避免跨平台研发成本

Chrome Extension

⭐️

  1. 跨平台
  2. 开发量小
  1. 用户需要安装对应的 Chrome Extension
  2. 能捕获大部分事件,但仍有一部分事件会逃逸
  3. 对浏览器有约束

我认为工程师的使命是在有限的复杂度中寻找最优解,当梳理完三种思路后,基本就将 Chrome Extension 作为当下的首选实现方案。

一方面 Chrome Extension 能解决用户的几个强诉求组合(如关闭文件 CMD + W ,新建文件 CMD + N ,新开 Tab CMD + T ),解决这几个按键就基本搞定了绝大部分用户。 那么还剩下一些 Chrome Extension 也无法拦到的组合,比如 CMD + Q (请不要在此时按这个组合,会错过文末的彩蛋),熟悉 macOS 快捷键的同学应该都知道,这是强退应用的快捷键,理论上它的心智就是强退应用,拦不拦都无所谓了,逃逸就逃逸吧。

另一方面我们的 Cloud IDE 不同于其他中后台产品,我们只需要兼容到 Chrome@latest,为此方案提供了最佳的宿主环境。

基于 Chrome Extension 的快捷键冲突解决方案

综上所述,基本上这个方案的主旋律已经定调了:

  1. 在方案足够轻,复杂度足够低的前提下,ROI 才足够高
  2. 无需考虑跨平台
  3. 允许逃逸一部分不那么常用的组合

具体思路

用一句话概括这个方案的实现思路:在目标应用的 tab 里触发键盘事件时,屏蔽浏览器的原生行为,走 web 应用预期的行为。

如何屏蔽?

从这句话中,我们可以看到,“屏蔽浏览器的原生行为” 是此方案的大前提,那么问题来了,Chrome Extension 可以做到么?

可以

扩展用的很溜的同学应该接触过这个入口,很多扩展是提供快捷键自定义能力的,用于通过某个组合来执行对应的 command,经过实测发现,像 CMD + N CMD + W 等组合可以被扩展快捷键 override,虽然 Chrome 的官方文档并没有提到通过扩展快捷键 override browser 的快捷键是一个 feature,但至少从眼前的行为来看,这条路子是可行的。

image.png

因此最开始的流程图就变成了:

那么这么做会有什么新的问题么?

有的

可以看到这个流程图中缺少了一个环节, browser do sth. 没有了。

有人可能会有疑问:我们确实是想把 browser 响应的事件去掉呀,为什么会有问题呢?

将上图再扩充一下就清晰了:

从扩充后的流程图中不难发现,实际上我们仍然需要那部分被 override 的 browser 行为,因为用户的浏览器除了跑我们的 Web 应用之外,还需要满足其他的日常浏览需求,如果只是因为想在 Cloud IDE 里通过 CMD + T打开文件 tab,从而失去了 Browser 原生的快速新建页签的能力,那就又绕回了这个问题的起点。

预期是什么?

预期就像图中所描述的,当前 tab 为我们想要 override 的 tab 时,broswer 行为不生效,当前 tab 为其他页面时,继续保留 browser 的行为。

可以完美做到么?

做不到,但是可以假装做得到

先祭出一张 Chrome Extension 的架构图(via kmsfan

image.png

由图可见,Chrome Extension 有几个核心的概念:

  1. Background
  2. Popup
  3. Content Scripts
  4. Injected Scripts

概念不一一展开介绍了,捡两个我们用到的概念说一说。

background js 为开发者提供了丰富的 Chrome API 调用能力,常见的如 开关页面 跳转 tab 等基本能力都能通过 Chrome API 模拟出来,因此可以 patch 浏览器的原生事件。

流程图再次变化一下:

那还有什么问题么?

还有问题

以 Cloud IDE 关闭文件的组合键举例 CMD + OPT + W ,我们虽然通过 Chrome Extension 拦截了 CMD + W ,但是 web 应用的 listener 是注册在 CMD + OPT +W 上的,也就是说我们光拦事件没有用,还需要将对应在 web 应用中的事件补上,而 content js 是 tab 级别并且能获取到对应页面的上下文的,因此可以通过 content js 来 patch 应用注册的事件。

流程图继续演进:

为什么采用这种方式?

一方面我们的 Web 应用是一个架构很复杂的应用,不希望因这个方案带来侵入性的改造,避免衍生出更多的逻辑分支与环境概念。另一方面,它是一种通用的解决思路,不单单是用来解决 Cloud IDE 遇到的问题,其他的 Web 应用都可以无痕接入类似的方案。

最后看看代码实现,以 CMD + N 新建文件举例:

对于 backgound js 而言,我们需要模拟 新开 window 行为,这个能力 Chrome API 已经提供了,即:

chrome.windows.create()
复制代码

image.png

对应的 content js,我们需要将 web 应用注册的事件补发,即:

image.png

将 backgroud 与 content 中的 patch 组合起来,注册到扩展对应的快捷键中:

image.png

看看最终效果

使用方案前

在 Web 应用中使用 CMD + N 打开了新的浏览器窗口,不符合用户的预期

cmd_n4.gif

使用方案后

在 Web 应用中使用 CMD + N 打开了新的 file tab,符合预期

cmd_n2.gif

写到这里,基本上这条野路子就已经描述完了,这依旧不是最完美的解决方案,但至少为解决类似问题提供了新的方向与思路。

啊!终于可以 “关闭” 文件了~

最后!

我们是蚂蚁研发效能部,致力于为蚂蚁和多家金融企业提供核电级的全生命周期研发产品,研发效能部的产品涵盖了蚂蚁的整个研发活动,包含代码服务(托管、审核、扫描、搜索、构建、内容挖掘)、代码编辑(Cloud IDE)、CI/CD、测试继承、环境搭建、全链路链条、配置管理、资源调度,以及基于研发活动完整生命周期的数据产品。 加入我们,一起打造蚂蚁集团下一代基于云原生的研发效能平台。

(P.S. 我们正在尝试小流量WFH,每周有一天的时间可以不限地点办公,或许是国内为数不多的在后疫情时期依旧对远程工作保持高效探索的神秘部门,相信很多工程师们会喜欢这种体验)

最后的最后!

我是小旋风,一个从未接过单的顺风车司机,如果您远道而来,我可以到机场接您到黄龙,这一切都是免费的,甚至还能倒撸一发冰美。

萧山 -> 黄龙 接机热邮:chaxuan.wh@antgroup.com

接机暗号:你们那儿还招人不

暗号是什么不重要,交通方式也不重要,title 也不重要,base 在哪也不重要,重要的是在探索的过程中发现有趣的事遇到志同道合的人。

最后的最后的最后!

附上 JD:

  1. 资深全栈工程师
  2. 代码平台技术专家
  3. 平台型产品专家
  4. 云原生容器专家
  5. 高级开发工程师
  6. 分布式计算架构专家
  7. 资深 IDE 研发工程师
  8. 代码智能化工程师
  9. 编译器研发工程师
文章分类
前端
文章标签