计划
- editor.ts IEditorGroupsAccessor
文件夹结构
前端界面部分
VSCode的特征,
- 前端各部分动态交互特别多。
- 左侧双击文件,高亮,右侧编辑区打开这个文件。右侧文件打开,左侧文件栏会切换高亮此文件,等等。很多场景比这个更复杂。
- 各部分的UI都非常的细碎。
- 文件栏,有折叠,有状态(颜色原点)展示有变动,有激活高亮,可新增文件,新增文件夹。
- UI相似部分特别多
- 如左侧的多个ActivityBar对应的Sidebar
- 一个文件栏,有Open Editors,有VSCode,有OUTLINE等折叠区块,相似性很高。
可以类比DOM和BOM,有一个类似的Editor Object Model,将整个编辑器分为几个部分,Sidebar,Editor,Activity Bar,Panel,Status Bar每个部分有属性,方法和事件。来实现相互的交互。
官方的一张图
整个前端架构是如何的?
并没有使用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
export class Workbench extends Disposable implements IPartService {
startup(): Thenable<IWorkbenchStartedInfo> {
this.workbenchStarted = true;
// Create Workbench Container
this.createWorkbench();
// Install some global actions
this.createGlobalActions();
// 初始化Services
this.initServices();
// Context Keys
this.handleContextKeys();
// Register Listeners
this.registerListeners();
// Settings
this.initSettings();
// Create Workbench and Parts
this.renderWorkbench();
// Workbench Layout
this.createWorkbenchLayout();
// Driver
if (this.environmentService.driverHandle) {
registerWindowDriver(this.mainProcessClient, this.configuration.windowId, this.instantiationService).then(disposable => this._register(disposable));
}
// Restore Parts
return this.restoreParts();
}
private createTitlebarPart(): void {
const titlebarContainer = this.createPart(Identifiers.TITLEBAR_PART, ['part', 'titlebar'], 'contentinfo');
this.titlebarPart.create(titlebarContainer);
}
}
renderWorkbench
private renderWorkbench(): void {
// Apply sidebar state as CSS class
if (this.sideBarHidden) {
DOM.addClass(this.workbench, 'nosidebar');
}
if (this.panelHidden) {
DOM.addClass(this.workbench, 'nopanel');
}
if (this.statusBarHidden) {
DOM.addClass(this.workbench, 'nostatusbar');
}
// Apply font aliasing
this.setFontAliasing(this.fontAliasing);
// Apply fullscreen state
if (browser.isFullscreen()) {
DOM.addClass(this.workbench, 'fullscreen');
}
// Create Parts
this.createTitlebarPart();
this.createActivityBarPart();
this.createSidebarPart();
this.createEditorPart();
this.createPanelPart();
this.createStatusbarPart();
// Notification Handlers
this.createNotificationsHandlers();
// Menubar visibility changes
if ((isWindows || isLinux) && this.useCustomTitleBarStyle()) {
this.titlebarPart.onMenubarVisibilityChange()(e => this.onMenubarToggled(e));
}
// Add Workbench to DOM
this.container.appendChild(this.workbench);
}
单看Editor部分的实现
单看Editor有哪些Part和Service各自的职责
- EditorPart 继承Part,并实现了IEditorGroupsService,IEditorGroupsAccessor
- EditorService
- IEditorGroupsService
- IEditorGroupsAccessor
- IEditorGroupView
所以对架构人要求非常高,需要有很好的整体抽象设计和拆分。
Part类 整个工作区抽象为几个区块
如 editorPart.ts
- 描述界面的抽象类Part,有一些template的方法
- 可能还会被抽象为一个Service
- 如
editorPart.ts
还实现了EditorGroupsServiceImpl
- 如
export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditorGroupsAccessor {
updateStyles(){} //
layout(){}
}
registerSingleton(IEditorGroupsService, EditorPart);
EditorService
export class EditorService extends Disposable implements EditorServiceImpl {
// 对外的事件监听函数
get onDidActiveEditorChange(): Event<void> { return this._onDidActiveEditorChange.event; }
constructor(
// 上面EditorPart的实例,editorGroupService
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILabelService private readonly labelService: ILabelService,
@IFileService private readonly fileService: IFileService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super();
this.fileInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileInputFactory();
this.registerListeners();
}
private registerListeners(): void {
this.editorGroupService.whenRestored.then(() => this.onEditorsRestored());
this.editorGroupService.onDidActiveGroupChange(group => this.handleActiveEditorChange(group));
this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
}
private handleActiveEditorChange(group: IEditorGroup): void {
if (group !== this.editorGroupService.activeGroup) {
return; // ignore if not the active group
}
if (!this.lastActiveEditor && !group.activeEditor) {
return; // ignore if we still have no active editor
}
if (this.lastActiveGroupId === group.id && this.lastActiveEditor === group.activeEditor) {
return; // ignore if the editor actually did not change
}
this.doEmitActiveEditorChangeEvent();
}
private doEmitActiveEditorChangeEvent(): void {
const activeGroup = this.editorGroupService.activeGroup;
this.lastActiveGroupId = activeGroup.id;
this.lastActiveEditor = activeGroup.activeEditor;
this._onDidActiveEditorChange.fire();
}
}
registerSingleton(IEditorService, EditorService);
Service类 服务的设计
整个个程序都Service化,通过依赖注入,或者通过暴露ServicesAccessor的实例来访问到任何一个Service,可以理解为Service后,都是全局可访问的
基本上逻辑部分都交给Service
- editorService listService 界面交互相关Service
- configurationService themeService 数据配置相关的service
CommandsRegistry.registerCommand('_workbench.open', function (accessor: ServicesAccessor, args: [URI, IEditorOptions, EditorViewColumn, string?]) {
const editorService = accessor.get(IEditorService);
const editorGroupService = accessor.get(IEditorGroupsService);
const openerService = accessor.get(IOpenerService);
const urlService = accessor.get(IURLService);
}
editorService.ts
编辑器区域对外交互的服务,通过属性,事件监听,API方法进行区块交互
- 属性 数据 标明当前的状态 激活的,可见的编辑窗口
- 事件监听 监听变化 可监听激活的,可见的编辑窗口变化
- 方法 可进行操作,判断 打开编辑器,判断是否打开
// 属性
activeEditor
visibleEditors
// 事件
onDidActiveEditorChange
onDidVisibleEditorsChange
// 方法
openEditor
isOpen
各种类抽象
- Part
- Widget
- Panel
- Tree
设计模式
- 依赖注入
- 注册器模式
依赖注入
依赖注入在整个程序中占了很大的部分
依赖注入的好处?
- 对依赖的类解耦,不直接依赖于具体的类,只依赖抽象接口。跨平台时传入不同的类
export class OpenWorkspaceConfigFileAction extends Action {
static readonly ID = 'workbench.action.openWorkspaceConfigFile';
static readonly LABEL = nls.localize('openWorkspaceConfigFile', "Open Workspace Configuration File");
constructor(
id: string,
label: string,
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
@IEditorService private editorService: IEditorService
) {
super(id, label);
this.enabled = !!this.workspaceContextService.getWorkspace().configuration;
}
run(): Thenable<any> {
return this.editorService.openEditor({ resource: this.workspaceContextService.getWorkspace().configuration });
}
}
依赖注入的实现
背后的过程
- 使用instantiationService背后来实例化,而非直接使用new
- 1 找到类依赖的Service,即constructor函数带有
@
开头的参数,即依赖的services的ID。- 通过调用
_util.getServiceDependencies
函数找到依赖- 通过类上挂载的
_util.DI_DEPENDENCIES
静态属性找到依赖,静态属性是在构造函数装饰器添加的
- 通过类上挂载的
- 通过调用
- 2 然后根据id获取service的实例
- 3 实例化,传入对应参数
构造函数装饰器
- https://www.tslang.cn/docs/handbook/decorators.html 先了解函数参数装饰器
- 背后的service都通过createDecorator生成一个装饰器
- 装饰器执行时,会把Service依赖挂载到
_util.DI_DEPENDENCIES
这个属性上
依赖注入的过程
// 实例化都使用instantiationService来调用
this.activitybarPart = this.instantiationService.createInstance(ActivitybarPart, Identifiers.ACTIVITYBAR_PART);
// arguments defined by service decorators
let serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);
// vs/platform/instantiation/common/instantiation.ts
export namespace _util {
export const serviceIds = new Map<string, ServiceIdentifier<any>>();
export const DI_TARGET = '$di$target';
export const DI_DEPENDENCIES = '$di$dependencies';
export function getServiceDependencies(ctor: any): { id: ServiceIdentifier<any>, index: number, optional: boolean }[] {
return ctor[DI_DEPENDENCIES] || [];
}
}
装饰器执行的过程
// 执行createDecorator生成IEditorService装饰器
export const IEditorService = createDecorator<IEditorService>('editorService');
// 创造装饰器
export function createDecorator<T>(serviceId: string): { (...args: any[]): void; type: T; } {
if (_util.serviceIds.has(serviceId)) {
return _util.serviceIds.get(serviceId);
}
const id = <any>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, false);
};
id.toString = () => serviceId;
_util.serviceIds.set(serviceId, id);
return id;
}
// 将这个Service依赖挂载到类的_util.DI_DEPENDENCIES静态属性上
function storeServiceDependency(id: Function, target: Function, index: number, optional: boolean): void {
if (target[_util.DI_TARGET] === target) {
target[_util.DI_DEPENDENCIES].push({ id, index, optional });
} else {
target[_util.DI_DEPENDENCIES] = [{ id, index, optional }];
target[_util.DI_TARGET] = target;
}
}
注册器模式 ListService
快捷键的注册模式
KeybindingsRegistry.registerCommandAndKeybindingRule
ListService的源码
export class ListService implements IListService {
private lists: IRegisteredList[] = [];
private _lastFocusedWidget: ListWidget | undefined = undefined;
get lastFocusedList(): ListWidget | undefined {
return this._lastFocusedWidget;
}
register(widget: ListWidget, extraContextKeys?: (IContextKey<boolean>)[]): IDisposable {
// 加入列表中
// 监听widget的状态,如销毁,focus等状态变更,修改lists和_lastFocusedWidget状态
}
}
使用ListService
// 面包屑上的打开
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'breadcrumbs.revealFocusedFromTreeAside',
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.CtrlCmd | KeyCode.Enter,
when: ContextKeyExpr.and(BreadcrumbsControl.CK_BreadcrumbsVisible, BreadcrumbsControl.CK_BreadcrumbsActive, WorkbenchListFocusContextKey),
handler(accessor) {
const editors = accessor.get(IEditorService);
const lists = accessor.get(IListService);
const element = <OutlineElement | IFileStat>lists.lastFocusedList.getFocus();
if (element instanceof OutlineElement) {
// open symbol in editor
return editors.openEditor({
resource: OutlineModel.get(element).textModel.uri,
options: { selection: Range.collapseToStart(element.symbol.selectionRange) }
}, SIDE_GROUP);
Service模式
优秀代码部分
- 代码分层,代码的抽象层次一致
- 逻辑集中在workbench.ts中,一眼看去非常清晰
- 看startup函数,渲染,初始service,状态restore 【高层策略】集中
- 如果是我,恢复过去状态,我可能分散写在sidebar和editor中了
- restore的逻辑写在了workbeanch中,而非各组件中,这样就非常有顺序了
性能 管控
Disposable类,dispose 方法
- dispose
问题
- 面向对象的维护性,修改太复杂了
- 一个
- 界面的描述不像React标签或Vue模板那么清晰
测试
Typescript 约束
能做定义的都做定义
- 都有 impl的抽象约束,需要用 implements 这样实现
export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditorGroupsAccessor {
}
- 抽象类