支持远程开发和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 或在 Windows Subsystem for Linux (WSL) 中打开文件夹时,远程开发或 GitHub Codespaces 扩展会自动安装(或更新)服务器。(VS Code 还会自动管理服务器的启动和停止,因此用户不会察觉到它的存在。)

架构图

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上的扩展的仓库,并在代码空间中打开它以在基于浏览器的编辑器中使用它。如果您愿意,也可以在VS Code中打开代码空间

  2. 虽然GitHub Codespaces的默认镜像应该已经包含了大多数扩展所需的所有先决条件,但你可以在新的VS Code终端窗口中安装任何其他所需的依赖项(例如,使用yarn installsudo apt-get)(⌃⇧` (Windows, Linux Ctrl+Shift+`))。

  3. 最后,按下 F5 或使用 运行和调试 视图在代码空间内启动扩展。

    注意: 您将无法在出现的窗口中打开扩展源代码文件夹,但您可以打开子文件夹或代码空间中的其他位置。

出现的扩展开发主机窗口将包括在代码空间中运行的扩展,调试器已附加到该扩展。

在自定义开发容器中进行调试

按照以下步骤操作:

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

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

  3. 可选: 此命令运行后,您可以修改 .devcontainer 文件夹的内容以包含其他构建或运行时要求。详情请参阅深入的 创建开发容器 文档。

  4. 运行Dev Containers: Reopen in ContainerCodespaces: Add Dev Container Configuration Files...,稍等片刻,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: Connect to Host...以连接到主机。

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

  3. 安装任何可能缺失的依赖项(例如使用yarn installapt-get)在新的VS Code终端窗口中(⌃⇧` (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 自动安装扩展时,都会使用 Marketplace 版本(而不是本地机器上已安装的版本)。

虽然在大多数情况下这是有意义的,但您可能希望使用(或共享)未发布的扩展版本进行测试,而不必设置调试环境。要安装未发布的扩展版本,您可以将扩展打包为VSIX并手动安装到已连接到正在运行的远程环境的VS Code窗口中。

按照以下步骤操作:

  1. 如果这是一个已发布的扩展,您可能希望将"extensions.autoUpdate": false添加到settings.json中,以防止其自动更新到最新的Marketplace版本。
  2. 接下来,使用 vsce package 将您的扩展打包为 VSIX。
  3. 连接到codespaceDev ContainersSSH主机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,您可以使用这些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节点模块,以允许现有扩展功能。你可以使用URI调用该模块,VS Code将使得URI的默认应用程序出现在客户端。然而,这并不是一个完整的实现,因为不支持选项,并且不会返回child_process对象。

我们建议扩展程序利用vscode.env.openExternal方法来启动本地操作系统上为给定URI注册的默认应用程序,而不是依赖第三方节点模块。更好的是,vscode.env.openExternal 会自动进行本地主机端口转发! 你可以用它指向远程机器或代码空间上的本地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://code.visualstudio.com'));

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

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

转发本地主机

虽然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(`http://localhost:${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,该URI可用作身份验证提供者的回调。在Codespaces基于浏览器的编辑器中运行时,它会连接一个https://*.github.dev URI,无需任何代码更改或特殊条件。

虽然OAuth不在本文档的范围内,但请注意,如果您将此示例适配到真实的认证提供者,您可能需要在提供者前面构建一个代理服务。这是因为并非所有提供者都允许vscode://回调URI,而其他提供者不允许在HTTPS上使用通配符主机名进行回调。我们还建议尽可能使用带有PKCE流程的OAuth 2.0授权码(例如,Azure AD支持PKCE)以提高回调的安全性。

在远程运行或在Codespaces浏览器编辑器中的不同行为

在某些情况下,您的Workspace Extension可能需要在远程运行时改变行为。在其他情况下,您可能希望在基于浏览器的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扩展,要么都是工作区扩展)时这些API可以工作,但在UI扩展和工作区扩展之间这些API将无法工作。

幸运的是,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来管理扩展资源。使用这个API而不是硬编码vscode-resource:// URI是确保基于浏览器的Codespaces编辑器与你的扩展一起工作的必要条件。详情请参阅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 服务器。即使你的扩展正在运行一些你想与之交互以更新 webview 内容的本地 web 服务,你也可以从扩展本身而不是直接从你的 HTML 内容中完成此操作。

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

为什么使用消息传递而不是本地主机Web服务器?

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

以下是使用 Remote - SSH 扩展时的问题示例,但该问题也存在于 Dev Containers 和 GitHub Codespaces 中:

Webview问题

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

从webview使用localhost的解决方法

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

每个选项都允许webview内容通过VS Code用于与VS Code Server通信的相同通道进行路由。例如,如果我们更新上一节中关于Remote - SSH的图示,您将得到以下内容:

Webview 解决方案

选项 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(`http://localhost:${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="http://localhost:${LOCAL_STATIC_PORT}/canvas.png">
    </body>
    </html>`;

在这个例子中,无论是在远程还是本地情况下,任何对http://localhost:3000的请求都会自动映射到Express.js网络服务器正在运行的动态端口上。

使用原生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中的libc实现(musl)与其他发行版(glibc)之间的根本差异,包含的本机代码或运行时可能无法工作。

要解决这个问题:

  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.js fs模块。

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

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

尽量避免这些情况。

已知问题

有一些扩展问题可以通过为工作区扩展添加一些功能来解决。下表是正在考虑的已知问题列表:

Problem Description
Cannot access attached devices from Workspace extension Extensions that access locally attached devices will be unable to connect to them when running remotely. One approach to overcome this is to create a companion UI extension whose job is to access the attached device and offers commands that the remote extension can invoke too.
Another approach is reverse tunneling, which is being tracked in a VS Code repo issue.

问题和反馈