大纲

  • 整体设计 vscode, monaco editor,extension,Electron,language service
  • IDE代码的组织
  • 复杂动态界面的代码抽象设计
  • 为什么IDE不卡?相比Atom
  • 插件的设计

系列文章

  • vscode整体介绍
  • 从vscode启动流程学electron应用开发
  • 从vscode看大型程序设计 service 设计模式
  • 从vscode看复杂前端交互组织
  • 从vscode看产品设计

TODO

  • 代码是如何组织的
  • 梳理和理解了Register模式
  • 启动流程
  • 右键菜单流程解析
  • 右键菜单复杂交互背后
  • 如果在界面上加一块界面,需要怎么做?
  • 复杂动态界面代码抽象,常见联动如何做?纯DOM,以及事件绑定

整体设计

  • vscode
  • monaco editor
  • extension
  • Electron
  • language service,eslint等等

进程结构

多进程架构带来的速度优化

image.png

有一个主进程和多个渲染进程。如果VScode你开了多个窗口,每个窗口对应一个渲染进程,都是互相独立不影响的。每个渲染进程下,又开启有独立的插件宿主进程。

主进程
**
负责启动,打开浏览器的窗口,自动更新等与系统相关的功能。

渲染进程

**
即我们看到的整个浏览器窗口。因为Electron的缘故,可以在浏览器中运行Nodejs代码。我们开的每一个窗口都对应一个独立的渲染进程,互相之间不影响。

插件进程

每个渲染进程下又有独立的插件进程,因此保证了主进程的运行速度,但同时插件对UI的操作能力非常弱

进程间通讯,IPC/RPC

渲染进程中可以开Worker进行耗时计算,

通信 调用

processes.png

  • IPC通信
    • 主进程和render进程的通信
    • 利用electron提供的ipcRender和ipcMain进行通信。
    • IPC通信,类似EventEmitter,通过消息机制调用
  • RPC通信
    • Render进程和插件进程的通信,有一个 RPCProtocol.ts 实现。
    • RPC是更高级的实现,调用其它进程的方法,就像调用本地API方法一样。
    • Electron中的remote就是RPC的一种实现

IDE代码的组织

  • 依赖注入
  • 案例,启动流程的代码解析
  • 案例,右键菜单的代码解析

分类

  • workbench 整体视图框架
  • platform 可被依赖注入的各种纯服务
  • editor 文本编辑器
  • code 应用主入口
  • base 通用的公共方法和公共视图组件
  • extensions 内置插件


  1. MainThreadWebviews // 很重要的,什么?

流程解析

启动流程代码解析

  • main.js
  • vs/code/electron-main/main

  1. /src/vs/workbench/browser/parts/editor/

右键菜单代码解析

复杂动态界面的代码抽象设计

特征

  • 动态化
  • 界面复杂,区块繁多
  • 相似性较多

有哪些设计?

  • 面向对象 非常细力度华
  • 区块Controller维护
  • Command
  • 依赖注入,Service
  • 案例,菜单栏变化,以及和右侧文件区联动
  • 案例,右侧代码和eslint的联动

Service服务化设计

整个程序都Service化,通过依赖注入,或者通过暴露ServicesAccessor来访问到任何一个Service。

  • 通过依赖注入方式,方便的引入依赖的Service
  • 依赖注入,可以帮助Vscode在不同端(Web,PC端)根据特征注入不同的Service
  • 有助于将Controller中较复杂逻辑拆分出Service,降低耦合度

有哪些Service

  • editorService
  • workspaceContextService
  • listService 这个代码设计值得看一下 register的注册模式
  • IConfigurationService

editorService.ts

/src/vs/workbench/services/editor/common/editorService.ts

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

Accessor获取方式

  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);

List Service

Command设计解耦

https://tuhrig.de/messages-vs-events-vs-commands/

各组件间存在互相调用,有几种解决方式

  • 引用类,即new实例,然后调用方法
  • Command,即命令模式,
  • Event 主要是有多个监听者,即多个消费者,
  • Message,跟command没啥差别

但是在VSCode中,存在插件调用等消息传递,不遍引用类,因此用command调用非常合适

同时Command也比记忆各种类

面向对象的深度使用

相似组件抽象interface & 约束

将不用类型都抽象出其interface,对实现的类约束其实现。因为在vscode中有很多外观有差异,但是行为表现差不多的组件。

  1. export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSashLayoutProvider {
  2. }
  3. export abstract class Widget extends Disposable {
  4. }

类的标记

IMainContext 和 rpcProtocol

统一管理 通过类别来判断

不同的列表都存储在了listService中统一存放。

但是通过 element instanceOf OutlineElement 来判断点击的是否是某种需要的类型

  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);

面向对象 多个implements

其实是各种定义interface,来约束类的实现,定义interface也帮助仔细想清楚设计哪些能力

面向对象继承

存在的问题

  • 修改麻烦。增加新的属性,每个类都要修改
  • 对人的要求太高了。设计的不好,会混乱不堪
  1. export interface ICodeEditor extends editorCommon.IEditor {}
  2. export interface IActiveCodeEditor extends ICodeEditor {}

较高层次抽象

将整个工作区抽象为几个较大的区块,并通过service模式来互相交互

  • sidebarPart
  • editorService
  • StatusbarPart IStatusbarService
  • TitlebarPart

问题

  • 面向对象,修改不容易
  • 没有类似jsx或vue的模板,整个界面看起来不清晰

设计模式

  • 依赖注入
  • 注册器模式

依赖注入

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

  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. }

注册器模式

快捷键的注册模式

  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);

为什么不卡

插件的设计

  • 如何在插件中获取到vscode对象
  • 和插件的通信方式

Typescript的一些用法

  • implements 实现接口的约束,定义了非常多的协议
    • extHost.protocol.ts
  • private 的声明
  • @符号 什么意思?依赖注入时使用的标记
  • 面向对象来判断属于什么类型
  1. if (this.element instanceof FileElement) {
  2. } else if (this.element instanceof OutlineModel) {
  3. }

借鉴

Visual Studio Code有哪些工程方面的亮点 - 知乎
微软 VSCode IDE 源码分析揭秘 - 知乎
从 VSCode 看大型 IDE 技术架构 - 知乎
vscode编辑器打开大项目能够快速预览,这是如何做到的?软件算法比atom做的好? - 知乎

vscode 定制开发 —— Workbench 源码解读及实战 | 匠心博客
vscode 定制开发 —— 基础准备 | 匠心博客
vscode源码剖析 | xzper
Visual Studio Code / Egret Wing 技术架构:基础 · Chen’s Blog
Visual Studio Code / Egret Wing 技术架构:插件系统 · Chen’s Blog

What I Learned by Building My Own VS Code Extension | CSS-Tricks

VSCode插件开发小记 · 构建我的被动收入
VSCode 插件架构|极客教程