图解 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年我能跳槽成功,不要再为生活奔波了。 哭泣

相关推荐
阿伟来咯~19 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端24 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱27 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai36 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨37 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js