本文总结 VSCode 的大致启动过程,即从启动到打开第一个界面之间的过程。阅读版本为官方当前 最新代码 的 master 分支。
启动 CodeMain
下面介绍从启动到运行 CodeMain
的过程。
VSCode 的启动脚本为 src/main.js
。其在完成一些必要的设置后 (包括启用 ASAR、设置一些路径和缓存、注册 Scheme、启动全局监听器以及解析启动的命令行参数和配置本地化),等待 app 的 ready
触发运行 onReady
加载 VSCode 的代码 ( 之后的代码均经过精简 ):
1 | app.once('ready', function () { |
onReady
函数在缓存和本地化完成后用得到的缓存目录 cachedDataDir
和本地化配置 nlsConfig
作为参数运行 startup
:
1 | async function onReady() { |
startup
继续做一些缓存和本地化的工作后,使用 AMD 方式加载文件 vs/code/electron-main/main
,该文件即为 CodeMain
所在文件。
1 | function startup(cachedDataDir, nlsConfig) { |
进入该文件以后,会直接执行文件底部的启动代码创建一个 CodeMain
实例并调用它的 main
方法。
1 | // Main Startup |
启动 CodeApplication
main
方法主要调用实例方法 startup
来继续启动过程
1 | class CodeMain { |
创建 instantiationService
startup
需要进行一个比较重要的步骤,即启动实例化服务 instantiationService
。
VSCode 中,将有独立功能的可复用的基础代码作为服务 ( Service ) 提供给需要这些功能的实例,实例化服务 instantiationService
也是其中之一。这一步中,需要对其实例化,用于对程序中实例尤其是服务进行管理。
startup
首先会调用 createServices
来得到 instantiationService
和一个实例坏境 instanceEnvironment
。instantiationService
用于管理服务们,因此其实例化时需要提供一个服务列表services
,createServices
主要就是完成 services
的构建并使用得到的 services
去构造 instantiationService
。
构造 services
services
是一个 ServiceCollection
的实例,ServiceCollection
在内部使用一个 Map
保存服务标识符 ( ServiceIdentifier
) 和服务实例或描述符 ( SyncDescriptor
) 的映射:
1 | export class ServiceCollection { |
对外提供 get
、set
等方法对这个 Map
进行操作。
下面对 ServiceIdentifier
和 SyncDescriptor
进行说明。
ServiceIdentifier
服务标识符用来唯一表示一个服务,只能使用 createDecorator
创建。该函数接受一个唯一的服务ID ( serviceId ) 创建一个标识符,该标识符同时也是一个装饰器,该装饰器用于实现依赖注入。
1 | export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> { |
SyncDescriptor
SyncDescriptor
用于保存创建一个实例所需参数,包括构造函数和一些参数等。主要用于延迟实例化某些服务,之后如果需要使用该服务时,再根据 SyncDescriptor
中的参数进行实例化。
综上,instantiationService
中使用一个 ServiceCollection
实例 services
保存了一个 ServiceIdentifier
到 SyncDescriptor
或服务实例的映射。因此通过 instantiationService
即可访问到所有服务的实例 (如果是 SyncDescriptor
会在访问时创建实例)。
services
创建后,它会用来实例化 instantiationService
,此时使用已经注册的服务继续进行启动。接下来的目标是启动 CodeApplication
运行 CodeApplication
启动前需要进行两个主要的准备工作:
- 初始化需要初始化的服务,这里主要是
EnvironmentService
、ConfigurationService
、StateService
。 - 创建用于实例化
CodeApplication
的 IPC 服务器mainIpcServer
上面两步都使用 instantiationService
提供的 invokeFunction
方法获得对实例的访问能力,该方法需传入一个回调函数,通过函数的参数 accessor
的 get
的方法可以得到受其管理的实例,此时如果还未创建的实例也会创建。
完成后,再使用 instantiationService
的 createInstance
方法传入 mainIpcServer
和 instanceEnvironment
创建 CodeApplication
实例,并调用其 startup
方法启动 CodeApplication
。
1 | instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup(); |
CodeApplication 的构造
上面创建 CodeApplication
时,只传递了两个参数,但其构造函数来看,远不止需要两个参数:
1 | export class CodeApplication extends Disposable { |
之所以不需要传递所有的参数,是因为其他有装饰器的参数将通过依赖注入得到。
依赖注入
前面提到过,使用 createDecorator
可以创建一个装饰器,装饰器可以装饰函数参数 ( 此处是类的构造函数 ) ,同时记录该参数对应的类为该类的依赖项。因此,利用这个信息,我们就可以在创建一个对象时,查询并创建所有该类对象构建时所需的对象们,并将这些参数传递到该类的构造函数中创建对象。下面举例说明 instantiationService
是如何注入 CodeApplication
的。
首先,CodeMain
会使用下面语句创建 CodeApplication
:
1 | instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment); |
这里首先调用了 instantiationService.createInstance
,然后该函数调用私有函数 instantiationService._createInstance
创建对象:
1 | createInstance(ctorOrDescriptor: any | SyncDescriptor<any>, ...rest: any[]): any { |
这里做的事:
- 取得注入项参数表
serviceArgs
,这里主要使用instantiationService
的_getOrCreateServiceInstance
得到这些注入项serviceArgs
。 - 将传入
_createInstance
的args
补齐长度,确保注入项作为正确的参数传入构造函数。
构造一个对象需要构造函数 ctor
和参数表,这里的参数表分为两类一类是传递给 _createInstance
的参数 args
,另一类就是注入的服务 serviceArgs
。
得到这些后就可以构造对象:
1 | return <T>new ctor(...[...args, ...serviceArgs]); |
这样就完成了 CodeApplication
的构建,并且实现了对构造函数中的服务的注入。这里也能看到,注入的参数放在构造函数的参数表尾部。
总结依赖注入构建对象的过程:
- 对象首先使用参数中需要注入的对象对应的装饰器注解该参数,该注解完成依赖记录用于之后根据依赖图创建依赖对象。
- 所有对象使用
instantiationService
的createInstance
方法构建,此时需要传入构造函数ctor
和非注入项的参数args
。 - 根据之前记录的依赖情况,分析注入项依赖,取得或构建该对象的依赖,得到注入参数表
serviceArgs
。 - 使用构造函数
ctor
、非注入项的参数args
和注入参数表serviceArgs
构造对象。
创建 CodeWindow
准备
CodeApplication
构造完成立即调用其 startup
方法。该方法做了三件主要的事:
- 创建 Electron 的 IPC 服务器
electronIpcServer
。 - 启动和共享进程通信的客户端
sharedProcessClient
。 - 创建自己的实例化服务
appInstantiationService
,该服务是instantiationService
的子服务,之后打开窗口时需要访问这些服务。
完成上面步骤和一些其他的准备后,使用下面语句打开窗口:
1 | // 获得 appInstantiationService 的访问能力并传入相关参数来打开一个窗口 |
openFirstWindow
openFirstWindow
在注册了一系列 IPC 信道后,使用 windowsMainService
的 open
方法打开窗口。
1 | const app = this; |
windowsMainService
位于 src/vs/platform/windows/electron-main/windowsMainService.ts
。
windowsMainService
首先收集所有需要打开的工作区、文件夹和文件,然后调用 doOpen
进行下一步操作,这步会返回一个 CodeWindow
实例数组。
1 | const usedWindows = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyToRestore, emptyToOpen, fileInputs, foldersToAdd); |
这里考虑在一个新的窗口打开文件的过程,这里会调用 windowsMainService.openInBrowserWindow
实现新窗口的打开。
1 | usedWindows.push(this.openInBrowserWindow({ |
usedWindows
是该函数返回的 CodeWindow
实例数组,这里使用 openInBrowserWindow
创建新的窗口并放入返回的数组中。
openInBrowserWindow
同样使用 instantiationService
来实例化一个 CodeWindow
对象,并将该对象加入 WindowsMainService
的窗口数组中且绑定相关事件。
1 | // Create the window |
创建好窗口后,使用 doOpenInBrowserWindow
在 BrowserWindow
打开窗口,doOpenInBrowserWindow
主要调用 CodeWindow
的 load
方法进行加载。
CodeWindow
在创建的时候会首先创建一个 BrowserWindow
对象 _win
,调用 load
时,该函数调用 _win.loadURL
来加载指定文件。
1 | this._win.loadURL(this.getUrl(configuration)); |
getUrl
在设置相关的配置以后,调用 doGetUrl
得到真正打开的 URL。
1 | class CodeWindow { |
至此,成功加载了第一个页面,该页面位于 src/vs/code/electron-browser/workbench/workbench.html
。
小结
本文总结了从启动到加载第一个页面的大致过程,大概总结如下:
- 进入
main.js
,使用异步方式加载CodeMain
CodeMain
创建各种服务尤其是实例化服务instantiationService
,并使用instantiationService
使用依赖注入构造CodeApplication
。CodeApplication
创建 IPC 服务器等进程通讯功能然后使用windowsMainService
创建CodeWindow
CodeWindow
最终创建BrowserWindow
并加载页面。