在本文编写时,Monaco Editor的版本为0.41.0
文章脉络
-
Monaco Editor历史:与VS Code的关系
-
Monaco Editor的打包构建
- 打包构建核心逻辑
- 打包构建基础语言特性
-
关键逻辑(依赖注入)
- 依赖如何收集
- 依赖如何实例化
-
总结
Monaco Editor历史:与VS Code的关系
Monaco Editor是微软开发的运行在浏览器端的代码编辑器,由Erich Gamma带领开发,Erich Gamma是大家熟知的书籍《设计模式:可复用面向对象软件的基础》的作者,参与过Eclipse的开发与软件架构设计,Erich Gamma51岁从IBM离职后,加入到Microsoft,并在苏黎世领导团队从事online developer tooling的相关工作。
外观上看,Monaco Editor与VS Code中代码编辑的部分很相似,事实上二者关系非常密切。在VS Code的源码中可以发现,Monaco Editor确实是从VS Code源码中抽离出来的。因此可能你会认为Monaco Editor是后于VS Code诞生的,然而事实刚好相反,Monaco Editor比VS Code要更早诞生。
在2011年,Monaco Editor项目便开始了。最初它是一个以"Monaco"为代号的项目,目标是实现不使用任何的UI Framework,支持语法高亮和智能提示的编辑器,最早用于接入到Azure相关的产品中;后来团队决定项目往Web IDE方向发展,并且改名为Monaco Workbench,加入了代码浏览、Git、代码搜索等功能;到了2014年,适逢Electron的诞生,团队决定项目从Web端转向桌面端,于是VS Code诞生了,并且积极拥抱开源,支持插件系统扩展API,还有添加了Language Server Protocol功能,从此做大做强。
Monaco Editor的打包构建
Monaco Editor打包构建的流程可以分解为两个步骤:
- 步骤1:打包构建核心逻辑,在
./build/build-monaco-editor.ts
; - 步骤2:打包构建基础语言特性,在
./build/build-languages.ts
;
打包构建核心逻辑
事实上Monaco Editor的核心逻辑是从VS Code中分离出来的,那它是怎么进行分离的呢?
先从Monaco Editor项目的打包脚本分析,在./build/build-monaco-editor.ts
文件中可以找到项目打包流程的逻辑,同时对比monaco-editor的npm包中的文件,分析出整体流程大致如下:
- 1、打包输出AMD模块格式到dev目录
- 2、打包输出AMD模块格式到min目录
- 3、打包输出ESM模块格式到esm目录
- 4、输出.d.ts声明文件
在脚本中会发现,打包流程会从node_modules
中一个叫monaco-editor-core
的npm包中拷贝代码,那么monaco-editor-core
是何方圣神呢?
从monaco-editor-core
的README文件中可知,它是Monaco Editor的一个构建模块,可以简单理解成它是处于VS Code和Monaco Editor之间的一个中间模块,在从VS Code抽离过程中,会先从VS Code的项目中生成并发布这个monaco-editor-core
的npm包,最后在发布Monaco Editor时在从monaco-editor-core
拷贝代码;如果进一步仔细查看monaco-editor-core
包中的代码,可以发现基本和monaco-editor
包一模一样。
那么monaco-editor-core
是如何从VS Code项目中分离出来的呢?
VS Code是使用Gulp进行打包构建的,在项目的./build
目录下的gulpfile.editor.js
文件包含构建monaco-editor-core
包的主要逻辑,脚本内定义的gulp taskeditor-distro
正是整个打包流程的入口,它的流程会从以下源码中三个入口对所有相关依赖的文件逐一拷贝到monaco-editor-core
包中:
'vs/editor/editor.main',
'vs/editor/editor.worker',
'vs/base/worker/workerMain',
'vs/editor/editor.worker'
和'vs/base/worker/workerMain'
这两个入口文件与Monaco Editor的web worker线程的逻辑有关;'vs/editor/editor.main'
这个入口文件与Manaco Editor的主体逻辑有关;
打包构建基础语言特性
除了核心逻辑,Monaco Editor还包含基础语言特性的逻辑,它的打包逻辑在./build/basic-languages.ts
中,它的主要逻辑如下:
- 打包html语言特性,转换为ESM、AMD模块格式;
- 打包css语言特性,转换为ESM、AMD模块格式;
- 打包json语言特性,转换为ESM、AMD模块格式;
- 打包typescript语言特性,转换为ESM、AMD模块格式;
- 打包其他基础语言的语法高亮配置;
到这里我们大概了解整个Monaco Editor的打包流程,同时也验证了Monaco Editor的代码确实是VS Code代码的一部分;
关键逻辑(依赖注入)
本章节将从editor.main
入口文件开始分析Monaco Editor的关键逻辑,总的来说Monaco Editor做了两件事情:
- 1、导入和执行关键JS逻辑并完成统一注册
导入的关键JS逻辑主要是对编辑器中Action、Command、Contribution以及Icon等相关对象进行注册(register),注册后的对象将被统一管理,按需使用;
至于Action、Command、Contribution有什么作用呢?这个在本文中暂时不讨论。
- 2、对外导出Monaco Editor的Api
例如我们常用的editor.create
方法,是属于editor.api
文件中暴露出来editor
模块的方法,除了editor
模块外,editor.api
文件还对外暴露出常用的languages
模块;
editor.create
可以说是Monaco Editor最关键的逻辑,这个方法内部会初始化StandaloneServices服务以及其他一堆的单例Monaco Editor服务;
说到服务
,这里不得不介绍Monaco Editor的服务(service)
的概念,在Monaco Editor中存在许许多多的服务(service)
,这些服务间存在着依赖的关系,为了解决这些服务
间相互依赖时的耦合问题,Monaco Editor采用依赖注入(主要是构造函数注入)的方式去给服务
注入它所需要的子服务
,下面将介绍Monaco Editor中是怎么实现依赖注入的。
依赖注入的概念这里就不详述了,它的主要作用是降低模块的耦合度,提高可维护性;Monaco Editor中有许多的服务(Service),这些服务提供不同的方法以供其他服务使用,服务既可以是调用者,也可以是被调用者,这意味着服务与服务之间将存在依赖,而Monaco Editor中,通过依赖注入(DI)的方式实现服务的注入,而无需服务的调用者手动初始化被调用者服务,极大地降低服务间的耦合度;
说起依赖注入,难免会想起控制反转,这里区分一下这两个概念,
控制反转(IOC)
是思想,依赖注入(DI)
是实现;
Monaco Editor的源码中,可以发现大量的依赖注入逻辑,例如在StandaloneResourcePropertiesService
中会使用参数装饰器@IConfigurationService
来实现IConfigurationService
依赖的注入;
class StandaloneResourcePropertiesService implements ITextResourcePropertiesService {
declare readonly _serviceBrand: undefined;
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
}
getEOL(resource: URI, language?: string): string {
const eol = this.configurationService.getValue('files.eol', { overrideIdentifier: language, resource });
if (eol && typeof eol === 'string' && eol !== 'auto') {
return eol;
}
return (isLinux || isMacintosh) ? '\n' : '\r\n';
}
}
实现依赖注入需要解决两个问题:
- 依赖如何收集:如何手机一个对象所依赖的对象;
- 依赖如何实例化:在对象实例化时,它所依赖的对象如何被实例化;
那么在Monaco Editor中,是如何解决这两个问题的呢?下面将详细分析:
依赖如何收集
Monaco Editor使用装饰器去进行依赖的手机,服务
通过createDecorate
函数去定义自己的参数装饰器,参数装饰器将用在构造函数中声明依赖关系:
export function createDecorator(serviceId: string): ServiceIdentifier {
if (_util.serviceIds.has(serviceId)) {
return _util.serviceIds.get(serviceId)!;
}
const id = function (target: Function, key: string, index: number): any {
if (arguments.length !== 3) {
throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
}
storeServiceDependency(id, target, index);
};
id.toString = () => serviceId;
_util.serviceIds.set(serviceId, id);
return id;
}
每一个装饰器都作为id缓存存在_util.serviceIds
的Map数据结构中,既用与复用,也用于作为服务
的唯一标识;storeServiceDependency
方法将服务
的id标识保存在被装饰类的[_util.DI_DEPENDENCIES]
属性当中,通过这样的方式,就实现了被装饰的服务所依赖其他服务的收集;
function storeServiceDependency(id: Function, target: Function, index: number): void {
if ((target as any)[_util.DI_TARGET] === target) {
(target as any)[_util.DI_DEPENDENCIES].push({ id, index });
} else {
(target as any)[_util.DI_DEPENDENCIES] = [{ id, index }];
(target as any)[_util.DI_TARGET] = target;
}
}
依赖如何实例化
完成依赖的收集后,基本上每个服务
间的依赖关系已经分析完成,每个服务
上都挂载着它所依赖的其他服务
的id
,但是有了这些id
并不能实现实例化,需要让这些id
与对应服务
的类匹配上,在Monaco Editor中,服务
可以通过调用registerSingleton
方法去实现id
与服务
的类的一一对应;
export function registerSingleton(id: ServiceIdentifier, ctorOrDescriptor: { new(...services: Services): T } | SyncDescriptor, supportsDelayedInstantiation?: boolean | InstantiationType): void {
if (!(ctorOrDescriptor instanceof SyncDescriptor)) {
ctorOrDescriptor = new SyncDescriptor(ctorOrDescriptor as new (...args: any[]) => T, [], Boolean(supportsDelayedInstantiation));
}
_registry.push([id, ctorOrDescriptor]);
}
例如,当我需要为服务StandaloneResourcePropertiesService
进行注册时,通过该方法即可实现:
registerSingleton(ITextResourcePropertiesService, StandaloneResourcePropertiesService, InstantiationType.Eager);
Monaco Editor还做了另外一个事情,就是通过ServiceCollection
类,将这些通过registerSingleton
注册的服务收拢到ServiceCollection
中,ServiceCollection
在服务实例化的过程中起了很大作用;
那么完整的实例化到底是怎么实现的呢?按照依赖注入的概念,依赖是需要通过"外部容器"
来帮忙实现的;在Monaco Editor中,这个外部容器是InstantiationService
类,它提供的createInstance
是关键逻辑所在;
createInstance(ctorOrDescriptor: any | SyncDescriptor, ...rest: any[]): any {
let _trace: Trace;
let result: any;
if (ctorOrDescriptor instanceof SyncDescriptor) {
_trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor.ctor);
result = this._createInstance(ctorOrDescriptor.ctor, ctorOrDescriptor.staticArguments.concat(rest), _trace);
} else {
_trace = Trace.traceCreation(this._enableTracing, ctorOrDescriptor);
result = this._createInstance(ctorOrDescriptor, rest, _trace);
}
_trace.stop();
return result;
}
例如当我需要初始化StandaloneEditor
类时,我需要如下调用,第一个参数就是需要被实例化的类:
instantiationService.createInstance(StandaloneEditor, domElement, options);
createInstance
接着会调用私有方法_createInstance
:
private _createInstance(ctor: any, args: any[] = [], _trace: Trace): T {
// 获取ctor类的依赖
const serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);
const serviceArgs: any[] = [];
for (const dependency of serviceDependencies) {
// 初始化依赖
const service = this._getOrCreateServiceInstance(dependency.id, _trace);
if (!service) {
this._throwIfStrict(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`, false);
}
serviceArgs.push(service);
}
const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;
// 参数个数不匹配,则进行调整
if (args.length !== firstServiceArgPos) {
console.trace(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);
const delta = firstServiceArgPos - args.length;
if (delta > 0) {
args = args.concat(new Array(delta));
} else {
args = args.slice(0, firstServiceArgPos);
}
}
// 初始化ctor
return Reflect.construct(ctor, args.concat(serviceArgs));
}
_createInstance
先是通过_util.getServiceDependencies
获取ctor
的依赖后,并逐一初始化;完成依赖初始化后,再对ctor
本身进行初始化;
在进行依赖的初始化时,调用了_getOrCreateServiceInstance
私有方法:
protected _getOrCreateServiceInstance(id: ServiceIdentifier, _trace: Trace): T {
if (this._globalGraph && this._globalGraphImplicitDependency) {
this._globalGraph.insertEdge(this._globalGraphImplicitDependency, String(id));
}
const thing = this._getServiceInstanceOrDescriptor(id);
if (thing instanceof SyncDescriptor) {
return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true));
} else {
_trace.branch(id, false);
return thing;
}
}
这里_getOrCreateServiceInstance
会先从ServiceCollection
中获取是否已经实例化的单例,如果没有的话,将继续调用_createAndCacheServiceInstance
进行实例化:
private _createAndCacheServiceInstance(id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace): T {
type Triple = { id: ServiceIdentifier; desc: SyncDescriptor; _trace: Trace };
// 定义图的数据结构,用于构造依赖的有向无环图
const graph = new Graph(data => data.id.toString());
let cycleCount = 0;
const stack = [{ id, desc, _trace }];
// 深度优先遍历,检查有向无环图中是否存在环
while (stack.length) {
const item = stack.pop()!;
graph.lookupOrInsertNode(item);
if (cycleCount++ > 1000) {
throw new CyclicDependencyError(graph);
}
for (const dependency of _util.getServiceDependencies(item.desc.ctor)) {
const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
if (!instanceOrDesc) {
this._throwIfStrict(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`, true);
}
this._globalGraph?.insertEdge(String(item.id), String(dependency.id));
if (instanceOrDesc instanceof SyncDescriptor) {
const d = { id: dependency.id, desc: instanceOrDesc, _trace: item._trace.branch(dependency.id, true) };
graph.insertEdge(item, d);
stack.push(d);
}
}
}
// 从根节点开始自底向上进行依赖的初始化
while (true) {
const roots = graph.roots();
if (roots.length === 0) {
if (!graph.isEmpty()) {
throw new CyclicDependencyError(graph);
}
break;
}
for (const { data } of roots) {
const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);
if (instanceOrDesc instanceof SyncDescriptor) {
const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation, data._trace);
this._setServiceInstance(data.id, instance);
}
graph.removeNode(data);
}
}
return this._getServiceInstanceOrDescriptor(id);
}
_createAndCacheServiceInstance
是整个依赖注入初始化逻辑的核心,内部首先会对整个依赖链路进行循环依赖的检查,这里使用到了图论
中的有向无环图
,图中的每个节点表示依赖
,当整个图中不存在环则表示不存在循环依赖;接着再从图中无出度的节点开始自底向上进行初始化,初始化这里调用了_createServiceInstanceWithOwner
方法进行初始化;最后调用_setServiceInstance
方法将已经实例化的的依赖保存到ServiceCollection
中以便复用;
至此,Monaco Editor服务实现依赖注入已经介绍完毕,简单来说依赖注入首先是自上而下获取依赖项,然后自下而上实例化依赖,非常值得学习的设计模式;
总结
本文首先介绍了Monaco Editor的历史,了解到VS Code与Monaco Editor之间的关系;然后通过分析Monaco Editor的打包构建过程,知道了Monaco Editor的源码是从VS Code中分离出来的;最后介绍了Monaco Editor中关键的依赖注入逻辑,了解到Monaco Editor中各种服务是如何解决依赖收集以及如何实现依赖初始化的。