支援遠端開發與 GitHub Codespaces

Visual Studio Code 遠端開發 讓您可以透明地與位於其他機器(無論是虛擬機還是實體機)上的原始程式碼和執行階段環境進行互動。GitHub Codespaces 是一項服務,它透過託管的雲端環境擴充了這些功能,這些環境可從 VS Code 及基於瀏覽器的編輯器進行存取。

為了確保效能,遠端開發與 GitHub Codespaces 都會透明地在遠端執行特定的 VS Code 擴充功能。然而,這可能會對擴充功能的運作方式產生細微影響。雖然許多擴充功能無需任何修改即可運作,但您可能需要進行一些調整,以確保您的擴充功能能在所有環境中正常工作,不過這些變更通常相當輕微。

本文總結了擴充功能開發者需要了解的關於遠端開發與 Codespaces 的資訊,包括擴充功能的 架構、如何在遠端工作區或 Codespaces 中 偵錯您的擴充功能,以及針對 擴充功能無法正常運作時該怎麼辦 的建議。

架構與擴充功能類型

為了讓使用者在使用遠端開發或 Codespaces 時能盡可能感到透明,VS Code 區分了兩種擴充功能:

  • UI 擴充功能 (UI Extensions):這些擴充功能貢獻於 VS Code 使用者介面,且始終在使用者的本機機器上執行。UI 擴充功能無法直接存取遠端工作區中的檔案,也無法執行安裝在該工作區或機器上的腳本/工具。UI 擴充功能的範例包括:佈景主題、程式碼片段、語言語法和按鍵對應。

  • 工作區擴充功能 (Workspace Extensions):這些擴充功能在與工作區所在位置相同的機器上執行。在本地工作區時,工作區擴充功能會在本地機器上執行;在遠端工作區或使用 Codespaces 時,工作區擴充功能則會在遠端機器/環境中執行。工作區擴充功能可以存取工作區中的檔案,以提供豐富的跨檔案語言服務、偵錯支援,或在工作區中對多個檔案執行複雜操作(直接操作或透過呼叫腳本/工具)。雖然工作區擴充功能不側重於修改 UI,但它們也能貢獻檔案瀏覽器、視圖和其他 UI 元素。

當使用者安裝擴充功能時,VS Code 會根據其類型自動將其安裝到正確的位置。如果一個擴充功能可以作為兩種型態執行,VS Code 將嘗試為該情況選擇最佳方案;UI 擴充功能將在 VS Code 的 本機擴充功能主機 (Local Extension Host) 中執行,而工作區擴充功能將在位於小型 VS Code Server 中的 遠端擴充功能主機 (Remote Extension Host) 中執行(若該工作區為遠端),否則若在本地,它將在 VS Code 的本機擴充功能主機中執行。為確保能使用最新的 VS Code 客戶端功能,伺服器必須與 VS Code 客戶端版本完全相符。因此,當您在容器、遠端 SSH 主機、使用 Codespaces 或 Windows Subsystem for Linux (WSL) 中開啟資料夾時,遠端開發或 GitHub Codespaces 擴充功能會自動安裝(或更新)伺服器。(VS Code 也會自動管理伺服器的啟動與停止,因此使用者通常不會察覺其存在。)

Architecture diagram

VS Code API 設計為當從 UI 或工作區擴充功能呼叫時,會自動在正確的機器(本地或遠端)上執行。然而,如果您的擴充功能使用了非 VS Code 提供的 API(例如使用 Node API 或執行 shell 腳本),它在遠端執行時可能無法正常運作。我們建議您測試擴充功能的所有功能,確認其在本地和遠端工作區中皆能正常運作。

偵錯擴充功能

雖然您可以 在遠端環境中安裝開發版本的擴充功能 進行測試,但如果您遇到問題,通常會希望直接在遠端環境中偵錯。在本節中,我們將介紹如何在 GitHub Codespaces本機容器SSH 主機WSL 中編輯、啟動並偵錯您的擴充功能。

通常,進行測試的最佳起點是使用限制埠存取的遠端環境(例如 Codespaces、容器或具有限制性防火牆的遠端 SSH 主機),因為在這些環境中能正常運作的擴充功能,通常在較少限制的環境(如 WSL)中也能順利運作。

使用 GitHub Codespaces 進行偵錯

GitHub Codespaces 預覽版中偵錯您的擴充功能是一個很好的起點,因為您可以使用 VS Code 和基於瀏覽器的 Codespaces 編輯器進行測試與疑難排解。如果偏好,您也可以使用 自訂開發容器

遵循以下步驟

  1. 前往 GitHub 上包含您擴充功能的儲存庫,並 在 codespace 中開啟它,以在基於瀏覽器的編輯器中作業。如果您偏好,也可以 在 VS Code 中開啟 codespace

  2. 雖然 GitHub Codespaces 的預設映像檔應已包含大多數擴充功能所需的先決條件,但您可以在新的 VS Code 終端視窗中安裝任何其他必要的相依套件(例如使用 yarn installsudo apt-get)(⌃⇧` (Windows, Linux Ctrl+Shift+`))。

  3. 最後,按下 F5 或使用 執行與偵錯 (Run and Debug) 視圖,在 codespace 中啟動擴充功能。

    注意:您將無法在出現的視窗中開啟擴充功能原始碼資料夾,但您可以開啟子資料夾或 codespace 中的其他位置。

出現的擴充功能開發主機視窗將包含您正在 codespace 中執行並掛載了偵錯器的擴充功能。

在自訂開發容器中進行偵錯

遵循以下步驟

  1. 若要於本地使用開發容器,請 安裝並設定 Dev Containers 擴充功能,並使用 檔案 > 開啟... / 開啟資料夾... 在本地 VS Code 中開啟您的原始程式碼。若要改用 Codespaces,請前往 GitHub 上包含您擴充功能的儲存庫,並 在 codespace 中開啟它,以在基於瀏覽器的編輯器中作業。如果您偏好,也可以 在 VS Code 中開啟 codespace

  2. 從命令面板 (F1) 選擇 Dev Containers: Add Dev Container Configuration Files...Codespaces: Add Dev Container Configuration Files...,並挑選 Node.js & TypeScript(若未使用 TypeScript 則選 Node.js)以新增必要的容器設定檔。

  3. 選用:此命令執行後,您可以修改 .devcontainer 資料夾的內容,以納入額外的建置或執行階段需求。詳細資訊請參閱深入的 建立開發容器 文件。

  4. 執行 Dev Containers: Reopen in ContainerCodespaces: Reopen in Container,稍等片刻,VS Code 將設定容器並連線。現在您就可以像在本地一樣,從容器內部開發您的原始程式碼。

  5. 在新的 VS Code 終端視窗(⌃⇧` (Windows, Linux Ctrl+Shift+`))中執行 yarn installnpm install,以確保安裝了 Linux 版本的 Node.js 原生相依套件。您也可以安裝其他作業系統或執行階段相依套件,但建議將這些加入 .devcontainer/Dockerfile,以便在重建容器時可用。

  6. 最後,按下 F5 或使用 執行與偵錯 (Run and Debug) 視圖,在此容器內啟動擴充功能並掛載偵錯器。

    注意:您將無法在出現的視窗中開啟擴充功能原始碼資料夾,但您可以開啟子資料夾或容器中的其他位置。

出現的擴充功能開發主機視窗將包含您正在第 2 步定義的容器中執行並掛載了偵錯器的擴充功能。

使用 SSH 偵錯

請依照下列步驟:

  1. 安裝並設定 Remote - SSH 擴充功能 後,從 VS Code 的命令面板 (F1) 選擇 Remote-SSH: Connect to Host... 以連線到主機。

  2. 連線後,使用 檔案 > 開啟... / 開啟資料夾... 選擇包含擴充功能原始碼的遠端資料夾,或從命令面板 (F1) 選擇 Git: Clone 以複製並在遠端主機上開啟。

  3. 在新的 VS Code 終端視窗(⌃⇧` (Windows, Linux Ctrl+Shift+`))中安裝任何缺少的必要相依套件(例如使用 yarn installapt-get)。

  4. 最後,按下 F5 或使用 執行與偵錯 (Run and Debug) 視圖,在遠端主機上啟動擴充功能並掛載偵錯器。

    注意:您將無法在出現的視窗中開啟擴充功能原始碼資料夾,但您可以開啟子資料夾或 SSH 主機上的其他位置。

出現的擴充功能開發主機視窗將包含您正在 SSH 主機上執行並掛載了偵錯器的擴充功能。

使用 WSL 偵錯

遵循以下步驟

  1. 安裝並設定 WSL 擴充功能 後,從 VS Code 的命令面板 (F1) 選擇 WSL: New Window

  2. 在出現的新視窗中,使用 檔案 > 開啟... / 開啟資料夾... 選擇包含擴充功能原始碼的遠端資料夾,或從命令面板 (F1) 選擇 Git: Clone 以複製並在 WSL 中開啟。

    提示:您可以選擇 /mnt/c 資料夾來存取您在 Windows 端複製的任何原始程式碼。

  3. 在新的 VS Code 終端視窗(⌃⇧` (Windows, Linux Ctrl+Shift+`))中安裝任何缺少的必要相依套件(例如使用 apt-get)。您至少應該執行 yarn installnpm install 以確保可以使用 Linux 版本的原生 Node.js 相依套件。

  4. 最後,按下 F5 或使用 執行與偵錯 (Run and Debug) 視圖,以您在本地執行的方式啟動擴充功能並掛載偵錯器。

    注意:您將無法在出現的視窗中開啟擴充功能原始碼資料夾,但您可以開啟子資料夾或 WSL 中的其他位置。

出現的擴充功能開發主機視窗將包含您正在 WSL 中執行並掛載了偵錯器的擴充功能。

安裝開發版本的擴充功能

無論何時,當 VS Code 在 SSH 主機、容器、WSL 或透過 GitHub Codespaces 自動安裝擴充功能時,都會使用市集 (Marketplace) 版本(而非您本地機器上已安裝的版本)。

雖然這在大多數情況下是合理的,但您可能希望在不需要設定偵錯環境的情況下使用(或分享)未發佈版本的擴充功能進行測試。若要安裝未發佈版本的擴充功能,您可以將其打包為 VSIX,並手動安裝到已經連線至遠端環境的 VS Code 視窗中。

遵循以下步驟

  1. 如果這是一個已發佈的擴充功能,您可能需要在 settings.json 中加入 "extensions.autoUpdate": false,以防止其自動更新為最新的市集版本。
  2. 接下來,使用 vsce package 將您的擴充功能打包為 VSIX。
  3. 連線至 codespaceDev ContainersSSH 主機WSL 環境
  4. 在擴充功能視圖的 更多動作 (More Actions) (...) 選單中使用 從 VSIX 安裝... (Install from VSIX...) 命令,將擴充功能安裝到此特定視窗中(而非本機視窗)。
  5. 收到提示時請重新載入。

提示:安裝後,您可以使用 開發人員:顯示正在執行的擴充功能 (Developer: Show Running Extensions) 命令來查看 VS Code 是在本地還是遠端執行該擴充功能。

處理遠端擴充功能的相依關係

擴充功能可以依賴其他擴充功能以取得 API。例如:

  • 擴充功能可以從其 activate 函式匯出 API。
  • 此 API 將可供所有在相同擴充功能主機中執行的擴充功能使用。
  • 消費者擴充功能會在 package.json 中宣告它們使用 extensionDependencies 屬性依賴提供者擴充功能。

當所有擴充功能都在本地執行並共享相同的擴充功能主機時,擴充功能依賴關係運作良好。

在處理遠端場景時,可能會發生在遠端執行的擴充功能依賴於在本地執行的擴充功能。例如,本地擴充功能公開了一個對遠端擴充功能運作至關重要的命令。在這種情況下,我們建議遠端擴充功能將本地擴充功能宣告為 extensionDependency,但問題在於這些擴充功能執行在兩個不同的擴充功能主機上,這意味著來自提供者的 API 對消費者不可用。因此,提供者擴充功能必須完全放棄匯出任何 API 的能力,方法是在其 package.json 中使用 "api": "none"。擴充功能仍然可以使用 VS Code 命令(非同步)進行通訊。

這對提供者擴充功能來說似乎是一個不必要的嚴格限制,但使用 "api": "none" 的擴充功能僅僅放棄了從其 activate 方法傳回 API 的能力。在其他擴充功能主機上執行的消費者擴充功能仍然可以依賴它們並將其啟用。

常見問題

VS Code 的 API 設計為無論您的擴充功能位於何處,都能自動在正確的位置執行。記住這一點,有幾個 API 可以幫助您避免非預期的行為。

執行位置不正確

如果您的擴充功能無法如預期般運作,可能是因為它在錯誤的位置執行。最常見的情況是,您希望擴充功能僅在本地執行,但它卻在遠端執行。您可以使用命令面板 (F1) 中的 開發人員:顯示正在執行的擴充功能 (Developer: Show Running Extensions) 命令來查看擴充功能在何處執行。

如果 開發人員:顯示正在執行的擴充功能 命令顯示 UI 擴充功能被錯誤地當作工作區擴充功能(或反之),請嘗試在擴充功能的 package.json 中設定 extensionKind 屬性,如 擴充功能類型一節 所述。

您可以透過 remote.extensionKind 設定 快速 測試 變更擴充功能類型的效果。此設定是擴充功能 ID 到擴充功能類型的映射。例如,如果您想強制將 Azure Databases 擴充功能作為 UI 擴充功能(而非其預設的工作區型態),並將 Remote - SSH: Editing Configuration Files 擴充功能作為工作區擴充功能(而非其預設的 UI 型態),您可以這樣設定:

{
  "remote.extensionKind": {
    "ms-azuretools.vscode-cosmosdb": ["ui"],
    "ms-vscode-remote.remote-ssh-edit": ["workspace"]
  }
}

使用 remote.extensionKind 可以讓您在無需修改 package.json 並重建的情況下,快速測試已發佈版本的擴充功能。

保存擴充功能資料或狀態

在某些情況下,您的擴充功能可能需要保存不屬於 settings.json 或單獨工作區設定檔(例如 .eslintrc)的狀態資訊。為了解決此問題,VS Code 在啟用期間傳遞給您的擴充功能的 vscode.ExtensionContext 物件上提供了一組實用的儲存屬性。如果您的擴充功能已經利用了這些屬性,無論它在何處執行,它都應該繼續正常運作。

然而,如果您的擴充功能依賴於目前的 VS Code 路徑慣例(例如 ~/.vscode)或特定 OS 資料夾的存在(例如 Linux 上的 ~/.config/Code)來保存資料,您可能會遇到問題。幸運的是,更新您的擴充功能以避免這些挑戰應該很簡單。

如果您保存的是簡單的鍵值對,可以使用 vscode.ExtensionContext.workspaceStatevscode.ExtensionContext.globalState 分別儲存工作區特定或全域的狀態資訊。如果您的資料比鍵值對更複雜,globalStorageUristorageUri 屬性提供了「安全」的 URI,您可以使用這些 URI 在檔案中讀取/寫入全域或工作區特定的資訊。

若要使用這些 API:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    context.subscriptions.push(
        vscode.commands.registerCommand('myAmazingExtension.persistWorkspaceData', async () => {
            if (!context.storageUri) {
                return;
            }

            // Create the extension's workspace storage folder if it doesn't already exist
            try {
                // When folder doesn't exist, and error gets thrown
                await vscode.workspace.fs.stat(context.storageUri);
            } catch {
                // Create the extension's workspace storage folder
                await vscode.workspace.fs.createDirectory(context.storageUri)
            }

            const workspaceData = vscode.Uri.joinPath(context.storageUri, 'workspace-data.json');
            const writeData = new TextEncoder().encode(JSON.stringify({ now: Date.now() }));
            vscode.workspace.fs.writeFile(workspaceData, writeData);
        }
    ));

    context.subscriptions.push(
        vscode.commands.registerCommand('myAmazingExtension.persistGlobalData', async () => {

        if (!context.globalStorageUri) {
            return;
        }

        // Create the extension's global (cross-workspace) folder if it doesn't already exist
        try {
            // When folder doesn't exist, and error gets thrown
            await vscode.workspace.fs.stat(context.globalStorageUri);
        } catch {
            await vscode.workspace.fs.createDirectory(context.globalStorageUri)
        }

        const workspaceData = vscode.Uri.joinPath(context.globalStorageUri, 'global-data.json');
        const writeData = new TextEncoder().encode(JSON.stringify({ now: Date.now() }));
        vscode.workspace.fs.writeFile(workspaceData, writeData);
    ));
}

在機器間同步使用者全域狀態

如果您的擴充功能需要在不同機器間保留使用者狀態,請使用 vscode.ExtensionContext.globalState.setKeysForSync 將狀態提供給 設定同步 (Settings Sync)。這有助於防止在多台機器上重複對使用者顯示相同的歡迎或更新頁面。

擴充功能能力 (Extension Capabilities) 主題中有一個使用 setKeysforSync 的範例。

保存祕密資訊

如果您的擴充功能需要保存密碼或其他祕密,建議使用 Visual Studio Code 的 SecretStorage API,它提供了一種基於加密的檔案系統安全儲存文字的方法。例如,在桌面上,我們使用 Electron 的 safeStorage API 在將祕密儲存到檔案系統之前進行加密。此 API 始終會將祕密儲存在用戶端,但無論您的擴充功能在哪裡執行,您都可以使用此 API 來擷取相同的祕密值。

注意:此 API 是保存密碼與祕密資訊的推薦方式。您 不應 使用 vscode.ExtensionContext.workspaceStatevscode.ExtensionContext.globalState 來儲存祕密,因為這些 API 會以明文儲存資料。

以下是一個範例:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  // ...
  const myApiKey = context.secrets.get('apiKey');
  // ...
  context.secrets.delete('apiKey');
  // ...
  context.secrets.store('apiKey', myApiKey);
}

使用剪貼簿

過去,擴充功能開發者會使用像是 clipboardy 之類的 Node.js 模組來與剪貼簿互動。遺憾的是,如果您在工作區擴充功能中使用這些模組,它們會使用遠端的剪貼簿,而非使用者本機的剪貼簿。VS Code 剪貼簿 API 解決了這個問題。無論呼叫它的擴充功能類型為何,它始終在本地執行。

若要在擴充功能中使用 VS Code 剪貼簿 API:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('myAmazingExtension.clipboardIt', async () => {
      // Read from clipboard
      const text = await vscode.env.clipboard.readText();

      // Write to clipboard
      await vscode.env.clipboard.writeText(
        `It looks like you're copying "${text}". Would you like help?`
      );
    })
  );
}

在本地瀏覽器或應用程式中開啟項目

產生處理程序或使用像 opn 這樣的模組為特定 URI 啟動瀏覽器或其他應用程式在本地場景中運作良好,但工作區擴充功能在遠端執行,這可能會導致應用程式在錯誤的一端啟動。VS Code 遠端開發 部分地 shim 了 opn node 模組,以允許現有的擴充功能運作。您可以使用 URI 呼叫該模組,VS Code 會讓該 URI 的預設應用程式在用戶端出現。然而,這並非完整的實作,因為不支援選項,且不會回傳 child_process 物件。

除了依賴第三方 node 模組,我們建議擴充功能利用 vscode.env.openExternal 方法,為給定的 URI 啟動本機作業系統上註冊的預設應用程式。更棒的是,vscode.env.openExternal 會自動進行 localhost 埠轉發! 您可以使用它指向遠端機器或 codespace 上的本地 Web 伺服器,即使該埠在外部被封鎖,也能服務內容。

注意:目前 Codespaces 基於瀏覽器的編輯器中的轉發機制僅支援 http 和 https 請求。不過,當從 VS Code 連線到 codespace 時,您可以與任何 TCP 連線進行互動。

若要使用 vscode.env.openExternal API:

import * as vscode from 'vscode';

export async function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('myAmazingExtension.openExternal', () => {
      // Example 1 - Open the VS Code homepage in the default browser.
      vscode.env.openExternal(vscode.Uri.parse('https://vscode.com.tw'));

      // Example 2 - Open an auto-forwarded localhost HTTP server.
      vscode.env.openExternal(vscode.Uri.parse('https://:3000'));

      // Example 3 - Open the default email application.
      vscode.env.openExternal(vscode.Uri.parse('mailto:<fill in your email here>'));
    })
  );
}

轉發 localhost

雖然 vscode.env.openExternal 中的 localhost 轉發機制很有用,但在某些情況下,您可能希望在不實際啟動新瀏覽器視窗或應用程式的情況下轉發某些內容。這就是 vscode.env.asExternalUri API 的用途。

注意:目前 Codespaces 基於瀏覽器的編輯器中的轉發機制僅支援 http 和 https 請求。不過,當從 VS Code 連線到 codespace 時,您可以與任何 TCP 連線進行互動。

若要使用 vscode.env.asExternalUri API:

import * as vscode from 'vscode';
import { getExpressServerPort } from './server';

export async function activate(context: vscode.ExtensionContext) {

    const dynamicServerPort = await getWebServerPort();

    context.subscriptions.push(vscode.commands.registerCommand('myAmazingExtension.forwardLocalhost', async () =>

        // Make the port available locally and get the full URI
        const fullUri = await vscode.env.asExternalUri(
            vscode.Uri.parse(`https://:${dynamicServerPort}`));

        // ... do something with the fullUri ...

    }));
}

請務必注意,API 回傳的 URI 可能根本沒有參照到 localhost,因此您應該使用其完整內容。對於 Codespaces 基於瀏覽器的編輯器來說,這一點尤為重要,因為那裡無法使用 localhost。

回呼與 URI 處理常式

vscode.window.registerUriHandler API 允許您的擴充功能註冊一個自訂 URI,如果它在瀏覽器中被開啟,將會觸發擴充功能中的回呼函式。註冊 URI 處理常式的常見使用案例是在實作 OAuth 2.0 驗證提供者(例如 Azure AD)的服務登入時。不過,它可以用於任何您希望外部應用程式或瀏覽器將資訊發送到您的擴充功能的場景。

VS Code 中的遠端開發與 Codespaces 擴充功能將透明地處理將 URI 傳遞給您的擴充功能,無論它實際上在哪裡執行(本地或遠端)。但是,vscode:// URI 在 Codespaces 基於瀏覽器的編輯器中無法運作,因為在瀏覽器中開啟這些 URI 會嘗試將其傳遞給本機 VS Code 客戶端,而非基於瀏覽器的編輯器。幸運的是,使用 vscode.env.asExternalUri API 可以輕鬆解決這個問題。

讓我們結合使用 vscode.window.registerUriHandlervscode.env.asExternalUri 來建立一個 OAuth 驗證回呼範例:

import * as vscode from 'vscode';

// This is ${publisher}.${name} from package.json
const extensionId = 'my.amazing-extension';

export async function activate(context: vscode.ExtensionContext) {
  // Register a URI handler for the authentication callback
  vscode.window.registerUriHandler({
    handleUri(uri: vscode.Uri): vscode.ProviderResult<void> {
      // Add your code for what to do when the authentication completes here.
      if (uri.path === '/auth-complete') {
        vscode.window.showInformationMessage('Sign in successful!');
      }
    }
  });

  // Register a sign in command
  context.subscriptions.push(
    vscode.commands.registerCommand(`${extensionId}.signin`, async () => {
      // Get an externally addressable callback URI for the handler that the authentication provider can use
      const callbackUri = await vscode.env.asExternalUri(
        vscode.Uri.parse(`${vscode.env.uriScheme}://${extensionId}/auth-complete`)
      );

      // Add your code to integrate with an authentication provider here - we'll fake it.
      vscode.env.clipboard.writeText(callbackUri.toString());
      await vscode.window.showInformationMessage(
        'Open the URI copied to the clipboard in a browser window to authorize.'
      );
    })
  );
}

當在 VS Code 中執行此範例時,它會接線一個可作為驗證提供者回呼的 vscode://vscode-insiders:// URI。而在 Codespaces 基於瀏覽器的編輯器中執行時,它會接線一個 https://*.github.dev URI,無需任何程式碼變更或特殊條件。

雖然 OAuth 超出了本文檔的範圍,但請注意,如果您將此範例改編為實際的驗證提供者,您可能需要在提供者前方建立一個代理服務。這是因為並非所有提供者都允許 vscode:// 回呼 URI,其他提供者也不允許透過 HTTPS 的回呼使用萬用字元主機名稱。我們也建議盡可能使用 具備 PKCE 的 OAuth 2.0 授權碼流程(例如 Azure AD 支援 PKCE),以提高回呼的安全性。

在遠端執行或 Codespaces 瀏覽器編輯器中執行的行為差異

在某些情況下,您的工作區擴充功能可能需要在遠端執行時改變行為。在另一些情況下,您可能希望在 Codespaces 基於瀏覽器的編輯器中執行時改變行為。VS Code 提供了三個 API 來檢測這些情況:vscode.env.uiKindextension.extensionKindvscode.env.remoteName

接著,您可以依照下列方式使用這三個 API:

import * as vscode from 'vscode';

export async function activate(context: vscode.ExtensionContext) {
  // extensionKind returns ExtensionKind.UI when running locally, so use this to detect remote
  const extension = vscode.extensions.getExtension('your.extensionId');
  if (extension.extensionKind === vscode.ExtensionKind.Workspace) {
    vscode.window.showInformationMessage('I am running remotely!');
  }

  // Codespaces browser-based editor will return UIKind.Web for uiKind
  if (vscode.env.uiKind === vscode.UIKind.Web) {
    vscode.window.showInformationMessage('I am running in the Codespaces browser editor!');
  }

  // VS Code will return undefined for remoteName if working with a local workspace
  if (typeof vscode.env.remoteName === 'undefined') {
    vscode.window.showInformationMessage('Not currently connected to a remote workspace.');
  }
}

使用命令在擴充功能間通訊

有些擴充功能在啟用時會回傳 API,供其他擴充功能使用(透過 vscode.extension.getExtension(extensionName).exports)。雖然如果涉及的所有擴充功能都在同一端(全部是 UI 擴充功能或全部是工作區擴充功能),這可以運作,但它們無法在 UI 和工作區擴充功能之間運作。

幸運的是,無論擴充功能位於何處,VS Code 都會自動將執行的任何命令路由到正確的擴充功能。您可以自由呼叫任何命令(包括其他擴充功能提供的命令),而無需擔心影響。

如果您有一組需要相互互動的擴充功能,使用私有命令公開功能可以幫助您避免意外影響。但是,您作為參數傳遞的任何物件在傳輸前都會被「字串化」 (JSON.stringify),因此該物件不能包含循環參考,且在另一端會變成「純 JavaScript 物件」。

例如

import * as vscode from 'vscode';

export async function activate(context: vscode.ExtensionContext) {
  // Register the private echo command
  const echoCommand = vscode.commands.registerCommand(
    '_private.command.called.echo',
    (value: string) => {
      return value;
    }
  );
  context.subscriptions.push(echoCommand);
}

有關使用命令的詳細資訊,請參閱 命令 API 指南

使用 Webview API

與剪貼簿 API 一樣,Webview API 始終在使用者本機或瀏覽器中執行,即使從工作區擴充功能呼叫也是如此。這意味著許多基於 webview 的擴充功能應該可以直接運作,即使在遠端工作區或 Codespaces 中使用也是如此。不過,為了讓您的 webview 擴充功能在遠端執行時正常運作,仍需注意一些考量事項。

始終使用 asWebviewUri

您應該使用 asWebviewUri API 來管理擴充功能資源。為了確保 Codespaces 基於瀏覽器的編輯器能與您的擴充功能搭配運作,您必須使用此 API,而不是寫死 vscode-resource:// URI。詳細資訊請參閱 Webview API 指南,以下是一個快速範例:

您可以在內容中依照下列方式使用該 API:

// Create the webview
const panel = vscode.window.createWebviewPanel(
  'catWebview',
  'Cat Webview',
  vscode.ViewColumn.One
);

// Get the content Uri
const catGifUri = panel.webview.asWebviewUri(
  vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif')
);

// Reference it in your content
panel.webview.html = `<!DOCTYPE html>
<html>
<body>
    <img src="${catGifUri}" width="300" />
</body>
</html>`;

對動態 webview 內容使用訊息傳遞 API

VS Code webview 包含一個 訊息傳遞 (message passing) API,允許您動態更新 webview 內容,而無需使用本機 Web 伺服器。即使您的擴充功能正在執行一些您想要與之互動以更新 webview 內容的本機 Web 服務,您也可以直接從擴充功能本身執行此操作,而不是從 HTML 內容直接呼叫。

對於遠端開發與 GitHub Codespaces 而言,這是一個確保您的 webview 程式碼在 VS Code 與 Codespaces 基於瀏覽器的編輯器中皆能正常運作的重要模式。

為什麼選擇訊息傳遞而非 localhost Web 伺服器?

替代模式是在 iframe 中提供 Web 內容,或讓 webview 內容直接與 localhost 伺服器互動。遺憾的是,預設情況下,webview 內的 localhost 會解析為開發者的本機機器。這意味著對於遠端執行的工作區擴充功能,它所建立的 webview 將無法存取擴充功能所啟動的本機伺服器。即使您使用機器的 IP,您所連線的埠在雲端 VM 或容器中通常也會預設被封鎖。即便這在 VS Code 中可行,它也無法在 Codespaces 基於瀏覽器的編輯器中運作。

以下是使用 Remote - SSH 擴充功能時出現問題的說明,但此問題在 Dev Containers 和 GitHub Codespaces 中同樣存在:

Webview problem

如果可能,您應該避免這樣做,因為這會使您的擴充功能變得非常複雜。訊息傳遞 (Message passing) API 可以在沒有這些麻煩的情況下實現相同的使用者體驗。擴充功能本身會在遠端的 VS Code Server 中執行,因此它可以透明地與您的擴充功能因 webview 傳遞的訊息而啟動的任何 Web 伺服器進行互動。

從 webview 使用 localhost 的變通方法

如果您因為某些原因無法使用 訊息傳遞 (message passing) API,有兩種選項可與 VS Code 中的遠端開發與 GitHub Codespaces 擴充功能搭配運作。

每個選項都允許 webview 內容透過 VS Code 用於與 VS Code Server 通訊的相同通道進行路由。例如,如果您針對 Remote - SSH 更新上一節的說明,您會得到如下結果:

Webview Solution

選項 1 - 使用 asExternalUri

VS Code 1.40 引入了 vscode.env.asExternalUri API,允許擴充功能以程式化的方式遠端轉發本地 httphttps 請求。當您的擴充功能在 VS Code 中執行時,您可以使用相同的 API 將請求從 webview 轉發到 localhost Web 伺服器。

使用此 API 為 iframe 取得完整的 URI 並將其新增到您的 HTML 中。您還需要在 webview 中啟用指令碼,並在 HTML 內容中新增 CSP。

// Use asExternalUri to get the URI for the web server
const dynamicWebServerPort = await getWebServerPort();
const fullWebServerUri = await vscode.env.asExternalUri(
  vscode.Uri.parse(`https://:${dynamicWebServerPort}`)
);

// Create the webview
const panel = vscode.window.createWebviewPanel(
  'asExternalUriWebview',
  'asExternalUri Example',
  vscode.ViewColumn.One,
  {
    enableScripts: true
  }
);

const cspSource = panel.webview.cspSource;
panel.webview.html = `<!DOCTYPE html>
        <head>
            <meta
                http-equiv="Content-Security-Policy"
                content="default-src 'none'; frame-src ${fullWebServerUri} ${cspSource} https:; img-src ${cspSource} https:; script-src ${cspSource}; style-src ${cspSource};"
            />
        </head>
        <body>
        <!-- All content from the web server must be in an iframe -->
        <iframe src="${fullWebServerUri}">
    </body>
    </html>`;

請注意,在上述範例中,在 iframe 中提供的任何 HTML 內容 都需要使用相對路徑,而不是寫死 localhost

選項 2 - 使用埠對應

如果您 不打算支援 Codespaces 基於瀏覽器的編輯器,則可以使用 Webview API 中提供的 portMapping 選項。(此方法也適用於來自 VS Code 客戶端的 Codespaces,但不適用於瀏覽器)。

若要使用埠對應,請在建立 webview 時傳入 portMapping 物件:

const LOCAL_STATIC_PORT = 3000;
const dynamicServerPort = await getWebServerPort();

// Create webview and pass portMapping in
const panel = vscode.window.createWebviewPanel(
  'remoteMappingExample',
  'Remote Mapping Example',
  vscode.ViewColumn.One,
  {
    portMapping: [
      // This maps localhost:3000 in the webview to the web server port on the remote host.
      { webviewPort: LOCAL_STATIC_PORT, extensionHostPort: dynamicServerPort }
    ]
  }
);

// Reference the port in any full URIs you reference in your HTML.
panel.webview.html = `<!DOCTYPE html>
    <body>
        <!-- This will resolve to the dynamic server port on the remote machine -->
        <img src="https://:${LOCAL_STATIC_PORT}/canvas.png">
    </body>
    </html>`;

在此範例中,無論在遠端或本地情況下,任何對 https://:3000 發出的請求,都將自動對應到 Express.js Web 伺服器正在執行的動態埠。

使用原生 Node.js 模組

隨附(或動態取得)於 VS Code 擴充功能的原生模組必須 使用 Electron 的 electron-rebuild 重新編譯。然而,VS Code Server 執行的是標準(非 Electron)版本的 Node.js,這可能會導致二進位檔在遠端使用時失敗。

解決此問題的方法:

  1. 將兩組二進位檔(Electron 和標準 Node.js)納入(或動態取得)VS Code 所提供的 Node.js 版本中的「模組」版本。
  2. 檢查 vscode.extensions.getExtension('your.extensionId').extensionKind === vscode.ExtensionKind.Workspace 以根據擴充功能是在遠端還是本地執行來設定正確的二進位檔。
  3. 您可能也想同時透過 遵循類似邏輯 來新增對非 x86_64 目標和 Alpine Linux 的支援。

您可以透過前往 說明 > 開發人員工具 (Help > Developer Tools) 並在主控台中輸入 process.versions.modules 來找到 VS Code 使用的「模組」版本。然而,為了確保原生模組能在不同的 Node.js 環境中無縫運作,您可能需要針對所有您想支援的 Node.js「模組」版本和平台(Electron Node.js、官方 Node.js Windows/Darwin/Linux,所有版本)編譯原生模組。node-tree-sitter 模組就是一個執行得很好的範例。

支援非 x86_64 主機或 Alpine Linux 容器

如果您的擴充功能完全是用 JavaScript/TypeScript 編寫的,則您可能不需要做任何事來增加對其他處理器架構或基於 musl 的 Alpine Linux 的支援。

然而,如果您的擴充功能適用於 Debian 9+、Ubuntu 16.04+ 或 RHEL / CentOS 7+ 遠端 SSH 主機、容器或 WSL,但在受支援的非 x86_64 主機(例如 ARMv7l)或 Alpine Linux 容器上失敗,則該擴充功能可能包含 x86_64 glibc 特定的原生程式碼或執行階段,這些程式碼或執行階段在這些架構/作業系統上會失敗。

例如,您的擴充功能可能只包含 x86_64 編譯版本的原生模組或執行階段。對於 Alpine Linux,由於 Alpine Linux (musl) 與其他發行版 (glibc) 在 libc 實作方式上的 根本差異,所包含的原生程式碼或執行階段可能無法運作。

解決此問題的方法:

  1. 如果您是動態取得編譯後的程式碼,可以透過 process.arch 偵測非 x86_64 目標並下載為正確架構編譯的版本來新增支援。如果您是在擴充功能中內建所有支援架構的二進位檔,則可以使用此邏輯來選擇正確的檔案。

  2. 對於 Alpine Linux,您可以透過 await fs.exists('/etc/alpine-release') 偵測作業系統,並再次下載或使用適用於基於 musl 作業系統的正確二進位檔。

  3. 如果您不想支援這些平台,也可以使用相同的邏輯來提供適當的錯誤訊息。

需要注意的是,某些第三方 npm 模組包含可能導致此問題的原生程式碼。因此,在某些情況下,您可能需要與 npm 模組作者合作以新增額外的編譯目標。

避免使用 Electron 模組

雖然依賴非擴充功能 API 暴露的內建 Electron 或 VS Code 模組很方便,但必須注意 VS Code Server 執行的是標準(非 Electron)版本的 Node.js。這些模組在遠端執行時將會缺失。有少數例外情況,其中有特定程式碼使其可以運作。

請使用基礎 Node.js 模組或擴充功能 VSIX 中的模組以避免這些問題。如果絕對必須使用 Electron 模組,請務必在該模組缺失時提供後備方案。

以下範例將在找到 Electron original-fs node 模組時使用它,否則後備使用基礎 Node.js fs 模組:

function requireWithFallback(electronModule: string, nodeModule: string) {
  try {
    return require(electronModule);
  } catch (err) {}
  return require(nodeModule);
}

const fs = requireWithFallback('original-fs', 'fs');

請盡可能避免這些情況。

已知問題

有一些擴充功能問題可以透過為工作區擴充功能新增功能來解決。下表列出了正在考慮中的已知問題:

問題 說明
無法從工作區擴充功能存取連接的裝置 存取本地連接裝置的擴充功能在遠端執行時將無法連線到它們。克服此問題的一種方法是建立一個配套的 UI 擴充功能,其職責是存取連接的裝置並提供遠端擴充功能也能呼叫的命令。
另一種方法是反向隧道,目前正在 VS Code 儲存庫問題 中進行追蹤。

問題與回饋

© . This site is unofficial and not affiliated with Microsoft.