图解 VSCode 源码架构


前言

大家好这里是阳九. 一个菜鸡JS全栈码农。

今天来讲一下大家每天都在使用的代码编辑器VSCode,让大家对这款伴你一起成长的工具多一点了解

VSCode代码体系极其庞大,给想入坑代码编辑器的同学一点上手文章

注意: 本文的资料只是个人学习笔记, 里面大概率有错误, 如果有VSCode大佬发现我的资料有问题请给小弟指正!!!

之前我写了一款模拟VSCode架构个人代码编辑器, 有兴趣的小伙伴可以关注一下, 当然因为工作生活很久没有更新了, 有很多bug和功能不足, 只是拿来学习一下也是不错的。

lzy-code-editor: 模仿VSCode原理与架构实现的简易版桌面端代码编辑器 (github.com)

先上大图

  • 还是跟原来一样 先放上一张大图, 当然这里也省略了很多不重要(其实是我没研究过)的模块
  • 之后我们分模块给大家一步步介绍

前置知识

Electron

  • VSCode基于Electron框架进行开发, Electron是一个微软家的开源框架,用于构建跨平台的桌面应用程序。同样基于Node平台,是不可多得的TS全栈+跨平台学习项目(本人墙裂推荐)

  • Electron同时提供了两个进程, 渲染进程和主进程

  1. 主进程: 运行在Node环境下的VSCode后台服务。VSCode在这里使用微服务架构,创建了众多Services单例并提供一系模块管理,与操作系统和底层硬件之间交互的任务。
  2. 渲染进程: 每个Electron打开的窗口都由一个渲染进程实例进行控制,渲染进程提供了一个前端沙箱环境,专门用来进行UI渲染,可以理解为浏览器窗口。多个窗口可以共享同一个渲染进程。

IOC依赖注入

  • 属于比较常见的设计模式的一种, 依赖复杂时使用它来进行模块之间的解耦和依赖管理, 由外部依赖注入容器(IOC容器)对实例进行统一管理和注入。

  • 在VSCode中我们经常可以看到这样的代码 在constructor里,VSCode使用装饰器进行依赖注入,(其引用的是一个模块的接口 如EditorService (编辑器实例) 和 IEditorService (编辑器接口) 一个是接口 一个是具体实现)

具体代码可参考\vscode\src\vs\platform\instantiationService中的 InstantiationService.createInstance方法, 将一个类构造函数和构造所需参数作为identifier,并递归依赖进行实例化,封装Map作为IOC容器进行外部依赖管理

ts 复制代码
export class InstantiationService implements IInstantiationService {
        ...
    	createInstance(ctorOrDescriptor: any | SyncDescriptor<any>, ...rest: any[]): any {...
        }
}

内部通信机制

上面提到,Electron的渲染进程和主进程之间有进程隔离,那么必然就有对应的通信机制,VSCode内部提供了以下三种通信机制, 这里我们介绍两种,

  • IPC进程通信模块,
  • :VS内部通信协议
  • Shared Process共享进程通信

:VS内部协议

  • VSCode使用Electorn的protocol模块 自定义URL Schema :vs 进行通信(URI Schemes)

  • 可以将其类比为我们平时常用的http协议, VSCode的主进程服务就是暴露了一堆:vs接口,供Workbench窗口和插件进程进行调用

  • 一个protocol模块使用的Demo

js 复制代码
const { app, protocol } = require('electron');

// 注册自定义协议
protocol.registerHttpProtocol('myapp', (req, callback) => {
  const url = req.url // 接收url
  
  if (url == "app/index.html") { // 返回数据
    callback({
      data: '<h1>Hello, My App!</h1>'
    });
  }
})
// 标记 myapp 协议为 "特权"
protocol.registerSchemesAsPrivileged([
  { scheme: 'myapp' }
]);
// 此时就可以使用注册的协议进行请求
const window = new BrowserWindow({ width: 800, height: 600 });
// 加载自定义协义 URL 页面
window.loadURL('myapp://app/index.html');
  • VSCode中注册了两个自定义协议
js 复制代码
protocol.registerSchemesAsPrivileged([
	{
		scheme: 'vscode-webview',
		privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, allowServiceWorkers: true, }
	},
	{
		scheme: 'vscode-file',
		privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true }
	}
]);
  • 在 VSCode 中,内部自定义协议的使用有以下几个好处(没错就是复制粘贴的)
  1. 安全性:由于其是内部自定义协议,因此能够有效地防止反向代理等网络攻击方式产生潜在安全风险
  2. 高效性:相对于基于常规 HTTP 协议的接口,在 WebSocket 等协议上封装数据进行传输时具备更佳的通信速度和实时响应
  3. 扩展性:VSCode 可以根据需要扩展并定制化自己特定场景下所需的 API 接口与相关功能实现,并通过此类机制将其直接暴露到应用程序中。
  4. 功能丰富 : 自定义 vscode: 协议可使 VS Code 帮助各种IDE任务如文件系统调用( vscode:file:// ), Git 操作监视(vscode-gitlens) ,Web 页面渲染预览(vscode-markdown), 虚拟终端(vscode-terminal)等高级交互指令

IPC通信模块

  • VSCode在Electron的IPC模块基础上进一步封装, 构建了两个类IPCServerIPCClient,具体代码封装细节可以在vscode工程的/src/vs/parts/ipc下查看
  • 对于每个打开的窗口,它既是Server也是Client (经典Server/Client模型)
  • 在一个S-C链接中, 又创建了多个Channel子类, 同样也是一个S-C模型
  • 至于单个Channel做了什么, 可以看这张图, 有没有点像咱们每天叨叨的七层网络模型? 消息出入信道时逐层处理 "应用层,网络层...然后是啥来着?" --- 汗流浃背了吧小老弟

  • 单个channel同时也基于Node实现了事件(消息驱动),持续侦听一个channel上的所有message,进行处理 事件循环! 这里要考!!

WorkBench编辑器UI

VSCode的UI界面, 使用面向对象进行html操作,介绍目录结构有几大重要部分

  • api: 主进程提供给UI-service所调用的api
  • service: UI所需要的微服务模块,提供各种操作方法
  • browser: 具体的UI代码,封装多个UI组件part如sideBar,titleBar等

我们可以到src/vs/workbench目录下查看

  • 进入src\vs\workbench\browser\workbench.ts我们可以看到,workbench主类上注册了多个UI组件,titleBar,bannerBar,sideBar,editor编辑器等等
ts 复制代码
// 继承外层layout(layout继承自HTMLElement对象)
export class Workbench extends Layout {

    startup(){
        // 初始化layout
        this.initLayout(accessor);
        // 初始化基础事件侦听器
        this.registerListeners(lifecycleService, storageService, configurationService, hostService, dialogService);
        // 正式渲染
	      this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);
    }
    // 创建并渲染多个UI组件
    private renderWorkbench(){
		for (const { id, role, classes, options } of [
			{ id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] },
			{ id: Parts.BANNER_PART, role: 'banner', classes: ['banner'] },
			{ id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] }, // Use role 'none' for some parts to make screen readers less chatty #114892
			{ id: Parts.SIDEBAR_PART, role: 'none', classes: ['sidebar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] },
			{ id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.willRestoreEditors() } },
			{ id: Parts.PANEL_PART, role: 'none', classes: ['panel', 'basepanel', positionToString(this.getPanelPosition())] },
			{ id: Parts.AUXILIARYBAR_PART, role: 'none', classes: ['auxiliarybar', 'basepanel', this.getSideBarPosition() === Position.LEFT ? 'right' : 'left'] },
			{ id: Parts.STATUSBAR_PART, role: 'status', classes: ['statusbar'] }
		]) {
			const partContainer = this.createPart(id, role, classes);
                        // 这里的partContainer是外层创建的div
			this.getPart(id).create(partContainer, options);
		}
    }
}

这里以TitleBar举例,就是通过document.createElement创建了一个div并进行渲染

ts 复制代码
export class TitlebarPart extends Part implements ITitleService {
    // 上面定义了一些关键事件和方法
    private onBlur(): void {}
    private onFocus(): void {}
    override updateStyles(): void {}
}
// 注册titleBar上的子组件 
MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, {
	submenu: MenuId.MenubarFileMenu,
	title: {value: 'File'}
});
MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, {
	submenu: MenuId.MenubarEditMenu,
	title: {value: 'Edit'}
	}
});
......
  • 这里的File,Edit等就是我们熟悉的部分(对不起我英语菜不配用英文的VSCode)

MonacoEditor

  • 提起这个我们应该不陌生, 也就是VSCode代码编辑器的核心部分,同样也是出于微软家,这里就不过多介绍了,大家可以自行npm引入到web端看一下

  • 这里不得不感叹一下,果然真正的大厂就是不一般, 打开VSCode的源码发现内部所有的东西都是微软自研的,很少用到第三方库😭😭😭

我们熟知的MonacoEditor直接作为文件塞进去了 而不是一个npm包, 包括很多底层工具也是这么处理的

内部构建器

  • 值得一提的是, 由于workbench与mainService模块进程隔离, 渲染进程直接作为html运行在沙箱中, 且monacoEditor推荐使用AMD模块化方案进行构建,
  • 为了统一渲染进程与主进程代码的构建方案, VSCode中提供了自己的内部构建工具
  • 这里给出本人写的注释

这里可以理解为内部自行实现了仅支持AMD的简易webpack,感兴趣的小伙伴可以查看vscode\src\vs\loader.js

CodeMain主进程服务

  • VSCode在Electron主进程中运行的服务,同时也是VSCode的入口全局单例,
  • 内部通过微服务架构提供了多种子服务模块,在其startUp时会创建

感兴趣的小伙伴可以查看src\vs\code\electron-main\main.ts, 微服务么 大家都懂,慢慢看呗

ts 复制代码
class CodeMain {

    private createServices(){
        // Environment
		const environmentMainService = new EnvironmentMainService();
                const bufferLogService = new BufferLogService();
                const fileService = new FileService(logService);
                const uriIdentityService = new UriIdentityService(fileService);
                const loggerService = new LoggerService(logService, fileService)
                ...
    }
}

VSCode插件进程与API

  • VSCode中运行的每个插件,都会单独启动一个node子进程(child_process),

主进程会暴露大量API供插件进程调用,插件进程间由共享进程进行插件通信, 如果我们做VSCode插件开发,其实也就是通过组合调用各种VSCode主进程提供的API实现对应的功能

  • 至于具体有哪些API 可以查看src\vs\workbench\api\common\extHost.api.impl.ts 中的 function createApiFactoryAndRegisterActors 那是相当的多,光提供功能服务的service都多的....
ts 复制代码
export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): IExtensionApiFactory {

	// services
	const initData = accessor.get(IExtHostInitDataService);
	const extHostFileSystemInfo = accessor.get(IExtHostFileSystemInfo);
	const extHostConsumerFileSystem = accessor.get(IExtHostConsumerFileSystem);
	const extensionService = accessor.get(IExtHostExtensionService);
	const extHostWorkspace = accessor.get(IExtHostWorkspace);
	const extHostTelemetry = accessor.get(IExtHostTelemetry);
	const extHostConfiguration = accessor.get(IExtHostConfiguration);
	const uriTransformer = accessor.get(IURITransformerService);
	const rpcProtocol = accessor.get(IExtHostRpcService);
	const extHostStorage = accessor.get(IExtHostStorage);
	const extensionStoragePaths = accessor.get(IExtensionStoragePaths);
	const extHostLoggerService = accessor.get(ILoggerService);
	const extHostLogService = accessor.get(ILogService);
	const extHostTunnelService = accessor.get(IExtHostTunnelService);
	const extHostTelemetryLogService = accessor.get(IExtHostTelemetryLogService);
	const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService);
	const extHostWindow = accessor.get(IExtHostWindow);
	const extHostSecretState = accessor.get(IExtHostSecretState);
	const extHostEditorTabs = accessor.get(IExtHostEditorTabs);
        ...
 }
  • 打开任务管理器会发现我的VSCode居然有这么多子进程! 还是用node启动的(玩过node的小伙伴应该知道这玩意多占内存) 所以没用的插件尽量关掉把,咱的16G小内存条吃不住啊(看我这么可怜能不能把vx收款码放在掘金上大家可怜可怜我?)

后记

VSCode是一个复杂庞大的系统, 内部还有很多东西值得我们去探索,各种功能的实现原理, 优化方案, 代码的设计思路,架构方案等等。

本文只是挑选了一些外层基础的东西带大家入坑。

是一个不可多得的跨平台全栈项目, 非常推荐大家去看一下。毕竟也是我们每天都在使用的工具

真的很久没有在掘金出现过了, 技术圈动荡,裁员,搬家。搞得我最近也是有点疲惫。

最近跟群友吹水,找到了一点写技术文章的欲望。分享一下存货, 也希望2024年我能跳槽成功,不要再为生活奔波了。 哭泣

相关推荐
掘金安东尼3 分钟前
官方:什么是 Vite+?
前端·javascript·vue.js
柒崽4 分钟前
ios移动端浏览器,vh高度和页面实际高度不匹配的解决方案
前端
渣哥20 分钟前
你以为 Bean 只是 new 出来?Spring BeanFactory 背后的秘密让人惊讶
javascript·后端·面试
烛阴29 分钟前
为什么游戏开发者都爱 Lua?零基础快速上手指南
前端·lua
大猫会长38 分钟前
tailwindcss出现could not determine executable to run
前端·tailwindcss
Moonbit43 分钟前
MoonBit Pearls Vol.10:prettyprinter:使用函数组合解决结构化数据打印问题
前端·后端·程序员
533_1 小时前
[css] border 渐变
前端·css
云中雾丽1 小时前
flutter的dart语言和JavaScript的消息循环机制的异同
前端
地方地方1 小时前
Vue依赖注入:provide/inject 问题解析与最佳实践
前端·javascript·面试
云中雾丽1 小时前
dart的继承和消息循环机制
前端