支持远程开发和 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 还会自动管理服务器的启动和停止,因此用户不会感知到它的存在。)
VS Code API 被设计为在从 UI 扩展或工作区扩展调用时,会自动在正确的机器上(本地或远程)运行。然而,如果你的扩展使用了非 VS Code 提供的 API——例如使用 Node API 或运行 shell 脚本——它在远程运行时可能无法正常工作。我们建议你测试扩展的所有功能在本地和远程工作区中都能正常工作。
调试扩展
虽然你可以在远程环境中安装扩展的开发版本进行测试,但如果遇到问题,你可能希望直接在远程环境中调试扩展。在本节中,我们将介绍如何在 GitHub Codespaces、本地容器、SSH 主机或 WSL 中编辑、启动和调试扩展。
通常,你的最佳测试起点是使用限制端口访问的远程环境(例如 Codespaces、容器或具有限制性防火墙的远程 SSH 主机),因为在这些环境中工作的扩展通常在限制性较小的环境中(如 WSL)也能工作。
使用 GitHub Codespaces 进行调试
在 GitHub Codespaces 预览版中调试你的扩展是一个很好的起点,因为你可以使用 VS Code 和 Codespaces 基于浏览器的编辑器进行测试和故障排除。如果愿意,你也可以使用自定义开发容器。
请遵循以下步骤:
-
导航到 GitHub 上包含你的扩展的仓库,然后在 codespace 中打开它,以便在基于浏览器的编辑器中进行操作。如果你更喜欢,也可以在 VS Code 中打开 codespace。
-
尽管 GitHub Codespaces 的默认镜像应该包含大多数扩展所需的所有先决条件,你仍然可以在新的 VS Code 终端窗口中安装任何其他所需的依赖项(例如,使用 `yarn install` 或 `sudo apt-get`)(⌃⇧` (Windows, Linux Ctrl+Shift+`))。
-
最后,按 F5 或使用**运行和调试**视图在 codespace 内启动扩展。
**注意:** 你将无法在出现的窗口中打开扩展源代码文件夹,但你可以在 codespace 中打开一个子文件夹或在其他位置打开。
出现的扩展开发主机窗口将包含在 codespace 中运行的你的扩展,并附有调试器。
在自定义开发容器中调试
请遵循以下步骤:
-
若要在本地使用开发容器,请安装并配置 Dev Containers 扩展,然后使用**文件 > 打开... / 打开文件夹...** 在 VS Code 中本地打开你的源代码。若要改用 Codespaces,请导航到 GitHub 上包含你的扩展的仓库,然后在 codespace 中打开它,以便在基于浏览器的编辑器中进行操作。如果你更喜欢,也可以在 VS Code 中打开 codespace。
-
从命令面板 (F1) 中选择**开发容器: 添加开发容器配置文件...** 或**Codespaces: 添加开发容器配置文件...**,然后选择 **Node.js & TypeScript**(如果你不使用 TypeScript,则选择 Node.js)来添加所需的容器配置文件。
-
**可选:** 此命令运行后,你可以修改 `.devcontainer` 文件夹的内容,以包含额外的构建或运行时要求。有关详细信息,请参阅深入的创建开发容器文档。
-
运行**开发容器: 在容器中重新打开**或**Codespaces: 添加开发容器配置文件...**,稍后,VS Code 将设置容器并连接。你现在将能够像在本地情况下一样在容器内部开发你的源代码。
-
在新的 VS Code 终端窗口中运行 `yarn install` 或 `npm install` (⌃⇧` (Windows, Linux Ctrl+Shift+`)),以确保安装了 Linux 版本的 Node.js 原生依赖项。你也可以安装其他操作系统或运行时依赖项,但你可能需要将它们添加到 `.devcontainer/Dockerfile` 中,以便在重建容器时它们可用。
-
最后,按 F5 或使用**运行和调试**视图在此容器内启动扩展并附加调试器。
**注意:** 你将无法在出现的窗口中打开扩展源代码文件夹,但你可以在容器中打开一个子文件夹或在其他位置打开。
出现的扩展开发主机窗口将包含在步骤 2 中定义的容器中运行的你的扩展,并附有调试器。
使用 SSH 进行调试
请遵循以下步骤:
-
在安装和配置 Remote - SSH 扩展后,从 VS Code 的命令面板 (F1) 中选择**Remote-SSH: 连接到主机...** 以连接到主机。
-
连接后,可以使用**文件 > 打开... / 打开文件夹...** 选择包含扩展源代码的远程文件夹,或者从命令面板 (F1) 中选择**Git: 克隆**以克隆并在远程主机上打开它。
-
在新的 VS Code 终端窗口中安装任何可能缺失的所需依赖项(例如,使用 `yarn install` 或 `apt-get`)(⌃⇧` (Windows, Linux Ctrl+Shift+`))。
-
最后,按 F5 或使用**运行和调试**视图在远程主机内启动扩展并附加调试器。
**注意:** 你将无法在出现的窗口中打开扩展源代码文件夹,但你可以在 SSH 主机上打开一个子文件夹或在其他位置打开。
出现的扩展开发主机窗口将包含在 SSH 主机上运行的你的扩展,并附有调试器。
使用 WSL 进行调试
请遵循以下步骤:
-
在安装和配置 WSL 扩展后,从 VS Code 的命令面板 (F1) 中选择**WSL: 新建窗口**。
-
在出现的新窗口中,使用**文件 > 打开... / 打开文件夹...** 选择包含扩展源代码的远程文件夹,或者从命令面板 (F1) 中选择**Git: 克隆**以克隆并在 WSL 中打开它。
**提示:** 你可以选择 `/mnt/c` 文件夹来访问你在 Windows 端上的任何克隆的源代码。
-
在新的 VS Code 终端窗口中安装任何可能缺失的所需依赖项(例如使用 `apt-get`)(⌃⇧` (Windows, Linux Ctrl+Shift+`))。你至少需要运行 `yarn install` 或 `npm install` 以确保 Linux 版本的 Node.js 原生依赖项可用。
-
最后,按 F5 或使用**运行和调试**视图启动扩展并附加调试器,就像你在本地操作一样。
**注意:** 你将无法在出现的窗口中打开扩展源代码文件夹,但你可以在 WSL 中打开一个子文件夹或在其他位置打开。
出现的扩展开发主机窗口将包含在 WSL 中运行的你的扩展,并附有调试器。
安装你的扩展的开发版本
任何时候 VS Code 自动在 SSH 主机、容器或 WSL 内部,或通过 GitHub Codespaces 安装扩展时,都会使用 Marketplace 版本(而不是你本地机器上已安装的版本)。
尽管这在大多数情况下是合理的,但你可能希望使用(或分享)一个未发布的扩展版本进行测试,而无需设置调试环境。要安装扩展的未发布版本,你可以将扩展打包为 `VSIX`,然后手动将其安装到已连接到正在运行的远程环境的 VS Code 窗口中。
请遵循以下步骤:
- 如果这是一个已发布的扩展,你可能需要在 `settings.json` 中添加 `"extensions.autoUpdate": false` 以防止其自动更新到最新的 Marketplace 版本。
- 接下来,使用 `vsce package` 将你的扩展打包为 VSIX。
- 连接到 codespace、开发容器、SSH 主机或 WSL 环境。
- 使用扩展视图**更多操作**(`...`)菜单中提供的**从 VSIX 安装...** 命令,在此特定窗口(而非本地窗口)中安装扩展。
- 出现提示时重新加载。
**提示:** 安装后,你可以使用**开发者: 显示正在运行的扩展**命令查看 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 数据库 扩展为 UI 扩展(而不是其工作区默认),并将 远程 - SSH:编辑配置文件 扩展设置为工作区扩展(而不是其 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.workspaceState` 或 `vscode.ExtensionContext.globalState` 存储工作区特定或全局状态信息。如果你的数据比键值对更复杂,`globalStorageUri` 和 `storageUri` 属性提供“安全”的 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.workspaceState` 或 `vscode.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` 方法,而不是依赖第三方 Node 模块,以在本地操作系统上为给定 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.js.cn'));
// 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>'));
})
);
}
转发 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(`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.registerUriHandler` 和 `vscode.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,可用作身份验证提供程序的 callback。当在 Codespaces 基于浏览器的编辑器中运行时,它会连接一个 `https://*.github.dev` URI,无需任何代码更改或特殊条件。
尽管 OAuth 超出了本文的范围,但请注意,如果你将此示例 адаптировал 到真实的身份验证提供商,你可能需要在提供商前面构建一个代理服务。这是因为并非所有提供商都允许 `vscode://` 回调 URI,而其他提供商则不允许 HTTPS 回调的通配符主机名。我们还建议尽可能使用 带 PKCE 流程的 OAuth 2.0 授权码(例如,Azure AD 支持 PKCE)以提高回调的安全性。
远程运行或在 Codespaces 浏览器编辑器中运行时行为的差异
在某些情况下,你的工作区扩展在远程运行时可能需要改变行为。在其他情况下,你可能希望在 Codespaces 基于浏览器的编辑器中运行时改变其行为。VS Code 提供了三个 API 来检测这些情况:`vscode.env.uiKind`、`extension.extensionKind` 和 `vscode.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 扩展和工作区扩展之间将无法工作。
幸运的是,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,允许你无需使用本地 Web 服务器即可动态更新 webview 内容。即使你的扩展正在运行一些你想要交互以更新 webview 内容的本地 Web 服务,你也可以从扩展本身而不是直接从 HTML 内容中执行此操作。
这对于远程开发和 GitHub Codespaces 来说是一个重要的模式,可确保你的 webview 代码在 VS Code 和 Codespaces 基于浏览器的编辑器中都能正常工作。
为什么使用消息传递而不是 localhost Web 服务器?
另一种模式是在 `iframe` 中提供 Web 内容,或者让 webview 内容直接与 localhost 服务器交互。不幸的是,默认情况下,webview 内部的 `localhost` 将解析为开发者的本地机器。这意味着对于远程运行的工作区扩展,它创建的 webview 将无法访问扩展启动的本地服务器。即使你使用机器的 IP,你连接的端口通常在云虚拟机或容器中默认会被阻止。即使这在 VS Code 中有效,它也无法在 Codespaces 基于浏览器的编辑器中工作。
以下是使用 Remote - SSH 扩展时问题的说明,但该问题也存在于开发容器和 GitHub Codespaces 中:
如果可能,**你应该避免这样做**,因为它会显著增加你的扩展的复杂性。消息传递 API 可以实现相同类型的用户体验,而不会带来这些麻烦。扩展本身将在远程端的 VS Code Server 中运行,因此它可以透明地与你的扩展因从 webview 传递给它的任何消息而启动的任何 Web 服务器进行交互。
从 webview 使用 localhost 的变通方法
如果由于某种原因无法使用消息传递 API,则有两种选项可以在 VS Code 中与远程开发和 GitHub Codespaces 扩展一起使用。
每个选项都允许 webview 内容通过 VS Code 与 VS Code Server 通信的相同通道进行路由。例如,如果我们更新上一节中 Remote - SSH 的图示,你将看到以下内容:
选项 1 - 使用 asExternalUri
VS Code 1.40 引入了 `vscode.env.asExternalUri` API,允许扩展以编程方式远程转发本地 `http` 和 `https` 请求。当你的扩展在 VS Code 中运行时,你可以使用相同的 API 将请求从 webview 转发到 `localhost` Web 服务器。
使用该 API 获取 iframe 的完整 URI 并将其添加到你的 HTML 中。你还需要在 webview 中启用脚本并将 CSP 添加到你的 HTML 内容中。
// 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 Web 服务器运行的动态端口。
使用原生 Node.js 模块
随 VS Code 扩展捆绑(或动态获取)的原生模块必须使用 Electron 的 `electron-rebuild` 重新编译。然而,VS Code Server 运行的是标准(非 Electron)版本的 Node.js,这可能导致二进制文件在远程使用时失败。
解决此问题:
- 包含(或动态获取)VS Code 附带的 Node.js“模块”版本的两组二进制文件(Electron 和标准 Node.js)。
- 检查 `vscode.extensions.getExtension('your.extensionId').extensionKind === vscode.ExtensionKind.Workspace` 以根据扩展是远程运行还是本地运行来设置正确的二进制文件。
- 你可能还希望通过遵循类似逻辑同时添加对非 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` 的实现方式存在根本性差异。
解决此问题:
-
如果你正在动态获取编译后的代码,可以通过使用 `process.arch` 检测非 x86_64 目标并下载针对正确架构编译的版本来添加支持。如果你在扩展中包含所有支持架构的二进制文件,则可以使用此逻辑来使用正确的二进制文件。
-
对于 Alpine Linux,你可以使用 `await fs.exists('/etc/alpine-release')` 检测操作系统,然后再次下载或使用适用于基于 `musl` 操作系统的正确二进制文件。
-
如果你不想支持这些平台,你可以使用相同的逻辑来提供一个好的错误消息。
重要的是要注意,一些第三方 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 仓库问题中正在跟踪。 |