vscode 是怎么跑起来的

12,292 阅读5分钟

vscode 是前端工程师常用的 ide,而且它的实现也是基于前端技术。既然是前端技术实现的,那么我们用所掌握的前端技术,完全可以实现一个类似 vscode 的 ide。但在那之前,我们首先还是要把 vscode 是怎么实现的理清楚。本文我们就来理一下 vscode 是怎么跑起来的。

首先, vscode 是一个 electron 应用,窗口等功能的实现基于 electron,所以想梳理清楚 vscode 的启动流程,需要先了解下 electron。

electron

electron 基于 node 和 chromium 做 js 逻辑的执行和页面渲染,并且提供了 BrowserWindow 来创建窗口,提供了 electron 包的 api,来执行一些操作系统的功能,比如打开文件选择窗口、进程通信等。

每个 BrowserWindow 窗口内的 js 都跑在一个渲染进程,而 electron 有一个主进程,负责和各个窗口内的渲染进程通信。

如图所示,主进程可以使用 nodejs 的 api 和 electron 提供给主进程的 api,渲染进程可以使用浏览器的 api、nodejs 的 api 以及 electron 提供给渲染进程的 api,除此以外,渲染进程还可以使用 html 和 css 来做页面的布局。

vscode 的每个窗口就是一个 BrowserWindow,我们启动 vscode 的时候是启动的主进程,然后主进程会启动一个 BrowserWindow 来加载窗口的 html,这样就完成的 vscode 窗口的创建。(后续新建窗口也是一样的创建 BrowserWindow,只不过要由渲染进程通过 ipc 通知主进程,然后主进程再创建 BrowserWindow,不像第一次启动窗口直接主进程创建 BrowserWindow 即可。)

vsocde 窗口启动流程

我们知道 vscode 基于 electron 来跑,electron 会加载主进程的 js 文件,也就是 vscode 的 package.json 的 main 字段所声明的 ./out/main.js,我们就从这个文件开始看。

src/main

(下面的代码都是我从源码简化来的,方便大家理清思路)

// src/main.js
const { app } = require('electron');

app.once('ready', onReady);

async function onReady() {
    try {
        startup(xxxConfig);
    } catch (error) {
        console.error(error);
    }
}

function startUp() {
    require('./bootstrap-amd').load('vs/code/electron-main/main');
}

可以看到,./src/main.js 里面只是一段引导代码,在 app 的 ready 事件时开始执行入口 js。也就是 vs/code/electron-main/main,这是主进程的入口逻辑。

CodeMain

// src/vs/code/electron-main/main.ts
class CodeMain {
    main(): void {
        try {
            this.startup();
        } catch (error) {
            console.error(error.message);
            app.exit(1);
        }
    }
    private async startup(): Promise<void> {
        // 创建服务
        const [
            instantiationService, 
            instanceEnvironment,
            environmentMainService,
            configurationService,
            stateMainService
        ] = this.createServices();
        
        // 初始化服务
        await this.initServices();
        
        // 启动
        await instantiationService.invokeFunction(async accessor => {
            //创建 CodeApplication 的对象,然后调用 startup
            return instantiationService.createInstance(CodeApplication).startup();
        });

    }
}

const code = new CodeMain();
code.main();

可以看到,vscode 创建了 CodeMain 的对象,这个是入口逻辑的开始,也是最根上的一个类。它创建和初始化了一些服务,然后创建了 CodeApplication 的对象。

也许你会说,创建对象为啥不直接 new,还要调用 xxx.invokeFunction() 和 xxx.createInstance() 呢?

这是因为 vscode 实现了 ioc 的容器,也就是在这个容器内部的任意对象都可以声明依赖,然后由容器自动注入。

本来是直接依赖,但是通过反转成注入依赖的方式,就避免了耦合,这就是 ioc (invert of control)的思想,或者叫 di(dependency inject)。

这个 CodeApplication 就是 ioc 容器。

CodeApplication

//src/vs/code/electron-main/app.ts

export class CodeApplication {

    // 依赖注入
    constructor(
        @IInstantiationService private readonly mainInstantiationService: IInstantiationService,
        @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService
    ){
        super();
    }
    
    async startup(): Promise<void> {
        const mainProcessElectronServer = new ElectronIPCServer();
        this.openFirstWindow(mainProcessElectronServer)
    }
    
    private openFirstWindow(mainProcessElectronServer: ElectronIPCServer): ICodeWindow[] {
        this.windowsMainService.open({...});
    }
}

CodeApplication 里面通过装饰器的方式声明依赖,当通过容器创建实例的时候会自动注入声明的依赖。

startup 里面启动了第一个窗口,也就是渲染进程,因为主进程和渲染进程之间要通过 ipc 通信,所以还会创建一个 ElectronIPCServer 的实例(其实它只是对 ipc 通信做了封装)。

最终通过 windowMainService 的服务创建了窗口。

虽然比较绕,但是通过 service 和 ioc 的方式,能够更好的治理复杂度,保证应用的架构不会越迭代越乱。

然后我们来看具体的窗口创建逻辑。

windowMainService

//src/vs/platform/windows/electron-main/windowsMainService.ts

export class WindowsMainService {
    
    open(openConfig): ICodeWindow[] {
       this.doOpen(openConfig);
    }
    
    doOpen(openConfig) {
        this.openInBrowserWindow();
    }
    
    openInBrowserWindow(options) {
        // 创建窗口
        this.instantiationService.createInstance(CodeWindow);
    }
}

在 windowMainService 里面最终创建了 CodeWindow 的实例,这就是对 BrowserWindow 的封装,也就是 vscode 的窗口。(用 xxx.createIntance 创建是因为要受 ioc 容器管理)

CodeWindow

//src/vs/platform/windows/electron-main/window.ts
import { BrowserWindow } from 'electron';

export class CodeWindow {
    constructor() {
        const options = {...};
        this._win = new BrowserWindow(options);
        this.registerListeners();
        this._win.loadURL('vs/code/electron-browser/workbench/workbench.html');
    }
}

CodeWindow 是对 electron 的 BrowserWindow 的封装,就是 vscode 的 window 类。

它创建 BrowserWindow 窗口,并且监听一系列窗口事件,最后加载 workbench 的 html。这就是 vscode 窗口的内容,也就是我们平时看到的 vscode 的部分。

至此,我们完成了 electron 启动到展示第一个 vscode 窗口的逻辑,已经能够看到 vscode 的界面了。

总结

vscode 是基于 electron 做窗口的创建和进程通信的,应用启动的时候会跑主进程,从 src/main 开始执行,然后创建 CodeMain 对象。

CodeMain 里会初始化很多服务,然后创建 CodeApplication,这是 ioc 的实现,全局唯一。对象的创建由容器来管理,里面所有的对象都可以相互依赖注入。

最开始会先通过 windowMainSerice 服务来创建一个 CodeWindow 的实例,这就是窗口对象,是对 electron 的BrowserWindow 的封装。

窗口内加载 workbench.html,这就是我们看到的 vscode 的界面。

vscode 就是通过这样的方式来基于 electron 实现了窗口的创建和界面的显示,感兴趣的可以参考本文去看下 vscode 1.59.0 的源码,是能学到很多架构方面的东西的,比如 ioc 容器来做对象的统一管理,通过各种服务来管理各种资源,这样集中管理的方式使得架构不会越迭代越乱,复杂度得到了很好的治理,此外,学习 vscode 源码也能够提升对 electron 的掌握。