支持远程开发和 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)中选择 Dev Containers: 添加 Dev Container 配置文件... 或 Codespaces: 添加 Dev Container 配置文件...,然后选择 Node.js & TypeScript(如果您不使用 TypeScript,则选择 Node.js)以添加所需的容器配置文件。
-
可选:此命令运行后,您可以修改
.devcontainer
文件夹的内容,以包含额外的构建或运行时要求。有关详细信息,请参阅深入的创建开发容器文档。 -
运行 Dev Containers: 在容器中重新打开 或 Codespaces: 添加 Dev Container 配置文件...,片刻之后,VS Code 将设置容器并连接。现在您可以在容器内部开发您的源代码,就像在本地一样。
-
在新的 VS Code 终端窗口中运行
yarn install
或npm install
(⌃⇧` (Windows, Linux Ctrl+Shift+`)),以确保安装了 Linux 版本的 Node.js 本机依赖项。您还可以安装其他操作系统或运行时依赖项,但您可能也希望将这些依赖项添加到.devcontainer/Dockerfile
中,以便在重新构建容器时它们可用。 -
最后,按 F5 或使用运行和调试视图在此同一容器内启动扩展并附加调试器。
注意:您将无法在出现的窗口中打开扩展源代码文件夹,但可以打开子文件夹或容器中的其他位置。
出现的扩展开发主机窗口将包含您的扩展,它在您在步骤 2 中定义的容器中运行,并且调试器已附加到它。
使用 SSH 调试
请按以下步骤操作
-
安装并配置远程 - 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);
}
使用剪贴板
历史上,扩展作者使用 Node.js 模块(例如 clipboardy
)与剪贴板进行交互。不幸的是,如果您在工作区扩展中使用这些模块,它们将使用远程剪贴板而不是用户的本地剪贴板。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
对象。
我们建议扩展不依赖第三方 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.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 处理程序的常见用例是实现使用 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,可用作身份验证提供程序的回调。在 Codespaces 基于浏览器的编辑器中运行时,它会连接一个 https://*.github.dev
URI,无需任何代码更改或特殊条件。
虽然 OAuth 超出本文档的范围,但请注意,如果您将此示例适配到真实的身份验证提供程序,您可能需要在提供程序前面构建一个代理服务。这是因为并非所有提供程序都允许 vscode://
回调 URI,而其他提供程序则不允许通过 HTTPS 进行回调的通配符主机名。我们还建议尽可能使用 OAuth 2.0 授权码与 PKCE 流程(例如,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 扩展或都是工作区扩展)时这些方法将起作用,但它们在 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,允许您动态更新 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 基于浏览器的编辑器中也无法工作。
以下是使用远程 - SSH 扩展时问题的说明,但该问题也存在于开发容器和 GitHub Codespaces 中
如果可能,您应该避免这样做,因为它会极大地使您的扩展复杂化。消息传递 API 可以在不带来这些麻烦的情况下实现相同类型的用户体验。扩展本身将在远程端的 VS Code Server 中运行,因此它可以透明地与您的扩展因从 Webview 传递的任何消息而启动的任何 Web 服务器进行交互。
从 Webview 使用 localhost 的解决方法
如果您出于某种原因无法使用消息传递 API,则有两种方法可以在 VS Code 中与远程开发和 GitHub Codespaces 扩展一起使用。
每个选项都允许 Webview 内容通过 VS Code 用于与 VS Code Server 通信的相同通道进行路由。例如,如果我们将上一节中关于远程 - 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(`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,这可能导致二进制文件在远程使用时失败。
为解决这个问题
- 为 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
) 中 libc
的实现方式与其他发行版 (glibc
) 之间存在根本差异,包含的原生代码或运行时可能无法工作。
为解决此问题
-
如果您正在动态获取编译代码,可以通过使用
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.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 仓库问题中进行跟踪。 |