VSCode For Web 深入淺出 -- 外掛載入機制

Duang發表於2023-05-09

最近我在瀏覽 VSCode for web 的 repo,在最近更新的一些 commit 中發現了一個新的 VSCode 外掛特性支援,名為 webOpener,它的作用是什麼呢?又是如何影響外掛載入的呢?在這一篇中我們結合 VSCode For Web 的外掛載入機制來詳細分析一下。

VSCode for web 的外掛載入機制

我們知道,由於 VSCode for web 執行在瀏覽器上,因此,它的外掛載入機制與 VSCode for desktop 有所不同。

在 VSCode for desktop 中,外掛是以 vsix 包的形式存在的,因此,VSCode for desktop 可以直接透過 vsix 包的形式載入外掛。而在 VSCode for web 中,由於瀏覽器的安全機制,不能直接載入 vsix 包。

因此,VSCode for web 採用了一種特殊的外掛載入機制。釋出 VSCode for web 外掛時,釋出系統會直接將專案編譯,併發布到 CDN 節點上。當使用者載入外掛時,透過向該目標 url 傳送請求,拉取遠端(也可以是本地)的 extension.js 檔案。並利用 web worker 載入機制,為每個外掛分配獨立執行緒載入與執行。

在生產環境中,對每個進入 VSCode 外掛商店的外掛,VSCode for web 會將支援 web 環境的外掛的 package.jsonextension.js 等檔案打包成一個 zip 包,然後根據 publisher 分配合適的二級域名,透過 CDN 分發。

以我在使用的One Dark Pro主題為例:

20230508145433

而在除錯模式中,我們可以透過 Install extension from location...命令,指定編譯後外掛的 url,從而載入外掛。

20230508143115

我們使用本地伺服器,指定一個已編譯好的 VSCode web extension,並填入本地伺服器地址 (https://localhost:5000),並重新整理頁面,那麼從 Chrome 的 Network 中可以看到 VSCode 向目標位置請求了package.jsonextension.js,並看到外掛已經被成功載入了。

20230508144906

透過這樣的方式,VSCode for web 在每次頁面開啟後,完成了對使用者自定義的外掛管理與載入。並由於web worker的特性,每個外掛的執行環境都是獨立且相互隔離的。

透過特殊 url 路由的方式的外掛載入機制

VSCode for web 最突出的特點是它是執行在瀏覽器上的,因此,我們可以利用 url,來實現一些奇妙的新特性。例如,透過特殊的 url 路由,免安裝地載入外掛。

目前,vscode.dev 可以使用這樣的方式載入外掛:

https://vscode.dev/+publisher.name

例如,在瀏覽器中輸入 https://vscode.dev/+ms-vscode.onedrive-browser 將載入 OneDrive 瀏覽器擴充套件。

當然,我們也可以使用同樣的方式載入本地編譯的外掛。由於 vscode.dev 強制要求 secure context ,因此,我們需要在本地啟動一個 https 的伺服器,並對 url 進行 base64 編碼,才能正常訪問。

訪問https://vscode.dev/+aHR0cHM6Ly9sb2NhbGhvc3Q6MzAwMA==即可。(後面那一段為"https://localhost:3000") 的 base64 編碼)

webOpener 特性介紹

有開發過 VSCode for desktop 的外掛的同學應該知道,vscode 外掛的所有能力都是在 package.json 中宣告的,這也是為什麼 VSCode 除了需要載入入口的 extension.js 外,還一定要載入外掛的 package.json 的原因。

在外掛 package.jsoncontributes 欄位中,我們可以宣告外掛的各種能力,例如,命令、選單、快捷鍵、主題、語言、偵錯程式等等。

對於 vscode for web 版本的外掛來說,我們還可以宣告 webOpener 能力,其所有屬性都是可選的。宣告如下:

{
  "name": "onedrive-browser",
  "contributes": {
    "webOpener": {
      "scheme": "onedrive",
      "import": "webOpener.js",
      "runCommands": [{ "command": "hello-world", "args": ["$url"] }]
    }
    ...
  }
}

webOpener.scheme

預設情況下,vscode.dev/+publisher.name 路由將直接開啟預設的 VSCode 示例工作區。但是,如果提供了 scheme path,則 VSCode 將根據路由引數開啟一個以該協議開啟 url 中後續 path 指向的資料夾,格式如下:

# 當 scheme 設定為 onedrive
https://vscode.dev/+publisher.name/remoteAuthority/path/segments/...

例如,當外掛 webOpener 的 scheme 設定為 onedrive 時,訪問 https://vscode.dev/+ms-vscode.onedrive-browser/myPersonalDrive/cool/folder ,此時訪問 url 將重定向為 onedrive:///myPersonalDrive/cool/folder

若此協議不在 VSCode 的內建協議中,我們可以在外掛中透過 vscode.workspace.registerFileSystemProvider 這個 API 註冊自定義的 FileSystemProvider,從而實現對自定義協議的 FileSystem 支援。

本質上,它開啟的方式與 VSCode for web 的 vscode.open 命令也是一致的。

webOpener.runCommands

當 VSCode 的主 workbench 載入完畢後,會觸發 webOpeneronDidCreateWorkbench 的鉤子,並執行此處宣告的命令集。

這將傳入一個命令陣列,例如:[{ "command": "test-extension.hello-world", "args": ["$url"] }],此時將可以執行自定義外掛 test-extension 的相關命令。

其中,$url 指代當前頁面 url。如果外掛的初始化依賴來自 url 的 query/path 等等資訊,這將很有用。

webOpener.import

這裡定義了 webOpener 載入的入口點。它是一個相對於外掛 package.json 的 ES Module 路徑,例如:webOpener.js

它與 extension.js 一樣,預設匯出一個 doRoute 函式,該函式將獲取 route 與 workbench 等資訊(workbench 這個例項中提供了當前 vscode for web 的命令、日誌、環境、window、workspace 等多種能力支援)。由於 webOpener.js 執行在主執行緒中,因此它能做到的事情要比處於 web worker 下的 vscode for web 外掛更多。

舉一個例子,這是一個簡單的 webOpener 貢獻 onedrive-browser:

export default function doRoute(route) {
  // If we're not already opening a OneDrive, show the picker immediately
  // when the user hits `vscode.dev/+ms-vscode.onedrive-browser`.
  if (route.workspace.folderUri?.scheme !== 'onedrive') {
    route.onDidCreateWorkbench.runCommands.push({
      command: 'onedrive-browser.openOneDrive',
      args: [],
    });
  }
}

它將在 workbench 載入完畢後,判斷當前的 workspace 是否為 onedrive,如果不是,則執行 onedrive-browser.openOneDrive 命令,從而開啟 onedrive 資料夾。

webOpener 與外掛的通訊機制

在瞭解了 webOpener 的基本特性之後,我們來看看該如何利用這些特性,與我們的 web 外掛進行通訊,從而擴充套件外掛能力。

我們可以看出,由於 webOpener 載入在主執行緒,且 doRoute 方法的執行時機在主執行緒 workbench 載入完畢之後,在請求遠端外掛並執行之前。因此,我們可以有兩種方式來傳遞資訊,與處於 web worker 下,與宿主隔離的外掛進行通訊。

第一種即為在 runCommands 中介紹的,透過執行 command 並傳遞 url 的方式傳遞初始化資訊。該方式也是 webOpener 與外掛通訊的常用方式之一,用於為初始化外掛時提供部分依賴引數。

第二種則是透過 doRoute 方法,捕獲此時的請求資訊,並根據請求資訊的不同對外掛能力進行不同的變更,但本質上還是透過 command 的方式給外掛傳送 args 來實現的。

我在當前最新版本的 vscode-dev 程式碼庫中(1.79.0),並未發現直接透過 webOpener 暴露類似 postMessage 的與外掛通訊的方法,因此到目前為止,我們只能透過給外掛的 command 方式觸發 trigger 與傳入引數這一種方式來實現與外掛的通訊。這導致了在 web 下外掛的能力其實相當受限。

總結

本篇文章解析了在 VSCode for web 中的外掛載入機制,以及如何透過 webOpener 特性來擴充套件外掛的能力。

我們可以看出,在現階段的 VSCode for web 中,外掛的載入機制也僅僅只是做到了可用狀態。由於 web worker 天然的與主執行緒隔離的特性,desktop 的很多好用的功能性外掛(即除了 theme/key-binding 這種不需要執行邏輯的外掛之外)在 web 端的支援還是會遇到很多問題,並不能無縫遷移。這點也是我在嘗試開發 VSCode for web 外掛時最大的痛點。

不過,隨著 VSCode for web 專案仍在進行高頻的開發與完善,希望未來的 VSCode for web 能在外掛開發與使用上儘可能對齊甚至相容 desktop 的體驗。

參考資料

相關文章