VSCode多进程架构和插件加载原理
前言
上周我在公司做了一个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可扩展性差,优点,带来了统一的视觉效果和交互风格。
最简单的插件
下面是官方的一个搭建插件的教程
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大块。
- 目录结构
- 源码调试
- 工作台(WorkBench)加载
- 插件(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>
<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();