支持远程开发

VS Code 远程开发允许你无缝在远程机器上开发代码。有了这项支持后,你就可以完全使用VS Code本地插件和你熟悉的方式远程工作了。

本节会介绍远程开发相关的知识,VS Code远程开发 插件架构,在远程目录测试插件,和远程插件不能正常工作的一些建议。大部分插件不需要改动就能适应远程开发环境,其他的插件也只要稍微改动一点就能适配远程开发了。

架构和插件类型


为了使远程开发尽力透明化,便于理解,我们可以将插件分为两类:

  • UI 插件: 这些插件可以配置VS Code的用户界面,而且只运行在用户的本地机器上。UI插件不能直接访问工作区的文件,或者在工作区的机器上运行脚本/工具。这类插件如:主题、代码片段、语言语法、快捷键映射。
  • 工作区插件: 这类插件运行在工作区所在机器上。当运行在本地时,工作区插件运行在本地机器上;在远程项目中,工作区插件运行在远程机器上。工作区插件可以访问工作区的文件并提供富文本支持和多语言服务器,调试和其他复杂的操作(也包含被脚本/工具调起的文件)。不过工作区插件在自定义UI上稍有限制,你可以配置的UI组件有资源管理器,视图容器等其他UI组件。

当一个用户安装了一个插件,VS Code会基于插件类型自动将插件安装到正确的环境中去:UI 插件运行在VS Code的本地插件主机中,工作区插件则运行在一个非常小的VS Code 服务器远程插件主机中。当你打开一个Windows Subsystem for Linux(WSL)、容器、或者远程SSH主机时这个服务会自动安装(更新)。VS Code还会自动管理这个服务的启停,所以用户根本意识不到它的存在。

architecture

VS Code API 会自动运行在正确的机器上(不管是本地还是远程)。但是如果你的插件使用的api不是VS Code提供的——比如运行shell脚本的Node API——当运行在远程时可能不会正常工作,因此我们建议你在所有的环境中测试一下你的插件。

测试和调试插件


这个部分将说明如何在远程目录下测试和调试插件。在这之前,我们先看一下怎么使用本地开发容器测试一个插件。本地测试容器是跨平台的,很容易部署,但是限制了访问文件系统的端口。由于只占用了非常小的OS空间,开发容器可以提供最为接近插件的真实运行环境。WSL,换句话说,就是一个典型的最小自治SSH主机。大部分场景下,你只要做小小的调整就可以解决问题了,相关主题查看常见问题

安装开发版插件

目前,VS Code自动在SSH主机、容器、WSL安装插件时会使用插件市场的版本(而不是你本机上当前安装的版本)。大部分时候这么做事合理的,但是我们现在可能需要一个未发布的版本来测试,所以你可以将插件打包成VSIX格式,然后打开已经连接到远程VS Code窗口中手动安装这个插件。

遵循以下步骤:

  1. 如果这个插件已经发布,你需要将配置文件setting.json设置成"extensions.autoUpdate": false,阻止插件自动更新到插件市场的最新版本。
  2. 下一步,使用vsce package将你的插件打包成VSIX
  3. 连接到开发容器SSH主机WSL环境
  4. 在你已经连接远程目录的项目中,使用命令 Install from VSIX…安装你打包好的插件
  5. 完成后重启

?> 小提示:安装完毕后,你可以使用 Developer: Show Running Extensions命令查看VS Code在本地运行插件还是在远程运行插件的。

在远程调试你的插件

通常,你在本地机器上构建、编辑、加载和调试你的插件,远程调试插件也差不多遵循相同的模式,你只要把这些工作全部放在打开的远程开发目录去做就好了。

使用开发容器

遵循以下步骤开发和调试容器中的插件。

  1. 添加Node.js开发容器定义。在你的插件文件夹下按F1,选择 Remote-Containers: Create Configuration File… 命令,然后选择 Node.js 8 & Typescript(或者只是 Node.js 8)。这就定义了你将会编辑和调试的插件容器。
  2. 命令运行后,你可以自由修改.devcontainer文件夹,添加比如构建或运行时的其他选项。深入容器了解更多内容。
  3. 【可选】 编辑launch.jsonargs属性后面添加第二个参数,指向你的容器中的工作区目录下的测试项目/测试数据。比如,如果你的测试数据在工作区的一个data文件夹下,需要按照如下步骤添加${workspaceFolder}/data:

注意:你不可以单独使用${workspaceFolder}作为第二个参数

  1. {
  2. "name": "Launch Extension",
  3. "type": "extensionHost",
  4. "request": "launch",
  5. "runtimeExecutable": "${execPath}",
  6. "args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/data"],
  7. "stopOnEntry": false,
  8. "sourceMaps": true,
  9. "outFiles": ["${workspaceFolder}/dist/**/*.js"],
  10. "preLaunchTask": "npm"
  11. }
  1. 运行 Remote-Containers: Reopen Folder in Container,VS Code会部署好容器,然后建立连接。现在你可以从容器内部修改源码了。
  2. 最后,按下F5或者使用 调试视图从容器内部加载然后启动调试器。
使用SSH 或 WSL

你在 SSH 主机WSL 中遵循类似的步骤。

  1. 使用SSH,你需要在远程主机上打开对应的项目(比如,使用 Remote-SSH: Connect to Host… 命令,然后在 File > Open打开对应的插件副本)。使用WSL,使用 File > New WSL Window然后 File > Open打开对应文件夹。
  2. 通过SSH主机/WSL打开文件夹后,你可以像在本地一样编辑源码了。
  3. 最后,按下 F5 或者使用 调试视图加载插件,像本地一样调试代码。

常见问题


VS Code API 会根据项目自动运行在正确的环境上。记住这点,然后我们来看看几个API,它会帮助你避免一些意外问题。

不正确的执行环境

如果你的插件出错了,它有可能是运行在了错误环境中。比较常见的场景是,你原本期望它运行在本地却运行在了远程上。你可以使用命令面板的 Developer: Show Running Extensions命令查看插件的运行情况。

如果这个命令显示某个UI 插件被当做工作区插件或者类似的情况,你可以试着在插件的package.json中设置extensionKind属性:

  1. {
  2. "extensionKind": "ui"
  3. }
  • "extensionKind": "ui" —— 将插件视为 UI 插件,强制在本地运行。
  • "extensionKind": "ui" —— 将插件视为 工作区插件,它有可能会在远程工作区的VS Code Server中运行。

你也可以在设置中修改 remote.extensionKind插件的类型,这项配置可以立竿见影地看到效果。比如,你想把 Azure Cosmos DB插件强制设置为 UI 插件(默认是工作区插件)然后把Debugger for Chrome设置为工作区插件(默认是UI 插件),你可以这么设置:

  1. {
  2. "remote.extensionKind": {
  3. "ms-azuretools.vscode-cosmosdb": "ui",
  4. "msjsdiag.debugger-for-chrome": "workspace"
  5. }
  6. }

使用 remote.extensionKind可以快速地测试发行版插件,而不用修改插件的package.json或重新构建插件。

保存插件数据或状态

有的时候,你的插件需要保留住不属于settings.json的数据或者独立的工作区配置文件(如.eslintrc),你可以使用插件激活入口的vscode.ExtensionContext对象。如果你的插件已经使用好了这些属性那就应该不会出错。

但是,你的插件如果依赖VS Code的路径约定(如 ~/.vscode)或者特定的OS目录(比如Linux上的 ~/.config/Code)来保存数据,那你就可能会遇到问题。不过还好这些小问题只要稍加修改插件就能处理掉。

如果你只是想保存一些键值对全局状态信息,你可以使用vscode.ExtensionContext.workspaceStatevscode.ExtensionContext.globalState。如果数据不止键值对且更为复杂,访问globalStoragePathstoragePath可以安全地获取对应的储存路径,供你读写文件。

这些API在1.31版本之后可用,你需要在package.json中配置版本:

  1. {
  2. "engines": {
  3. "vscode": "^1.31.0"
  4. }
  5. }

如何使用我们上面介绍的API:

  1. import * as vscode from 'vscode';
  2. import * as fs from 'fs';
  3. import * as path from 'path';
  4. export function activate(context: vscode.ExtensionContext) {
  5. context.subscriptions.push(
  6. vscode.commands.registerCommand('myAmazingExtension.persistWorkspaceData', () => {
  7. // 如果插件所属的工作区不存在储存路径,则创建一个
  8. if (!fs.existsSync(context.storagePath)) {
  9. fs.mkdirSync(context.storagePath);
  10. }
  11. // 将文件写入储存目录
  12. fs.writeFileSync(
  13. path.join(context.storagePath, 'workspace-data.json'),
  14. JSON.stringify({ now: Date.now() }));
  15. }));
  16. context.subscriptions.push(
  17. vscode.commands.registerCommand('myAmazingExtension.persistGlobalData', () => {
  18. // 如果插件所属的全局(跨工作区)文件夹不存在则创建一个
  19. if (!fs.existsSync(context.globalStoragePath)) {
  20. fs.mkdirSync(context.globalStoragePath);
  21. }
  22. // 为插件创建一个全局储存文件
  23. fs.writeFileSync(
  24. path.join(context.globalStoragePath, 'global-data.json'),
  25. JSON.stringify({ now: Date.now() }));
  26. }));
  27. }

保留密钥

如果你的插件需要保留密码或者其他密钥,你可能会想到使用本地操作系统的密钥储存功能(Windows Cert Store、 macOS KeyChain、Linux 的 libsecret-based keyring),而不是使用远程主机的储存功能。更有可能的是,你可能需要在Linux上使用libsecret,在插件中使用gnome-keyring去储存你的密钥,通常来说,这项功能在服务端或者容器内不一定能运作起来。

VS Code本身不提供密钥储存机制,不过需要插件作者会转而使用keytar node module包。因此,VS Code内建了keytar,你在 工作区插件 中如果使用了它,它就会自动无声地运行在后台。这样一来你就可以使用本地系统的 keychain/ keyring/ cert store 同时还避免了各种问题。

比如:

  1. import * as vscode from 'vscode';
  2. function getCoreNodeModule(moduleName) {
  3. try {
  4. return require(`${vscode.env.appRoot}/node_modules.asar/${moduleName}`);
  5. } catch (err) {}
  6. try {
  7. return require(`${vscode.env.appRoot}/node_modules/${moduleName}`);
  8. } catch (err) {}
  9. return undefined;
  10. }
  11. // Use it
  12. const keytar = getCoreNodeModule('keytar');
  13. await keytar.setPassword('my-service-name', 'my-account', 'iamal337d00d');
  14. const password = await keytar.getPassword('my-service-name', 'my-account');

使用剪贴板

由于历史原因,大部分插件作者会使用诸如clipboardy等Node.js的包和剪贴板交互。不幸的的是,如果你使用了这样的包,那么它就很有可能会运行在远程机器上。

VS Code 1.30引入的剪贴板则解决了这个问题。它总是运行在本地中,所以同样的,你只要修改你的engines.vscode版本就可以使用这个API了。

在插件中使用剪贴板API:

  1. import * as vscode from 'vscode';
  2. export function activate(context: vscode.ExtensionContext) {
  3. context.subscriptions.push(
  4. vscode.commands.registerCommand('myAmazingExtension.clipboardIt', async () => {
  5. // 读取剪贴板
  6. const text = await vscode.env.clipboard.readText();
  7. // 写入剪贴板
  8. await vscode.env.clipboard.writeText(
  9. `It looks like you're copying "${text}". Would you like help?`
  10. );
  11. })
  12. );
  13. }

在本地浏览器或者其他应用中打开些什么

在本地的场景下,通过使用子进程或者opn包启动浏览器或者其他应用是完全可行的,但是一旦插件运行到了远程上,这就会导致应用加载错误。VS Code远程开发部分兼容了opn包使得现有插件可以正常运行。你可以使用URI调用这个包,VS Code会带上这个URL在客户端唤起默认应用。由于不是完整实现,有些配置是不支持的,也不会返回child_process对象。

除了依赖第三方包,我们建议你使用vscode.env.openExternal方法在本地操作系统上启动默认应用打开对应的URI。而且vscode.env.openExternal还支持自动端口转发!你可以指向到远程的web server上,即使那个端口外部不可访问。

这项功能自1.31开始支持,所以你又要修改你的engines.vscode了。

如何使用vscode.env.openExternalAPI:

  1. import * as vscode from 'vscode';
  2. export async function activate(context: vscode.ExtensionContext) {
  3. context.subscriptions.push(
  4. vscode.commands.registerCommand('myAmazingExtension.openExternal', () => {
  5. // 示例 1 - 在默认浏览器中打开VS Code主页
  6. vscode.env.openExternal(vscode.Uri.parse('https://code.visualstudio.com'));
  7. // 示例 2 - 打开默认email应用
  8. vscode.env.openExternal(vscode.Uri.parse('mailto:vscode@microsoft.com'));
  9. })
  10. );
  11. }

使用命令在插件间通信

一些插件意在被其他插件调用(通过vscode.extension.getExtension(extensionName).exports),因此会在激活时返回一些API。如果这些插件都在同一环境(都是UI插件,或者都是工作区插件时)下工作不会出什么问题,但是如果一个是UI插件,一个是工作区插件就会出问题了。

如果你的插件需要彼此产生交互,使用私有命令暴露功能可以避免一些问题。不过记住一点,所有你传入的对象参数都会被字符串化(JSON.stringify),因此这些对象不可以有循环引用,这会导致接受方只收到一个”字符串化的object类型”。

示例:

  1. import * as vscode from 'vscode';
  2. export async function activate(context: vscode.ExtensionContext) {
  3. // 注册私有命令
  4. const echoCommand = vscode.commands.registerCommand(
  5. '_private.command.called.echo',
  6. (value: string) => {
  7. return value;
  8. }
  9. );
  10. context.subscriptions.push(echoCommand);
  11. }

使用命令的更多细节,请参考命令API指南

使用Webview API


就像剪贴板API,Webview API也总是运行在本地环境,即使是 工作区插件 调用的。也就是说大部分基于webview的插件都可以正常工作,但是还有些注意事项需要交代一下。

访问localhost

默认情况下,webview中的localhost会解析到用户本地机器上,也就是说,远程运行的插件所启动的服务在webview里是无法访问的。即使你访问远程机器的ip,云主机或容器中的端口还是默认被拦截的。我们来看一下示意图

webview to remote

你可以用webview的message passingAPI绕过各种限制,你还可以 添加端口映射 告诉webview哪些端口可以直接转发到远程机器上。

端口映射可以将webview所使用的localhost端口映射到远程插件启动的任意端口上。如果你的 工作区插件 运行在远程,而且你也定义了端口映射,那么这些流量会自动、安全地转发到远程机器上。如果你的插件只是运行在本地,端口映射则会将一个localhost端口重新映射到另一个端口上。Webview端口映射同时支持UI插件工作区插件,也同时支持本地和远程环境。

这项功能自1.34开始支持,请修改你的package.json添加该支持。

使用端口映射,只要在你创建的webview中添加传入一个portMapping对象即可:

  1. const STATIC_PORT = 3000;
  2. const dynamicServerPort = getExpressServerPort();
  3. const webviewPort = STATIC_PORT;
  4. // 创建webview然后传入portMapping
  5. const panel = vscode.window.createWebviewPanel(
  6. 'remoteMappingExample',
  7. 'Remote Mapping Example',
  8. vscode.ViewColumn.One,
  9. {
  10. portMapping: [
  11. // 这里映射了 webview 的 localhost:3000 到远程主机的 express 服务器端口
  12. { webviewPort: webviewPort, extensionHostPort: dynamicServerPort }
  13. ]
  14. }
  15. );
  16. // 你可以在HTML中使用"webviewPort"查看端口
  17. panel.webview.html = `<!DOCTYPE html>
  18. <body>
  19. <!-- This will resolve to the dynamic server port on the remote machine -->
  20. <img src="http://localhost:${webviewPort}/canvas.png">
  21. </body>
  22. </html>`;

现在webview前往localhost:3000的流量都会通过VS Code的安全信道直接走到远程机器上。

Access to remote

使用原生Node.js模块


和插件打包(或动态引入的包)的原生node包会被Electorn的electron-rebuild重新编译。但是VS Code Server运行在一个标准的(非Electron)的Node.js中,因此可能造成远程二进制库失效问题。

解决这个问题需要:

  1. 同时引入Node.js和Elctron标准的两种二进制包(别忘了动态引入的包)。
  2. 检查vscode.extensions.getExtension('your.extensionId').extensionKind === vscode.ExtensionKind.Workspace是否根据环境使用了正确的包。
  3. 如果你想支持非x86_64构建目标和Alpine Linux则遵循下述类似逻辑

使用VS Code的 Help > Developer Tools然后在控制台(console)中打印process.versions.modules可以找到VS Code使用的模块(modules)类型。如果你想要确保原生模块在各个Node.js环境中都能无缝运行,你可能把所有可能支持的平台(Electron Node.js, 官方Node.js Windows/Darwin/Linux的全部版本)相关的包全部引入。node-tree-sitter包在这方面是个非常好的例子。

为非x86_64主机或Apline Linux容器提供支持


如果你的插件只是用JavasSript/TypeScript写的,你的插件可能什么都不用做就能支持其他进程架构或基于musl的Apline Linux。

但是如果你的插件可以运行在Debian 9+, Ubuntu 16.04+, 或者基于 RHEL / CentOS 7+ 的远程SSH主机、容器或 WSL上,却无法支持非x86_64(比如 ARMv7l)或Alpine Linux容器,插件可能需要包含了x86_64的glibc特定机器码或运行时,所以导致了这些架构/操作系统上出现问题。

比如,你的插件坑包含了x86_64编译的原生模块或运行时版本。对于Alpine Linux来说就是因基础架构差异而无法运行这样的插件。

为了解决这个问题:

  1. 如果你是动态引入编译码,你可以用process.arch检测环境,然后根据对应架构下载对应架构下的依赖。如果你已经在插件中直接使用了二进制包,你也可以用同样的逻辑使用正确的包。
  2. 对于Alpine Linux来说,你可以用await fs.exists('/etc/alpine-release')检测操作系统,然后下载或者使用基于musl的正确的二进制包。
  3. 如果你不想支持这些平台,你也可以用同样的逻辑提供良好的错误提示。

你要非常注意一些第三方包可能依赖了导致这个问题的源码包。所以有时候你需要联系npm包作者提供额外的编译版本。

避免使用Electron模块


虽然依赖未暴露的内置Electron或者VS Code模块非常方便,但是你必须知道VS Code Server运作在标准的(非Electron)Node.js环境中,当插件运行在远程时就会丢失这些包。除了少数个例,比如keytar,用了特殊的实现所以在所有环境中都能正常工作。

使用基于Node.js的模块从而避免这些问题。如果你一定要用Electron模块,那就要确保如果包丢失的时候提供合适的后备方案。

下面的例子使用了Electron的original-fs包,缺失时则使用Node.js的fs模块。

  1. function requireWithFallback(electronModule: string, nodeModule: string) {
  2. try {
  3. return require(electronModule);
  4. } catch (err) {}
  5. return require(nodeModule);
  6. }
  7. const fs = requireWithFallback('original-fs', 'fs');

但是不论何时,你都应该避免这些问题。

已知问题


目前我们还有些影响 工作区插件 功能的问题亟待解决。下表是已知的问题列表:

问题 描述
端口拦截 当使用Docker容器或者SSH服务器开发时端口不会自动转发,而插件中也没有可以程序性转发端口的API。虽然在使用Webview API中已经适配了webview,但是其他场景下还需要插件作者手动暴露端口
无法访问/传输远程工作区文件到本地 插件通过外部应用打开工作区文件会遇到错误,因为外部应用无法直接访问远程文件。我们正在调查插件怎样才能从远程工作区传输文件的相关方案。
无法从工作区插件访问关联设备 关联本地设备的插件无法在远程运行时关联对应设备,我们正在调查相关问题的最佳方案。

FAQ