怎么从VSCode看大型IDE技术架构

发布时间:2022-01-05 19:56:51 作者:iii
来源:亿速云 阅读:121

这篇文章主要介绍“怎么从VSCode看大型IDE技术架构”,在日常操作中,相信很多人在怎么从VSCode看大型IDE技术架构问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么从VSCode看大型IDE技术架构”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

一、VSCode 是什么

官方定义

https://code.visualstudio.com/

怎么从VSCode看大型IDE技术架构

关键词:

怎么从VSCode看大型IDE技术架构

怎么从VSCode看大型IDE技术架构

点评:

对于工具软件而言,需要内心能想清楚边界。哪些是自己应该专注去做的,哪些可以外溢到交给第三方扩展来满足。

发展历程

团队负责人:Erich Gamma . JUnit 作者之一,《设计模式》作者之一, Eclipse 架构师。2011 加入微软,在瑞士苏黎世组建团队开发基于 web 技术的编辑器,也就是后来的 monaco-editor。VSCode 开发团队从 10 来个人开始,早期成员大多有 Eclipse 开发团队的背景。

Visual Studio Code有哪些工程方面的亮点 维护一个大型开源项目是怎样的体验?「Shape Up」 适合中小团队的一种工作方式

怎么从VSCode看大型IDE技术架构

Erich Gamma 在 GOTO 2016 发表了主题为 《The journey of visual studio code: Building an App Using JS/TypeScript, Node, Electron & 100 OSS Components》的演讲,详细讲解了这个项目的发展历程:

没时间观看视频的可以下载完整 PDF

PPT 的第一页,就是 Erich Gamma 截取自己正式加入微软之后收到的工作内容描述的邮件:

”探索一种全新的和桌面 IDE 一样成功的在线开发工具模式“

怎么从VSCode看大型IDE技术架构

整个团队从大致 10 个人开始,混合老中新三代不同水平的程序员,在微软这个巨无霸的商业公司里面想要落地这样一个宏大的愿景是不容易的,团队一开始定下的思路就是像 start up 一样工作,每月每年都要 ship 东西。

同时他也提出早期会疯狂的在公司内部寻找落地场景,比如 Visual Studio Online 的在线 Code DIff 页面,TypeScript 的官网的 Playground 编辑器,OneDrive 代码文件,Edge Dev Tool 的代码浏览等。

一个重要转折点是微软本身发生的巨大变化:

怎么从VSCode看大型IDE技术架构

伴随微软整个的开放开源跨平台风潮,Erich Gamma 敏锐的决定将产品从 Browser Based IDE 转向跨平台的 Desktop IDE,但仍然使用 Web 技术,于是 electron 粉墨登场,VSCode 开源这些事情接连发生,VSCode 团队花了六个月使用 Electron 将 Web 编辑器桌面化,又花了六个月将整个 IDE 插件化。

怎么从VSCode看大型IDE技术架构

产品定位

Erich Gamma 在 2017 SpringOne Platform 上有一个 关于 VSCode 的分享,讲解了在他开发 Eclipse 的过往经验基础上,对 VSCode 进行顶层设计时的诸多思路与决策,其中提到过对于 VSCode 的产品定位:

怎么从VSCode看大型IDE技术架构

从图中可以看出 VSCode 定位是处于编辑器和 IDE 的中间并且偏向轻量编辑器一侧的。

VSCode 的核心是“编辑器 + 代码理解 + 调试“,围绕这个关键路径做深做透,其他东西非常克制,产品保持轻量与高性能。

点评:

生产力工具类的软件一定要守住主线,否则很可能会变成不收门票的游乐园牛逼的产品背后一定有牛逼的团队,比如微软挖到 Anders Hejlsberg,接连创造了 C# 和 TypeScript,挖到 Erich Gamma,接连诞生了 monaco 和 vscode 这些明珠

二、Electron 是什么

上文提到 VSCode 有一个特性是跨平台,它的跨平台实质是通过 electron 实现的。所以我们需要先简单了解下 electron

官方定义

怎么从VSCode看大型IDE技术架构

怎么从VSCode看大型IDE技术架构

核心技术

怎么从VSCode看大型IDE技术架构

应用架构

https://electronjs.org/docs/tutorial/application-architecture

怎么从VSCode看大型IDE技术架构

进程间通讯:

点评:

普通 web 页面无法调用 native api,因此缺少一些能力electron 的 web 页面所处的 Render 进程可以将任务转发至运行在 NodeJS 环境的 Main 进程,从而实现 native API这套架构大大扩展了 electron app 相比 web app 的能力丰富度,但同时又保留了 web 快捷流畅的开发体验,再加上 web 本身的跨平台优势,结合起来让 electron 成为性价比非常高的方案

三、VSCode 技术架构

多进程架构

怎么从VSCode看大型IDE技术架构

# 检出代码git clone git@github.com:microsoft/vscode.gitcd vscode# 安装依赖yarn# 启动 web 版yarn watch && yarn web# 启动 桌面 版yarn watch && ./scripts/code.sh# 打包yarn run gulp vscode-[platform]yarn run gulp vscode-[platform]-min# platforms: win32-ia32 | win32-x64 | darwin | linux-ia32 | linux-x64 | linux-arm

源码组织

https://github.com/microsoft/vscode/wiki/Source-Code-Organization

下面是整个 VSCode project 的一些顶级的重点文件夹,后文会重点关注 src 与 extensions:

├── build         # 构建脚本├── extensions    # 内置插件├── scripts       # 工具脚本├── out           # 产物目录├── src           # 源码目录├── test          # 测试代码

VSCode 的代码架构也是随着产品的阶段演进演进不断更迭的:

怎么从VSCode看大型IDE技术架构

下文会分享一些整个 VScode 源码组织的一些亮点与特色:

1. 隔离内核 (src) 与插件 (extensions),内核分层模块化
2. 每层按环境隔离

内核里面每一层代码都会遵守 electron 规范,按不同环境细分文件夹:

点评:

实际开发中也遇到了类似问题,作为一个 低代码 + 可视化 的研发平台,许多功能模块的实现都需要横跨编辑态和运行态,如果代码不加以限制和区分,很容易导致错误的依赖关系和预期之外的 bug,因此最终也决定采用 (editor/runtime/common) 类似的隔离架构

3. 内核代码本身也采用扩展机制: Contrib

可以看到 /src/vs/workbench/contrib 这个目录下存放着非常多的 VSCode 的小的功能单元:

├── backup├── callHierarchy├── cli├── codeActions├── codeEditor├── comments├── configExporter├── customEditor├── debug├── emmet├──....中间省略无数....├── watermark├── webview└── welcome

Contrib 有一些特点:

VSCode 开发团队做这个设计的目的我猜大概是因为重型的工具软件功能点实在太多,如果这些功能代码直接采用原始的模块引用的方式聚合瓶装起来,是一个自顶向下的架构,对维护性的挑战比较大。

而采用暴露扩展点的方式,可以将依赖关系反转,依附于扩展点协议,将核心功能和非核心功能分割开,独立的小功能的代码实现可以单独聚合,降低信息密度与提升维护性。

但是 VSCode Contrib 的具体业务代码组织其实看起来没有太多范式,而且这个内核代码的扩展机制 Contrib 和 VSCode 开放给外界的插件化机制 extension 是有差异的,读起来十分头疼。

通过和 CloudIDE 开发同学木农的交流,我得到两条主要差异性:

4. 依赖注入

上一小节提到了 VSCode 的代码大量使用了依赖注入,这项技术的具体实现细节本文不会展开细讲,感兴趣的可以阅读一些好的实现:

TS 依赖注入常见的实现原理是使用 reflect-metadata 设置与获取元信息,从而可以实现在运行时拿到本来属于编辑态的 TypeScript 类型相关元信息,具体来说就是下面这些 API:

不过具体到 VSCode 的依赖注入,它没有使用 reflect-metadata 这一套,而是基于 decorator 去标注元信息,整个实现了一套自己的依赖注入方式,具体可以参考vscode 源码解析-依赖注入 这篇文章,大致包含如下几类角色:

举个例子来看,在 /src/core/platform 里面定义了大量 service,其他地方消费者 Client 都可以用依赖注入的方式使用到,伪代码如下:

class Client {  // 构造函数参数注入(依赖注入方式的一种)  constructor(    // 必选    @IModelService modelService: IModelService,     // 可选    @optional(IEditorService) editorService: IEditorService  ) {    // use services  }}// 实例化instantiationService.createInstance(Client);
5. 绝对路径 import

怎么从VSCode看大型IDE技术架构

怎么从VSCode看大型IDE技术架构

点评:绝对路径 import 是一个非常值得学习的技巧,具体的方式是配置 TypeScript compilerOptions.paths

相对路径 import 对阅读者的大脑负担高,依赖当前文件位置上下文信息才能理解重构代码的时候移动文件位置,相对路径需要修改本文件的所有 import,绝对路径不需要

6. 命令系统

VSCode 和 monaco-editor 都有自己的命令系统,蚂蚁 CloudIDE 团队的同学也曾经对命令系统的优势做过总结:

传统的模块调用是个网状,不太好找到一个切面来理解或治理:

怎么从VSCode看大型IDE技术架构

而命令系统是中心化的,各功能末端变成了扁平化的结构:

怎么从VSCode看大型IDE技术架构

7. TypeScript

怎么从VSCode看大型IDE技术架构

怎么从VSCode看大型IDE技术架构

怎么从VSCode看大型IDE技术架构

启动流程 (TLDR)

上文初步了解了 vscode 的技术架构与源码组织,手痒的同学估计有点等不及尝试走一遍 vscode 的启动流程了。

然后在正式发车之前,我需要给大家一点友情提醒,如果你没耐心看完下面的 VSCode 的启动流程,应该知道,人生得过且过 o(╥﹏╥)o

总体来看,VSCode 的启动代码真正 show 给我们看了一个复杂的客户端软件的代码会工程化到什么地步,这其中掺杂了大量的基于 TypeScript 的 OOP 式的代码组织,各种对边界,宿主环境,上下文的处理,本来简单的启动 APP 渲染一个页面流程变得极其复杂。

下面精简抽取核心启动链路的文件和方法看一看:

// /src/main.js 的精简核心链路const { app, protocol } = require('electron');app.once('ready', function () {  // electron 启动好之后,调用 vscode 的入口    onReady();});async function onReady() {    // 获取缓存文件目录地址和语言配置,执行启动        const [cachedDataDir, nlsConfig] = await Promise.all([nodeCachedDataDir.ensureExists(), resolveNlsConfiguration()]);        startup(cachedDataDir, nlsConfig);}function startup(cachedDataDir, nlsConfig) {  // 先加载 vscode 自己开源的 AMD Loader https://github.com/Microsoft/vscode-loader/  // 再使用这个 loader 去加载 VSCode 的主入口文件    require('./bootstrap-amd').load('vs/code/electron-main/main');}
// /src/vs/code/electron-main/main.ts 精简核心链路// 初始化主类const code = new CodeMain();// 执行主入口函数code.main();class CodeMain {  main() {    // vscode 的 class public 入口一般只是空壳,真正的都在 private 逻辑里面    this.startUp();  }  private async startup() {    // 先创建依赖的初始化 service        const [instantiationService, instanceEnvironment] = this.createServices();    // 创建编辑器实例并调用 startUp 方法    return instantiationService.createInstance(CodeApplication).startup();  }}
// /src/vs/code/electron-main/app.tsexport class CodeApplication extends Disposable {    async startup(): Promise<void> {        // IPC Server        const electronIpcServer = new ElectronIPCServer();        // SharedProcess        const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv);        // 创建一大堆依赖的 service        // IUpdateService IWindowsMainService IDialogMainService IMenubarService IStorageMainService......        const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient);        // 打开一个窗口            const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));    }      private openFirstWindow() {      const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);            return windowsMainService.open();    }}
// /src/vs/platform/windows/electron-main/windowsMainService.tsexport class WindowsMainService extends Disposable implements IWindowsMainService {  open() {    // 执行 open    this.doOpen();  }  private doOpen() {    // 打开浏览器窗口    this.openInBrowserWindow();  }  private openInBrowserWindow() {            // 创建窗口            const createdWindow = window = this.instantiationService.createInstance(CodeWindow, {                state,                extensionDevelopmentPath: configuration.extensionDevelopmentPath,                isExtensionTestHost: !!configuration.extensionTestsPath            });  }    private doOpenInBrowserWindow() {        // 加载页面    window.load(configuration);  }}
// /src/vs/code/electron-main/window.tsexport class CodeWindow extends Disposable implements ICodeWindow {  load() {        // 调用 electron 的 api 加载一个 url 的 html 页面        this._win.loadURL(this.getUrl(configuration));  }    private getUrl() {    // 获取要打开的 url    let configUrl = this.doGetUrl(config);    return configUrl;  }    private doGetUrl(config: object): string {    // 终于看到 html 了!!泪流满面〒▽〒    // 打开 VSCode 的工作台,也就是 workbench         return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;    }}

代码编辑器技术

因为本文关注的重点并不在真正的代码编辑器技术而是在调研一下大型软件的工程化,因此本文只会简要介绍一下代码编辑相关的的一些核心技术:

怎么从VSCode看大型IDE技术架构

怎么从VSCode看大型IDE技术架构

怎么从VSCode看大型IDE技术架构

怎么从VSCode看大型IDE技术架构

怎么从VSCode看大型IDE技术架构

点评:

monaco-editor 可以看出专家级人物的领域积累,属于 VSCode 的核心竞争力language server protocol 和 Debug Adaptor Prototal 的设计就属于高屋建瓴,可以看出思想层次,把自己的东西做成标准,做出生态,开放共赢

四、VSCode 插件系统

理念差异

怎么从VSCode看大型IDE技术架构

对比几大 IDE:

VSCode 插件的强隔离

怎么从VSCode看大型IDE技术架构

vscode 有哪些扩展能力?https://code.visualstudio.com/api/extension-capabilities/overview

点评:

视图扩展的克制带来统一的视觉与交互风格,带来好的用户体验,便于建立稳定的用户心智插件独立进程,与视图隔离,保证整体软件的质量、性能、安全性

Workbench 视图结构

https://code.visualstudio.com/docs/getstarted/userinterface

怎么从VSCode看大型IDE技术架构

在这个视图结构里面有哪些可扩展呢?详见 extending workbench:

怎么从VSCode看大型IDE技术架构

插件 API 注入

插件开发者调用 core 能力时需要引入名为 vscode 的 npm 模块

import * as vscode from 'vscode';

而实际上这只是一个 vscode.d.ts 类型声明文件,它声明了所有插件可用的 API 类型。

这些 API 的具体实现在 src/vs/workbench/api/common/extHost.api.impl.ts createApiFactoryAndRegisterActors

那么具体这些 API 在 plugin 执行上下文是何时注入的呢?其实是在插件 import 语句执行的时候动了手脚。

// /src/vs/workbench/api/common/extHostRequireInterceptor.tsclass VSCodeNodeModuleFactory implements INodeModuleFactory {    public load(_request: string, parent: URI): any {        // get extension id from filename and api for extension        const ext = this._extensionPaths.findSubstr(parent.fsPath);        if (ext) {            let apiImpl = this._extApiImpl.get(ExtensionIdentifier.toKey(ext.identifier));            if (!apiImpl) {                apiImpl = this._apiFactory(ext, this._extensionRegistry, this._configProvider);                this._extApiImpl.set(ExtensionIdentifier.toKey(ext.identifier), apiImpl);            }            return apiImpl;        }        // fall back to a default implementation        if (!this._defaultApiImpl) {            let extensionPathsPretty = '';            this._extensionPaths.forEach((value, index) => extensionPathsPretty += `\t${index} -> ${value.identifier.value}\n`);            this._logService.warn(`Could not identify extension for 'vscode' require call from ${parent.fsPath}. These are the extension path mappings: \n${extensionPathsPretty}`);            this._defaultApiImpl = this._apiFactory(nullExtensionDescription, this._extensionRegistry, this._configProvider);        }        return this._defaultApiImpl;    }}

vscode plugin 的 require 全部被 Microsoft/vscode-loader 劫持了,通过对 require 的 hack 将插件 API 注入到了运行环境。

插件开发与配置

脚手架: https://github.com/Microsoft/vscode-generator-code

官方 demo: https://github.com/Microsoft/vscode-eslint

npm install -g yo generator-codeyo code

一个插件核心就是一个配置文件:Extension Manifest JSON (package.json 里面的一个字段)

https://code.visualstudio.com/api/references/extension-manifest

一些关键配置如下:

main:主文件入口,比如导出一个 activate 方法,可以接受 ctx 做一些事情
  1. "main": "./src/extension.js", 


// extension.jsconst vscode = require("vscode");function activate(context) {  console.log('Congratulations, your extension "helloworld" is now active!');  let disposable = vscode.commands.registerCommand(    "extension.helloWorld",    function() {      vscode.window.showInformationMessage("Hello World!");    }  );  context.subscriptions.push(disposable);}exports.activate = activate;
Activation Events 激活时机
Contribution Points 扩展点

到此,关于“怎么从VSCode看大型IDE技术架构”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

推荐阅读:
  1. 从运维角度看 JAVA 技术
  2. phper从st到vscode

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

vscode ide

上一篇:Java单例模式怎么理解

下一篇:linux的cut和sort怎么用

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》