前言
大家好这里是阳九. 一个菜鸡JS全栈码农。
今天来讲一下大家每天都在使用的代码编辑器VSCode,让大家对这款伴你一起成长的工具多一点了解
VSCode代码体系极其庞大,给想入坑代码编辑器的同学一点上手文章
注意: 本文的资料只是个人学习笔记, 里面大概率有错误, 如果有VSCode大佬发现我的资料有问题请给小弟指正!!!
之前我写了一款模拟VSCode架构个人代码编辑器, 有兴趣的小伙伴可以关注一下, 当然因为工作生活很久没有更新了, 有很多bug和功能不足, 只是拿来学习一下也是不错的。
lzy-code-editor: 模仿VSCode原理与架构实现的简易版桌面端代码编辑器 (github.com)
先上大图
- 还是跟原来一样 先放上一张大图, 当然这里也省略了很多不重要(其实是我没研究过)的模块
- 之后我们分模块给大家一步步介绍
前置知识
Electron
-
VSCode基于Electron框架进行开发, Electron是一个微软家的开源框架,用于构建跨平台的桌面应用程序。同样基于Node平台,是不可多得的TS全栈+跨平台学习项目(本人墙裂推荐)
-
Electron同时提供了两个进程, 渲染进程和主进程
- 主进程: 运行在Node环境下的VSCode后台服务。VSCode在这里使用微服务架构,创建了众多Services单例并提供一系模块管理,与操作系统和底层硬件之间交互的任务。
- 渲染进程: 每个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 中,内部自定义协议的使用有以下几个好处(
没错就是复制粘贴的
)
- 安全性:由于其是内部自定义协议,因此能够有效地防止反向代理等网络攻击方式产生潜在安全风险
- 高效性:相对于基于常规 HTTP 协议的接口,在 WebSocket 等协议上封装数据进行传输时具备更佳的通信速度和实时响应
- 扩展性:VSCode 可以根据需要扩展并定制化自己特定场景下所需的 API 接口与相关功能实现,并通过此类机制将其直接暴露到应用程序中。
- 功能丰富 : 自定义 vscode: 协议可使 VS Code 帮助各种IDE任务如文件系统调用( vscode:file:// ), Git 操作监视(vscode-gitlens) ,Web 页面渲染预览(vscode-markdown), 虚拟终端(vscode-terminal)等高级交互指令
IPC通信模块
- VSCode在Electron的IPC模块基础上进一步封装, 构建了两个类
IPCServer
和IPCClient
,具体代码封装细节可以在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年我能跳槽成功,不要再为生活奔波了。 哭泣