尝试以扩展 VS Code 中的代理模式!

支持远程开发和 GitHub Codespaces

Visual Studio Code 远程开发 允许你透明地与位于其他机器(无论是虚拟的还是物理的)上的源代码和运行时环境进行交互。GitHub Codespaces 是一项服务,它通过可从 VS Code 和基于浏览器的编辑器访问的托管云环境来扩展这些功能。

为了确保性能,远程开发和 GitHub Codespaces 都会透明地远程运行某些 VS Code 扩展。然而,这可能会对扩展的工作方式产生细微影响。虽然许多扩展无需任何修改即可工作,但你可能需要进行更改,以使你的扩展在所有环境中都能正常工作,尽管这些更改通常很小。

本文总结了扩展作者需要了解的关于远程开发和 Codespaces 的内容,包括扩展架构、如何在远程工作区或 Codespaces 中调试扩展,以及关于扩展无法正常工作时应如何处理的建议。

架构和扩展类型

为了让用户尽可能透明地使用远程开发或 Codespaces,VS Code 区分了两种类型的扩展:

  • UI 扩展:这些扩展为 VS Code 用户界面做出贡献,并且始终在用户的本地机器上运行。UI 扩展无法直接访问远程工作区中的文件,也无法运行安装在该工作区或机器上的脚本/工具。UI 扩展的示例包括:主题、代码片段、语言语法和键位映射。

  • 工作区扩展:这些扩展在工作区所在的机器上运行。在本地工作区中,工作区扩展在本地机器上运行。在远程工作区或使用 Codespaces 时,工作区扩展在远程机器/环境中运行。工作区扩展可以访问工作区中的文件,以提供丰富的多文件语言服务、调试器支持,或对工作区中的多个文件执行复杂操作(直接或通过调用脚本/工具)。虽然工作区扩展不专注于修改 UI,但它们也可以贡献资源管理器、视图和其他 UI 元素。

当用户安装扩展时,VS Code 会根据其类型自动将其安装到正确的位置。如果一个扩展可以作为两种类型之一运行,VS Code 将尝试为当前情况选择最优类型;UI 扩展将在 VS Code 的本地扩展主机中运行,而工作区扩展将在位于小型 VS Code 服务器中的远程扩展主机中运行(如果它存在于远程工作区中),否则如果它存在于本地,则将在 VS Code 的本地扩展主机中运行。为确保最新的 VS Code 客户端功能可用,服务器需要与 VS Code 客户端版本精确匹配。因此,当你通过容器、远程 SSH 主机、Codespaces 或适用于 Linux 的 Windows 子系统 (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 或使用运行和调试视图在 codespace 内启动扩展。

    注意: 你将无法在弹出的窗口中打开扩展源代码文件夹,但你可以打开 codespace 中的子文件夹或任何其他位置。

弹出的扩展开发主机窗口将包含在 codespace 中运行的扩展,并附加了调试器。

在自定义开发容器中调试

请按照以下步骤操作:

  1. 要在本地使用开发容器,请安装并配置 Dev Containers 扩展,然后使用文件 > 打开... / 打开文件夹... 在 VS Code 中本地打开你的源代码。若要改用 Codespaces,请导航到 GitHub 上包含你的扩展的仓库,然后在 codespace 中打开它,以便在基于浏览器的编辑器中进行操作。如果喜欢,你也可以在 VS Code 中打开 codespace

  2. 从命令面板 (F1) 中选择 Dev Containers: 添加开发容器配置文件...Codespaces: 添加开发容器配置文件...,然后选择 Node.js & TypeScript(如果你不使用 TypeScript 则选择 Node.js),以添加所需的容器配置文件。

  3. 可选: 此命令运行后,你可以修改 .devcontainer 文件夹的内容,以包含额外的构建或运行时要求。有关详细信息,请参阅深入的创建开发容器文档。

  4. 运行 Dev Containers: 在容器中重新打开Codespaces: 添加开发容器配置文件...,很快,VS Code 将设置容器并连接。你现在可以像在本地一样,在容器内部开发源代码。

  5. 在新的 VS Code 终端窗口中运行 yarn installnpm install (⌃⇧` (Windows, Linux Ctrl+Shift+`)),以确保安装了 Linux 版本的 Node.js 原生依赖项。你还可以安装其他操作系统或运行时依赖项,但你可能也希望将这些添加到 .devcontainer/Dockerfile 中,以便在重新构建容器时它们可用。

  6. 最后,按 F5 或使用运行和调试视图在此同一容器内启动扩展并附加调试器。

    注意: 你将无法在弹出的窗口中打开扩展源代码文件夹,但你可以打开容器中的子文件夹或任何其他位置。

弹出的扩展开发主机窗口将包含在步骤 2 中定义的容器中运行的扩展,并附加了调试器。

使用 SSH 调试

按照步骤操作

  1. 安装和配置 Remote - SSH 扩展后,在 VS Code 的命令面板 (F1) 中选择 Remote-SSH: 连接到主机... 来连接到主机。

  2. 连接后,要么使用文件 > 打开... / 打开文件夹... 选择包含你扩展源代码的远程文件夹,要么从命令面板 (F1) 中选择 Git: 克隆 以克隆并在远程主机上打开它。

  3. 在新的 VS Code 终端窗口中安装任何可能缺失的所需依赖项(例如,使用 yarn installapt-get)(⌃⇧` (Windows, Linux Ctrl+Shift+`))。

  4. 最后,按 F5 或使用运行和调试视图在远程主机上启动扩展并附加调试器。

    注意: 你将无法在弹出的窗口中打开扩展源代码文件夹,但你可以打开 SSH 主机上的子文件夹或任何其他位置。

弹出的扩展开发主机窗口将包含在 SSH 主机上运行的扩展,并附加了调试器。

使用 WSL 调试

请按照以下步骤操作:

  1. 安装和配置 WSL 扩展后,从 VS Code 的命令面板 (F1) 中选择 WSL: 新窗口

  2. 在弹出的新窗口中,要么使用文件 > 打开... / 打开文件夹... 选择包含你扩展源代码的远程文件夹,要么从命令面板 (F1) 中选择 Git: 克隆 以克隆并在 WSL 中打开它。

    提示: 你可以选择 /mnt/c 文件夹来访问你在 Windows 端上的任何克隆的源代码。

  3. 在新的 VS Code 终端窗口中安装任何可能缺失的所需依赖项(例如,使用 apt-get)(⌃⇧` (Windows, Linux Ctrl+Shift+`))。你至少会想运行 yarn installnpm install 以确保 Linux 版本的原生 Node.js 依赖项可用。

  4. 最后,按 F5 或使用运行和调试视图启动扩展并像在本地一样附加调试器。

    注意: 你将无法在弹出的窗口中打开扩展源代码文件夹,但你可以打开 WSL 中的子文件夹或任何其他位置。

弹出的扩展开发主机窗口将包含在 WSL 中运行的扩展,并附加了调试器。

安装扩展的开发版本

无论何时 VS Code 自动在 SSH 主机、容器或 WSL 内部,或者通过 GitHub Codespaces 安装扩展,都会使用市场版本(而不是你本地机器上已安装的版本)。

尽管这在大多数情况下都说得通,但你可能希望使用(或共享)未发布的扩展版本进行测试,而无需设置调试环境。要安装未发布的扩展版本,你可以将扩展打包为 VSIX,并手动将其安装到已连接到正在运行的远程环境的 VS Code 窗口中。

请按照以下步骤操作:

  1. 如果这是一个已发布的扩展,你可能需要将 "extensions.autoUpdate": false 添加到 settings.json 中,以防止它自动更新到最新的市场版本。
  2. 接下来,使用 vsce package 将你的扩展打包为 VSIX。
  3. 连接到 codespace开发容器SSH 主机WSL 环境
  4. 使用“扩展”视图更多操作 (...) 菜单中提供的从 VSIX 安装... 命令,将扩展安装到这个特定的窗口(而不是本地窗口)。
  5. 出现提示时重新加载。

提示: 安装后,你可以使用开发者:显示正在运行的扩展命令来查看 VS Code 是在本地还是远程运行该扩展。

处理远程扩展的依赖项

扩展可以依赖其他扩展提供的 API。例如:

  • 一个扩展可以从其 activate 函数中导出 API。
  • 此 API 将对在同一扩展主机中运行的所有扩展可用。
  • 消费扩展在其 package.json 中使用 extensionDependencies 属性声明它们依赖于提供方扩展。

当所有扩展都在本地运行并共享同一扩展主机时,扩展依赖项可以正常工作。

在处理远程场景时,远程运行的扩展可能会依赖于本地运行的扩展。例如,本地扩展暴露了一个对远程扩展功能至关重要的命令。在这种情况下,我们建议远程扩展将本地扩展声明为 extensionDependency,但问题是这些扩展在两个不同的扩展主机上运行,这意味着提供方提供的 API 对消费方不可用。因此,要求提供方扩展通过在其扩展的 package.json 中使用 "api": "none" 来完全放弃导出任何 API 的能力。扩展仍然可以使用 VS Code 命令进行通信(这些命令是异步的)。

这对提供方扩展来说可能看似不必要的严格限制,但使用 "api": "none" 的扩展仅仅是放弃了从其 activate 方法返回 API 的能力。在其他扩展主机上执行的消费方扩展仍然可以依赖它们并被激活。

常见问题

VS Code 的 API 被设计为无论你的扩展位于何处,都能自动在正确的位置运行。考虑到这一点,有一些 API 可以帮助你避免意外行为。

错误的执行位置

如果你的扩展未按预期运行,它可能在错误的位置运行。最常见的情况是,当你期望扩展仅在本地运行时,它却在远程运行。你可以使用命令面板 (F1) 中的开发者:显示正在运行的扩展命令来查看扩展的运行位置。

如果开发者:显示正在运行的扩展命令显示 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)或某些操作系统文件夹(例如 Linux 上的 ~/.config/Code)的存在来持久化数据,你可能会遇到问题。幸运的是,更新你的扩展并避免这些挑战应该很简单。

如果你正在持久化简单的键值对,可以使用 vscode.ExtensionContext.workspaceStatevscode.ExtensionContext.globalState 分别存储工作区特定或全局状态信息。如果你的数据比键值对更复杂,globalStorageUristorageUri 属性提供了“安全”的 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 将状态提供给设置同步。这有助于避免在多台机器上向用户显示相同的欢迎或更新页面。

扩展功能主题中有一个使用 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 远程开发部分地填充了 opn Node 模块,以允许现有扩展正常运行。你可以使用 URI 调用该模块,VS Code 将使该 URI 的默认应用程序在客户端显示。然而,这不是一个完整的实现,因为不支持选项,并且不返回 child_process 对象。

我们建议扩展利用 vscode.env.openExternal 方法在你的本地操作系统上为给定的 URI 启动默认注册的应用程序,而不是依赖第三方 Node 模块。更好的是,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.js.cn'));

      // 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 在浏览器中打开时,将在你的扩展中触发一个回调函数。注册 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,这些 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>`;

使用消息传递 API 实现动态 Webview 内容

VS Code Webview 包含一个消息传递 API,它允许你动态更新 Webview 内容,而无需使用本地 Web 服务器。即使你的扩展正在运行一些你希望交互的本地 Web 服务以更新 Webview 内容,你也可以从扩展本身进行,而不是直接从 HTML 内容进行。

这是远程开发和 GitHub Codespaces 的一个重要模式,用于确保你的 Webview 代码在 VS Code 和 Codespaces 基于浏览器的编辑器中都能正常工作。

为什么使用消息传递而不是 localhost Web 服务器?

另一种模式是在 iframe 中提供 Web 内容,或者让 Webview 内容直接与 localhost 服务器交互。不幸的是,默认情况下,Webview 内部的 localhost 将解析为开发人员的本地机器。这意味着对于远程运行的工作区扩展,它创建的 Webview 将无法访问扩展生成的本地服务器。即使你使用机器的 IP,你连接的端口通常也会在云虚拟机或容器中默认被阻止。即使这在 VS Code 中有效,它也无法在 Codespaces 基于浏览器的编辑器中工作。

这是使用 Remote - SSH 扩展时问题的说明,但该问题也存在于开发容器和 GitHub Codespaces 中:

Webview problem

如果可能,你应该避免这样做,因为它会显著复杂化你的扩展。消息传递 API 可以实现相同类型的用户体验,而不会带来这些麻烦。扩展本身将在远程端的 VS Code 服务器中运行,因此它可以透明地与你的扩展因 Webview 传递给它的任何消息而启动的任何 Web 服务器进行交互。

从 Webview 使用 localhost 的变通方法

如果由于某种原因无法使用消息传递 API,则有两种选项可与 VS Code 中的远程开发和 GitHub Codespaces 扩展一起使用。

每个选项都允许 Webview 内容通过 VS Code 用于与 VS Code 服务器通信的同一通道进行路由。例如,如果我们在上一节中更新 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 服务器运行的是标准(非 Electron)版本的 Node.js,这可能导致二进制文件在远程使用时失败。

解决此问题的方法:

  1. 包含(或动态获取)VS Code 附带的 Node.js “模块”版本所需的两套二进制文件(Electron 和标准 Node.js)。
  2. 检查 vscode.extensions.getExtension('your.extensionId').extensionKind === vscode.ExtensionKind.Workspace 以根据扩展是在远程还是本地运行来设置正确的二进制文件。
  3. 你可能还希望通过遵循类似的逻辑,同时添加对非 x86_64 目标和 Alpine Linux 的支持。

你可以通过转到帮助 > 开发者工具并在控制台中输入 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 服务器运行的是标准(非 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 仓库问题中跟踪。

问题和反馈