解決Web 應用與瀏覽器快捷鍵衝突的一條野路子

blank

解決Web 應用與瀏覽器快捷鍵衝突的一條野路子

先介紹下大背景,我們是Ant Codespaces 團隊,主要為螞蟻的工程師們提供雲上研發能力。我們對外透出的是一個典型的B/S應用,用戶無需在本地下載任何軟件,在瀏覽器中就能完成標準技術棧場景下的研發工作,於此同時問題隨之而來。

Web應用相比於Native應用存在一個顯著的缺點:鍵盤事件會衝突,現像是部分快捷鍵組合在Web應用中會“失效”,舉幾個例子:如Cmd + WCmd + NCmd + T等等組合,這些事件不做額外處理是無法被Web 應用所正常響應的。

究其原因,可以歸於“ Web App還未來得及處理Keyborads Event,Browser已經做出了對應的響應並產生了副作用”,比如當Cmd + W被瀏覽器響應後,其副作用為關閉當前tab頁,頁面都被關閉了,Web App 自然也就被關了。

Ant Codespaces 作為研發上雲的重要組成部分,而快捷鍵又是工具產品提效的重要方式之一,因此在快捷鍵這件事上我們需要給用戶提供不落後於本地IDE 的按鍵組合功能與體驗。

既然我們遇到了這個問題,那麼友商是怎麼做的?

Github Codespaces

blank

Github Codespaces 巧妙避開了這部分衝突的快捷鍵,用戶可以自己到相關配置界面中去設置自己想要的快捷鍵(但此時用戶無法設置成與瀏覽器衝突的組合)

Theia

blank

Theia提供了Alt + W作為代替Cmd + W的默認組合

Coding

blank

Coding 同上,也是避開衝突+ 自主設置

試用了一圈外部產品後,我枯了。不出意外的發現友商們在解決此類問題時採用了一種通用的解決方案:避開與瀏覽器衝突的快捷鍵組合

這種方式雖然能解決問題,但代價是需要改變本地用戶的使用習慣,功能上是有了,但體驗上產生了差異。

隨後到Theia 老師處取了取經,也沒有發現太好的辦法

blank


快捷鍵背後的心智模型

既然業界已經有了通用的“方案”,為何還要繼續糾結這個問題?

從我切身體驗來看,我在今年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 等等等等。

blank

同樣我們也可以聯想一下行業外的其他角色是如何工作的。比如照相館老闆使用Photoshop,他們鍵盤上部分按鍵都快包漿了,可想而知他們有多依賴快捷鍵的能力,沒有快捷鍵固然還能繼續工作,但是效率上會降低很多,而時間就是金錢

因此可以草率的得出一條結論:遵循心智模型的快捷鍵組合能提升軟件使用的效率。

為什麼要用草率這個詞,因為這句話是我自己編的: )

所以我們不但要解決快捷鍵的“功能有無”問題,也要盡可能的解決快捷鍵“體驗一致”問題。


所有衝突的快捷鍵都會失靈嗎?

並不是

從現像上看,大體上可以分為兩類快捷鍵:

  1. 能被preventDefault
  2. 不能被preventDefault

可以被preventDefault的這部分組合很好解,以Cmd + S舉例,

在一個沒有額外處理鍵盤事件的Web應用裡, Cmd + S會觸發瀏覽器的网页保存,見:

blank

當通過以下代碼取消默認事件後,我們可以愉快的在listener 中加入callback

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

看看效果:

blank

這背後發生的故事就不在本文展開了,相信各位前端老法師們比我更懂Events

不能被preventDefault 的快捷鍵該如何是好

觀察上述行為後,可以發現當CMD + S未被取消默認事件時,會先執行cb,隨後再是執行默認事件(可以在此同款demo中感受一下):

blank

當preventDefault 後,自然就變成了:

blank

那麼,把事件CMD + S換為CMD + W會發生什麼?

將上述demo 改寫後試下:

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

直接翻車,說明CMD + W類的事件和CMD + S類的事件有著本質差別,我們可以在原有的流程圖上繼續做一個推測,當瀏覽器對這部分優先級更高的快捷鍵做出不可逆的副作用響應時,listener 的cb 即便preventDefault 也將變得無能為力,因為更高優先級的副作用已經產生了:

blank

那!怎!麼!辦!啊!

問題不大,辦法總歸是有的,無非是權衡一下投入和產出。

從流程圖上看這個問題的話,思路上大概是在emit keybords eventsbrowser do sth.之間加點騷東西,一是讓不可逆的副作用不再產生,二是讓listener的cb正常執行。只要能滿足這兩點,這事情基本上就成了,剩下的就看研發成本與實現方式。

blank

有了大致的思路後,繼續尋找實現思路的方式,大概分為三個方向:

  1. 寫一個native bridge,用來代理系統級的事件,再通過某種方式與瀏覽器通信(類似Postman Capture requests and cookies的實現方式)
  2. 增加Electron 版本的IDE,理論上VSC 的快捷鍵能力都能實現了
  3. 通過Chrome Extension 來代理快捷鍵
方式 複雜度 優點 缺點
Native Bridge ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ 1. 系統級能力,想做的能力基本都能做,沒有做不到只有想不到 1. 需要用戶額外安裝native bridge 與chrome extension
2. 需要考慮跨平台2. 複雜度較高
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,但至少從眼前的行為來看,這條路子是可行的。

blank

因此最開始的流程圖就變成了:

blank

那麼這麼做會有什麼新的問題麼?

有的

可以看到這個流程圖中缺少了一個環節, browser do sth.沒有了。

有人可能會有疑問:我們確實是想把browser 響應的事件去掉呀,為什麼會有問題呢?

將上圖再擴充一下就清晰了:

blank

從擴充後的流程圖中不難發現,實際上我們仍然需要那部分被override 的browser 行為,因為用戶的瀏覽器除了跑我們的Web 應用之外,還需要滿足其他的日常瀏覽需求,如果只是因為想在Cloud IDE里通過CMD + T打開文件tab,從而失去了Browser原生的快速新建頁籤的能力,那就又繞回了這個問題的起點。

預期是什麼?

預期就像圖中所描述的,當前tab 為我們想要override 的tab 時,broswer 行為不生效,當前tab 為其他頁面時,繼續保留browser 的行為。

可以完美做到麼?

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

先祭出一張Chrome Extension 的架構圖:

blank
via kmsfan

由圖可見,Chrome Extension 有幾個核心的概念:

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

概念不一一展開介紹了,撿兩個我們用到的概念說一說。

background js為開發者提供了豐富的Chrome API調用能力,常見的如开关页面跳转tab等基本能力都能通過Chrome API模擬出來,因此可以patch瀏覽器的原生事件。

流程圖再次變化一下:

blank

那還有什麼問題麼?

還有問題

以Cloud IDE關閉文件的組合鍵舉例CMD + OPT + W ,我們雖然通過Chrome Extension攔截了CMD + W ,但是web應用的listener是註冊在CMD + OPT +W上的,也就是說我們光攔事件沒有用,還需要將對應在web 應用中的事件補上,而content js 是tab 級別並且能獲取到對應頁面的上下文的,因此可以通過content js 來patch 應用註冊的事件。

流程圖繼續演進:

blank

為什麼採用這種方式?

一方面我們的Web 應用是一個架構很複雜的應用,不希望因這個方案帶來侵入性的改造,避免衍生出更多的邏輯分支與環境概念。另一方面,它是一種通用的解決思路,不單單是用來解決Cloud IDE 遇到的問題,其他的Web 應用都可以無痕接入類似的方案。

最後看看代碼實現,以CMD + N新建文件舉例:

對於backgound js而言,我們需要模擬新开window行為,這個能力Chrome API已經提供了,即:

chrome.windows.create()
blank

對應的content js,我們需要將web 應用註冊的事件補發,即:

blank

將backgroud 與content 中的patch 組合起來,註冊到擴展對應的快捷鍵中:

blank

看看最終效果

使用方案前

blank
在Web 應用中使用CMD + N 打開了新的瀏覽器窗口,不符合用戶的預期

使用方案後

blank
在Web 應用中使用CMD + N 打開了新的file tab,符合預期

啊!終於可以“關閉”文件了~


最後!

我們是螞蟻研發效能部,致力於為螞蟻和多家金融企業提供核電級的全生命週期研發產品,研發效能部的產品涵蓋了螞蟻的整個研發活動,包含代碼服務(託管、審核、掃描、搜索、構建、內容挖掘)、代碼編輯(Cloud IDE)、CI/CD、測試集成、環境搭建、全鏈路鏈條、配置管理、資源調度,以及基於研發活動完整生命週期的數據產品。加入我們,一起打造螞蟻集團下一代基於雲原生的研發效能平台。

(PS 我們正在嘗試小流量WFH,每周有一天的時間可以不限地點辦公,或許是國內為數不多的在後疫情時期依舊對遠程工作保持高效探索的神秘部門,相信很多工程師們會喜歡這種體驗)


最後的最後!

我是小旋風,一個從未接過單的順風車司機,如果您遠道而來,我可以到機場接您到黃龍,這一切都是免費的,甚至還能倒擼一發冰美。

蕭山-> 黃龍接機熱線熱郵:[email protected]

接機暗號:你們那兒還招人不。

暗號是什麼不重要,交通方式也不重要,title 也不重要,base 在哪也不重要,重要的是在探索的過程中發現有趣的事遇到志同道合的人。


最後的最後的最後!

附上JD:

  1. 資深全棧工程師
  2. 代碼平台技術專家
  3. 平台型產品專家
  4. 雲原生容器專家
  5. 高級開發工程師
  6. 分佈式計算架構專家
  7. 資深IDE研發工程師
  8. 代碼智能化工程師
  9. 編譯器研發工程師

What do you think?

Written by marketer

blank

用React Query 來管理數據請求

blank

🚢 G2Plot 2.0 全新來襲…