计划

  • editor.ts IEditorGroupsAccessor

文件夹结构

从 VSCode 看大型 IDE 技术架构 - 知乎

前端界面部分

VSCode的特征,

  • 前端各部分动态交互特别多
    • 左侧双击文件,高亮,右侧编辑区打开这个文件。右侧文件打开,左侧文件栏会切换高亮此文件,等等。很多场景比这个更复杂。
  • 各部分的UI都非常的细碎
    • 文件栏,有折叠,有状态(颜色原点)展示有变动,有激活高亮,可新增文件,新增文件夹。
  • UI相似部分特别多
    • 如左侧的多个ActivityBar对应的Sidebar
    • 一个文件栏,有Open Editors,有VSCode,有OUTLINE等折叠区块,相似性很高。

可以类比DOM和BOM,有一个类似的Editor Object Model,将整个编辑器分为几个部分,Sidebar,Editor,Activity Bar,Panel,Status Bar每个部分有属性,方法和事件。来实现相互的交互。

官方的一张图

Overview of Visual Studio Code containers elements

整个前端架构是如何的?

并没有使用redux等复杂的数据流管理,而是使用面向对象,明确的区块拆分,基本都是简单的数据结构,带数据变更的事件。

  • Workbench 作为main入口,初始化Service,初始化并渲染各Part,初始化一些逻辑(监听变更逻辑,恢复之前的tab)
  • 界面被抽象为几部分,抽象类是Part
  • Service类
    • Part对象交互的抽象,如Editor的EditorService
    • 其它各种服务都抽象为Service
    • 通过依赖注入的方式,方便在其它部分引用和使用

Workbench main入口

src/vs/workbench/browser/workbench.ts

作为整个前端的入口,负责了所有初始化的逻辑

  • 初始化各种服务 dom容器,全局action,初始services,上下文按键,监听工作区变化和逻辑
  • 渲染工作区
  • 恢复(restore)之前打开的tab

renderWorkbench 在大容器上(一个dom),为各个part创建了一个容器(一个dom)。

workbench.ts 初始化并管理这些Part

  1. export class Workbench extends Disposable implements IPartService {
  2. startup(): Thenable<IWorkbenchStartedInfo> {
  3. this.workbenchStarted = true;
  4. // Create Workbench Container
  5. this.createWorkbench();
  6. // Install some global actions
  7. this.createGlobalActions();
  8. // 初始化Services
  9. this.initServices();
  10. // Context Keys
  11. this.handleContextKeys();
  12. // Register Listeners
  13. this.registerListeners();
  14. // Settings
  15. this.initSettings();
  16. // Create Workbench and Parts
  17. this.renderWorkbench();
  18. // Workbench Layout
  19. this.createWorkbenchLayout();
  20. // Driver
  21. if (this.environmentService.driverHandle) {
  22. registerWindowDriver(this.mainProcessClient, this.configuration.windowId, this.instantiationService).then(disposable => this._register(disposable));
  23. }
  24. // Restore Parts
  25. return this.restoreParts();
  26. }
  27. private createTitlebarPart(): void {
  28. const titlebarContainer = this.createPart(Identifiers.TITLEBAR_PART, ['part', 'titlebar'], 'contentinfo');
  29. this.titlebarPart.create(titlebarContainer);
  30. }
  31. }

renderWorkbench

  1. private renderWorkbench(): void {
  2. // Apply sidebar state as CSS class
  3. if (this.sideBarHidden) {
  4. DOM.addClass(this.workbench, 'nosidebar');
  5. }
  6. if (this.panelHidden) {
  7. DOM.addClass(this.workbench, 'nopanel');
  8. }
  9. if (this.statusBarHidden) {
  10. DOM.addClass(this.workbench, 'nostatusbar');
  11. }
  12. // Apply font aliasing
  13. this.setFontAliasing(this.fontAliasing);
  14. // Apply fullscreen state
  15. if (browser.isFullscreen()) {
  16. DOM.addClass(this.workbench, 'fullscreen');
  17. }
  18. // Create Parts
  19. this.createTitlebarPart();
  20. this.createActivityBarPart();
  21. this.createSidebarPart();
  22. this.createEditorPart();
  23. this.createPanelPart();
  24. this.createStatusbarPart();
  25. // Notification Handlers
  26. this.createNotificationsHandlers();
  27. // Menubar visibility changes
  28. if ((isWindows || isLinux) && this.useCustomTitleBarStyle()) {
  29. this.titlebarPart.onMenubarVisibilityChange()(e => this.onMenubarToggled(e));
  30. }
  31. // Add Workbench to DOM
  32. this.container.appendChild(this.workbench);
  33. }

单看Editor部分的实现

单看Editor有哪些Part和Service各自的职责

  • EditorPart 继承Part,并实现了IEditorGroupsService,IEditorGroupsAccessor
  • EditorService
  • IEditorGroupsService
  • IEditorGroupsAccessor
  • IEditorGroupView

所以对架构人要求非常高,需要有很好的整体抽象设计和拆分。

Part类 整个工作区抽象为几个区块

editorPart.ts

  • 描述界面的抽象类Part,有一些template的方法
  • 可能还会被抽象为一个Service
    • editorPart.ts 还实现了 EditorGroupsServiceImpl
  1. export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditorGroupsAccessor {
  2. updateStyles(){} //
  3. layout(){}
  4. }
  5. registerSingleton(IEditorGroupsService, EditorPart);

EditorService

  1. export class EditorService extends Disposable implements EditorServiceImpl {
  2. // 对外的事件监听函数
  3. get onDidActiveEditorChange(): Event<void> { return this._onDidActiveEditorChange.event; }
  4. constructor(
  5. // 上面EditorPart的实例,editorGroupService
  6. @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
  7. @IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
  8. @IInstantiationService private readonly instantiationService: IInstantiationService,
  9. @ILabelService private readonly labelService: ILabelService,
  10. @IFileService private readonly fileService: IFileService,
  11. @IConfigurationService private readonly configurationService: IConfigurationService
  12. ) {
  13. super();
  14. this.fileInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileInputFactory();
  15. this.registerListeners();
  16. }
  17. private registerListeners(): void {
  18. this.editorGroupService.whenRestored.then(() => this.onEditorsRestored());
  19. this.editorGroupService.onDidActiveGroupChange(group => this.handleActiveEditorChange(group));
  20. this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
  21. }
  22. private handleActiveEditorChange(group: IEditorGroup): void {
  23. if (group !== this.editorGroupService.activeGroup) {
  24. return; // ignore if not the active group
  25. }
  26. if (!this.lastActiveEditor && !group.activeEditor) {
  27. return; // ignore if we still have no active editor
  28. }
  29. if (this.lastActiveGroupId === group.id && this.lastActiveEditor === group.activeEditor) {
  30. return; // ignore if the editor actually did not change
  31. }
  32. this.doEmitActiveEditorChangeEvent();
  33. }
  34. private doEmitActiveEditorChangeEvent(): void {
  35. const activeGroup = this.editorGroupService.activeGroup;
  36. this.lastActiveGroupId = activeGroup.id;
  37. this.lastActiveEditor = activeGroup.activeEditor;
  38. this._onDidActiveEditorChange.fire();
  39. }
  40. }
  41. registerSingleton(IEditorService, EditorService);

Service类 服务的设计

整个个程序都Service化,通过依赖注入,或者通过暴露ServicesAccessor的实例来访问到任何一个Service,可以理解为Service后,都是全局可访问的

基本上逻辑部分都交给Service

  • editorService listService 界面交互相关Service
  • configurationService themeService 数据配置相关的service
  1. CommandsRegistry.registerCommand('_workbench.open', function (accessor: ServicesAccessor, args: [URI, IEditorOptions, EditorViewColumn, string?]) {
  2. const editorService = accessor.get(IEditorService);
  3. const editorGroupService = accessor.get(IEditorGroupsService);
  4. const openerService = accessor.get(IOpenerService);
  5. const urlService = accessor.get(IURLService);
  6. }

editorService.ts 编辑器区域对外交互的服务,通过属性,事件监听,API方法进行区块交互

  • 属性 数据 标明当前的状态 激活的,可见的编辑窗口
  • 事件监听 监听变化 可监听激活的,可见的编辑窗口变化
  • 方法 可进行操作,判断 打开编辑器,判断是否打开
  1. // 属性
  2. activeEditor
  3. visibleEditors
  4. // 事件
  5. onDidActiveEditorChange
  6. onDidVisibleEditorsChange
  7. // 方法
  8. openEditor
  9. isOpen

各种类抽象

  • Part
  • Widget
  • Panel
  • Tree

设计模式

  • 依赖注入
  • 注册器模式

依赖注入

依赖注入在整个程序中占了很大的部分

依赖注入的好处?

  • 对依赖的类解耦,不直接依赖于具体的类,只依赖抽象接口。跨平台时传入不同的类
  1. export class OpenWorkspaceConfigFileAction extends Action {
  2. static readonly ID = 'workbench.action.openWorkspaceConfigFile';
  3. static readonly LABEL = nls.localize('openWorkspaceConfigFile', "Open Workspace Configuration File");
  4. constructor(
  5. id: string,
  6. label: string,
  7. @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
  8. @IEditorService private editorService: IEditorService
  9. ) {
  10. super(id, label);
  11. this.enabled = !!this.workspaceContextService.getWorkspace().configuration;
  12. }
  13. run(): Thenable<any> {
  14. return this.editorService.openEditor({ resource: this.workspaceContextService.getWorkspace().configuration });
  15. }
  16. }

依赖注入的实现

背后的过程

  • 使用instantiationService背后来实例化,而非直接使用new
  • 1 找到类依赖的Service,即constructor函数带有 @ 开头的参数,即依赖的services的ID。
    • 通过调用 _util.getServiceDependencies 函数找到依赖
      • 通过类上挂载的_util.DI_DEPENDENCIES静态属性找到依赖,静态属性是在构造函数装饰器添加的
  • 2 然后根据id获取service的实例
  • 3 实例化,传入对应参数

构造函数装饰器

  1. https://www.tslang.cn/docs/handbook/decorators.html 先了解函数参数装饰器
  2. 背后的service都通过createDecorator生成一个装饰器
  3. 装饰器执行时,会把Service依赖挂载到 _util.DI_DEPENDENCIES 这个属性上

依赖注入的过程

  1. // 实例化都使用instantiationService来调用
  2. this.activitybarPart = this.instantiationService.createInstance(ActivitybarPart, Identifiers.ACTIVITYBAR_PART);
  3. // arguments defined by service decorators
  4. let serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);
  5. // vs/platform/instantiation/common/instantiation.ts
  6. export namespace _util {
  7. export const serviceIds = new Map<string, ServiceIdentifier<any>>();
  8. export const DI_TARGET = '$di$target';
  9. export const DI_DEPENDENCIES = '$di$dependencies';
  10. export function getServiceDependencies(ctor: any): { id: ServiceIdentifier<any>, index: number, optional: boolean }[] {
  11. return ctor[DI_DEPENDENCIES] || [];
  12. }
  13. }

装饰器执行的过程

  1. // 执行createDecorator生成IEditorService装饰器
  2. export const IEditorService = createDecorator<IEditorService>('editorService');
  3. // 创造装饰器
  4. export function createDecorator<T>(serviceId: string): { (...args: any[]): void; type: T; } {
  5. if (_util.serviceIds.has(serviceId)) {
  6. return _util.serviceIds.get(serviceId);
  7. }
  8. const id = <any>function (target: Function, key: string, index: number): any {
  9. if (arguments.length !== 3) {
  10. throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
  11. }
  12. storeServiceDependency(id, target, index, false);
  13. };
  14. id.toString = () => serviceId;
  15. _util.serviceIds.set(serviceId, id);
  16. return id;
  17. }
  18. // 将这个Service依赖挂载到类的_util.DI_DEPENDENCIES静态属性上
  19. function storeServiceDependency(id: Function, target: Function, index: number, optional: boolean): void {
  20. if (target[_util.DI_TARGET] === target) {
  21. target[_util.DI_DEPENDENCIES].push({ id, index, optional });
  22. } else {
  23. target[_util.DI_DEPENDENCIES] = [{ id, index, optional }];
  24. target[_util.DI_TARGET] = target;
  25. }
  26. }

注册器模式 ListService

快捷键的注册模式

  1. KeybindingsRegistry.registerCommandAndKeybindingRule

ListService的源码

  1. export class ListService implements IListService {
  2. private lists: IRegisteredList[] = [];
  3. private _lastFocusedWidget: ListWidget | undefined = undefined;
  4. get lastFocusedList(): ListWidget | undefined {
  5. return this._lastFocusedWidget;
  6. }
  7. register(widget: ListWidget, extraContextKeys?: (IContextKey<boolean>)[]): IDisposable {
  8. // 加入列表中
  9. // 监听widget的状态,如销毁,focus等状态变更,修改lists和_lastFocusedWidget状态
  10. }
  11. }

使用ListService

  1. // 面包屑上的打开
  2. KeybindingsRegistry.registerCommandAndKeybindingRule({
  3. id: 'breadcrumbs.revealFocusedFromTreeAside',
  4. weight: KeybindingWeight.WorkbenchContrib,
  5. primary: KeyMod.CtrlCmd | KeyCode.Enter,
  6. when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),
  7. handler(accessor) {
  8. const editors = accessor.get(IEditorService);
  9. const lists = accessor.get(IListService);
  10. const element = <OutlineElement | IFileStat>lists.lastFocusedList.getFocus();
  11. if (element instanceof OutlineElement) {
  12. // open symbol in editor
  13. return editors.openEditor({
  14. resource: OutlineModel.get(element).textModel.uri,
  15. options: { selection: Range.collapseToStart(element.symbol.selectionRange) }
  16. }, SIDE_GROUP);

Service模式


优秀代码部分

  • 代码分层,代码的抽象层次一致
    • 逻辑集中在workbench.ts中,一眼看去非常清晰
    • 看startup函数,渲染,初始service,状态restore 【高层策略】集中
      • 如果是我,恢复过去状态,我可能分散写在sidebar和editor中了
      • restore的逻辑写在了workbeanch中,而非各组件中,这样就非常有顺序了

性能 管控

Disposable类,dispose 方法

  • dispose

问题

  • 面向对象的维护性,修改太复杂了
    • 一个
  • 界面的描述不像React标签或Vue模板那么清晰

测试


Typescript 约束

能做定义的都做定义

  • 都有 impl的抽象约束,需要用 implements 这样实现
  1. export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditorGroupsAccessor {
  2. }
  • 抽象类

借鉴