欢迎来到爱学习爱分享,在这里,你会找到许多有趣的技术 : )

VSCode 多进程架构和插件加载原理

开发者头条 369℃

前言

上周我在公司做了一个VSCode的分享,反响不是很好。内容坡度大,没有深入浅出。所以这一次我会更加详细的介绍细节,希望网上的同学看完之后有所收获。

先提出三个问题

1、为什么我打开VSCode,会打开N多个进程?

2、为什么我的全局搜索能用,但单个文件内的代码搜索却卡住了?

3、VSCode插件装多了会不会影响性能?

这些问题在本文都将得到解决。

目录

本文分为4个部分。

1、VSCode简介

2、多进程架构详解

3、最简单的插件

4、源码解析

VSCode简介

本文都是基于VSCode 1.47.3 的版本。

  • 作者,Erich Gamma,Eclipse 架构师,《设计模式》经典书籍,妥妥的业内大佬。
  • 关键词:免费、开源、轻量级、编辑器、跨平台、多语言、Git开箱即用、插件扩展。这里提一下扩平台,VSCode支持Mac、Windows、Linux、Web。
  • 体量:前端大型项目,总代码量100万,其中60万TypeScript代码(cloc工具统计)
  • 技术:Electron、TypeScript、Monaco、xTerm、LSP(Language Server Protocol)DAP(Debug Adapter Protocol)。我们将会着重介绍加粗的3个内容。

时间线

我第一次使用VSCode是2016年底,一个PHP同事和我说,VSCode写PHP真好用,随即我就被安利了。果然最好的语言就是要用最好的编辑器。

Electron

LSP

语言服务协议,编程语言需要为编辑器实现一些常用的功能,比如hover效果,代码提示(intelligence),代码诊断(diagnostics)等功能,每个编辑器都有一套自己的规则。 从图中我们可以看出,左边为编程语言,右边为编辑器。没有LSP之前,编程语言和编辑器之间是多对多的关系,这种复杂性为 n^2 。但是引入LSP之后,就变成了一对多的关系,主流编辑器都采用同一个协议规则,而编程语言只需要面向语言服务协议编写功能即可,这像不像面向接口编程。

这是一张HTML语言服务协议和PHP语言服务协议的图,PHP和HTML实现了这种服务,而客户端通过JSON RPC这种远程调用,在VSCode插件进程内初始化这些语言服务。(语言服务运行在插件进程内)。

想了解如何自定义一个语言服务,可以看一下这篇文章vscode插件快餐教程(7) – 从头开始写一个完整的lsp工程

DAP

调试适配器协议

它其实和LSP很像,所有的编程语言都公用一个调试界面,只需要实现DAP这个协议即可。

多进程架构详解

1、主进程(Main),一个 Electron 应用只有一个主进程。创建 GUI 相关的接口只由主进程来调用。

2、渲染进程(Renderer),每一个工作区(workbench)对应一个进程,同时是BrowserWindow实例。一个Electron项目可以有多个渲染进程。

3、插件进程(Extension),fork了渲染进程,每个插件都运行在一个NodeJS宿主环境中,即插件间共享进程。VSCode规定,插件不允许直接访问UI,这和Atom不同。

4、Debug进程,一个特殊的插件进程。

5、Search进程,搜索是密集型任务,单独占用一个进程。

6、进程之间通过IPC、RPC进行通信,这个后面会介绍。

7、LSP和DAP像两座桥梁,连接起语言和调试服务,它们都运行在插件进程中。

因为VSCode基于Electron,Electron基于chromium,所以进程和浏览器架构十分相似。

进程间通信

IPC

electron.ipcRenderer.send(
  "sendMessageFromRendererProcesses",
  "渲染进程向主进程发送异步消息"
);

electron.ipcMain.on("sendMessageFromRendererProcesses", (event, message) => {
  event.sender.send("sendMessageFromMainProcesses", "回应异步消息:" + message);
});

VSCode的IPC通信是基于Electron,进程间可以双向通信,并且支持同步异步通信。

RPC

const { BrowserWindow } = require("electron").remote;
let win = new BrowserWindow({ width: 800, height: 600 });
win.loadURL("https://github.com");

这里是渲染进程直接调用Electron的远程模块,重新初始化一个界面BrowserWindow,并且打开一个页面,地址为https://github.com。RPC一般用于单向调用,如渲染进程调用主进程。

小结

1、多进程架构,实现了视图与逻辑分离。

2、基于接口编程(LSP、DAP),规范了扩展功能。

3、插件进程,单独开启一个进程。不影响启动速度,不影响主进程和渲染进程,不能直接改变UI样式。缺点,UI可扩展性差,优点,带来了统一的视觉效果和交互风格。

最简单的插件

下面是官方的一个搭建插件的教程

Your First Extension

npm install -g yo generate-code
yo code
code ./helloworld

然后我们就生成了一个VSCode插件,目录如下,我和一个普通的前端项目没啥区别。我们只需要关心package.json和extension.ts。

package.json

{
  "engines": {
    "vscode": "^1.47.0"
  }
  "activationEvents": [
    "onLanguage:java",
    "onCommand:java.show.references",
    "onCommand:java.show.implementations",
    "onCommand:java.open.output",
    "onCommand:java.open.serverLog",
    "onCommand:java.execute.workspaceCommand",
    "onCommand:java.projectConfiguration.update",
    "workspaceContains:pom.xml",
    "workspaceContains:build.gradle"
  ]
}

有两个关键字,engines指VSCode兼容版本,activationEvents表示触发事件。onLanguage为语言为java时,输入命令onCommand:java.show.references(通过cmd + p可进入输入命令界面),或者工作区中包含pom.xml文件,这些都会加载插件。插件的加载机制是懒加载,只有触发了指定事件才会加载。

extension.ts

extension里导出一个activate函数,表示当插件被激活时执行函数内的内容。Demo里注册了一个命令到VSCode的context上下文中,且当执行hellworld这个命令时,会弹出一个提示语,我们将提示语由Hello World 改为了 Hello VS Code。

VSCode能自动实现插件项目,我们按F5即可进入调试模式,下面是一个输出提示语的视频。

源码解析

源码解析分为4大块。

  1. 目录结构
  2. 源码调试
  3. 工作台(WorkBench)加载
  4. 插件(Extension)加载

目录结构

上图分为上下两块内容,上面是VSCode外层的目录结构。下面为VSCode内部组织代码的规则,以base目录为例,它包含了个模块,vs目录下的其他模块,code、editor也是按照这个规则。

源码调试

项目的搭建比较简单,可以直接看官方的教程,How to Contribute。Mac的话主要一下Python版本和NodeJS脚本。

web版启动

yarn web

桌面版启动

./scripts/code.sh

本文讲的内容都是桌面版,启动完成之后,我们可以看到VSCode给我们提供的源码调试工具,OSS。

调试模式

调试模式和桌面启动有所不同,我们直接在VSCode里打开源码项目,进入调试面板,先Launch VS Code,然后就可以选择是调试主进程、渲染进程还是插件进程。

查看所有进程

ps aux|grep "OSS Helper"

启动完成之后,通过命令行查看进程情况,上面我截取了插件进程相关的信息。如果是源码情况下,关键词就是OSS Helper。我们正常使用VSCode,就可以用关键词Code Helper查看进程相关情况。

进程类型介绍

这是VSCode进程的类型,–type就是VSCode启动进程时识别进程类型的标识。有渲染进程,插件进程,GPU进程,可关闭,Watcher进程,和Webpack的Watch有些相似,都是监控文件变化的,搜索进程。插件是由渲染进程fork出来的,且一般情况插件共享一个进程,Debug进程比较特殊,它单独占用一个进程。

源码之加载工作台

// src/main.js
// 获取缓存文件目录地址和语言配置,用AMD Loader加载真正主入口
const { app, protocol } = require("electron");
app.once("ready", function () {
  onReady();
});

async function onReady() {
 const [cachedDataDir, nlsConfig] = await Promise.all([
    nodeCachedDataDir.ensureExists(),
    resolveNlsConfiguration(),
  ]);
  startup(cachedDataDir, nlsConfig);
}

function startup(cachedDataDir, nlsConfig) {
 require("./bootstrap-amd").load("vs/code/electron-main/main");
}
// src/vs/code/electron-main/main.ts
// 创建服务,初始化编辑器实例
const code = new CodeMain();
code.main();

class CodeMain {
  main() {
    this.startUp(args);
  }

  private async startup(args: ParsedArgs): Promise<void> {
   const [instantiationService, instanceEnvironment, environmentService] = this.createServices(args, bufferLogService);
   return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
  }
}

VSCode初始化实例的方式比较特殊,采用的是依赖注入的模式,关于VSCod依赖注入的文章,可以看腾讯同学写的文章,vscode 源码解析 – 依赖注入

// 打开一个窗口

// src/vs/code/electron-main/app.ts
class CodeApplication extends Disposable {
  async startup(): Promise<void> {
    const appInstantiationService = await this.createServices(machineId, sharedProcess, sharedProcessReady);

    const windows = appInstantiationService.invokeFunction((accessor) =>
      this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient)
    );
  }

  private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>): ICodeWindow[] {
    const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
    return windowsMainService.open({
      context: OpenContext.API,
      cli: { ...environmentService.args },
      forceEmpty: true,
      gotoLineMode: true
    });
  }
}

// 打开浏览器窗口,并加载配置
// src/vs/platform/windows/electron-main/windowsMainService.ts

export class WindowsMainService extends Disposable implements IWindowsMainService {
  open() {
    this.doOpen();
  }
  private doOpen() {
    this.openInBrowserWindow();
  }
  private openInBrowserWindow() {
    const createdWindow = (window = this.instantiationService.createInstance(
      CodeWindow,
      {
        state,
        extensionDevelopmentPath: configuration.extensionDevelopmentPath,
        isExtensionTestHost: !!configuration.extensionTestsPath,
      }
    ));
  }
  private doOpenInBrowserWindow() {
    window.load(configuration); // 加载页面
  }
}


// src/vs/code/electron-main/window.ts
// this._win 为 BrowserWindow 对象,是electron一个模块

class CodeWindow extends Disposable {
  load() {
    this._win.loadURL(this.getUrl(configuration));
  }

  private getUrl() {
    let configUrl = this.doGetUrl(config);
    return configUrl;
  }

  private doGetUrl(config: object): string {
    // 打开 VSCode 的工作台,也就是 workbench
    return `${require.toUrl(
      "vs/code/electron-browser/workbench/workbench.html"
    )}?config=${encodeURIComponent(JSON.stringify(config))}`;
  }
}

src/vs/code/electron-browser/workbench/workbench.html

<!DOCTYPE html>
<html>
  <body aria-label=""></body>

  <!-- Init Bootstrap Helpers -->
  <script src="../../../../bootstrap.js"></script>
  <script src="../../../../vs/loader.js"></script>
  <script src="../../../../bootstrap-window.js"></script>

  <!-- Startup via workbench.js -->
  <script src="workbench.js"></script>
</html>

源码之插件加载

src/vs/code/electron-browser/workbench/workbench.js
加载桌面插件,加载插件服务

bootstrapWindow.load(
  [
    "vs/workbench/workbench.desktop.main",
    "vs/nls!vs/workbench/workbench.desktop.main",
    "vs/css!vs/workbench/workbench.desktop.main",
  ],
  (workbench, configuration) => {
    // …
  }
);

src/vs/workbench/workbench.desktop.main.ts

import "vs/workbench/services/extensions/electron-browser/extensionService";

src/vs/workbench/services/extensions/electron-browser/extensionService.ts
监听生命周期钩子,实例化 ExtensionHostManager

class ExtensionService extends AbstractExtensionService implements IExtensionService {
  constructor() {
    this._lifecycleService.when(LifecyclePhase.Ready).then(() => {
        // reschedule to ensure this runs after restoring viewlets, panels, and editors
        runWhenIdle(() => {
          this._initialize();
        }, 50 /*max delay*/);
      });
  }

  protected async _initialize(): Promise<void> {
    this._startExtensionHosts(true, []);
  }

  private _startExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): void {
      // extensionHosts 为LocalProcessExtensionHost、RemoteExtensionHost、WebWorkerExtensionHost。
      const extensionHosts = this._createExtensionHosts(isInitialStart);
      extensionHosts.forEach((extensionHost) => {
        const processManager = this._instantiationService.createInstance(ExtensionHostManager, extensionHost, initialActivationEvents);
      });
  }
}

src/vs/workbench/services/extensions/common/extensionHostManager.ts
fork渲染进程,并加载 extensionHostProcess

class ExtensionHostManager extends Disposable {
  constructor() {
      this._proxy = this._extensionHost.start()!.then();
  }
}

src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts

class LocalProcessExtensionHost implements IExtensionHost {
  public start(): Promise<IMessagePassingProtocol> | null {
    // ...
    const opts = {
      env: objects.mixin(objects.deepClone(process.env), {
        // 加载插件进程
        AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess',
      }),
    }

    // Run Extension Host as fork of current process
    this._extensionHostProcess = fork(getPathFromAmdModule(require, 'bootstrap-fork'), ['--type=extensionHost'], opts);
  }
}

src/vs/workbench/services/extensions/node/extensionHostProcess.ts
插件进程的入口,同时开启插件激活逻辑

import { startExtensionHostProcess } from "vs/workbench/services/extensions/node/extensionHostProcessSetup";
startExtensionHostProcess().catch((err) => console.log(err));

src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts

export async function startExtensionHostProcess(): Promise<void> {
    const extensionHostMain = new ExtensionHostMain(
        renderer.protocol,
        initData,
        hostUtils,
        uriTransformer
    );
}

src/vs/workbench/services/extensions/common/extensionHostMain.ts

export class ExtensionHostMain {
  constructor() {
    // must call initialize *after* creating the extension service
        // because `initialize` itself creates instances that depend on it
        this._extensionService = instaService.invokeFunction(accessor => accessor.get(IExtHostExtensionService));
        this._extensionService.initialize();
  }
}

src/vs/workbench/api/node/extHost.services.ts

import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';

// 注册插件服务
registerSingleton(IExtHostExtensionService, ExtHostExtensionService);

src/vs/workbench/api/node/extHostExtensionService.ts
继承了抽象类,AbstractExtHostExtensionService

export class ExtHostExtensionService extends AbstractExtHostExtensionService {

}

src/vs/workbench/api/common/extHostExtensionService.ts

abstract class AbstractExtHostExtensionService extends Disposable {
  constructor() {
    this._activator = new ExtensionsActivator();
  }

  // 根据activationEvent事件名激活插件
  private _activateByEvent(activationEvent: string, startup: boolean): Promise<void> {
    return this._activator.activateByEvent(activationEvent, startup);
  }
}

整体的流程图如下

总结

1、VSCode使用了哪些技术,TS、Electron、Monaco、LSP、DAP。

2、多进程架构(记住图即可)。

3、从一个Hello World插件入手。

4、源码解析,从加载工作区,到开启插件进程,最后激活插件。

参考文献

转载请注明:爱学习爱分享 » VSCode 多进程架构和插件加载原理

喜欢 (0)or分享 (0)