Microsoft によって発売された最新のエディタとして、vscode は GitHub でオープンソース化されています。 vscode を使用したことのある人は、非常に高速であると述べています。フラッシュ セールは、Electron アーキテクチャを使用する Atom です。今回は、vscode が高速かつ効率的である理由をソースコード レベルから分析します。
Electron は、Node.js と Chromium に基づくクロスプラットフォームのデスクトップ アプリケーション開発フレームワークです。 JavaScipt、HTML、CSS を使用すると、Node.js がフロントエンドに実際に導入されます。 Electron は、BrowserWindow を通じてローカル ウィンドウを作成し、HTML ドキュメントをロードできます。BrowserWindow のコンテンツは、DOM 要素を作成できるだけでなく、任意の Node モジュールを使用することもでき、IPC を通じてメイン プロセスと通信することもできます。
各 Electron アプリケーションはメイン プロセス (メイン プロセス) に対応し、メイン プロセスによって BrowserWindow を通じて作成される各ローカル ウィンドウはレンダラー プロセス (レンダラー プロセス) に対応します。
vscode のメインプロセスは、主にウィンドウとメニューの作成、ライフサイクル管理、自動更新、その他のシステム関連機能を担当します。
コードの大部分はレンダリング プロセスで実行され、レンダリング プロセスはインターフェイスを表示し、ユーザー操作に応答します。前述したように、Node モジュールはブラウザでも使用できます。レンダリング プロセスでは、プラグインの初期化を担当する Node を介してプラグイン サブプロセスも作成されます。さらに、レンダリング プロセスでは、マークダウン解析などの複雑な計算を実行するワーカーを作成することもできます。
各レンダリング プロセスもプラグイン プロセスに対応しており、別のプロセスでプラグインを実行しても、vscode の方が高速になるのはこのためです。アトムよりも。 Atom のプラグインはレンダリング プロセスで直接実行されるため、プラグインが多数あるとフリーズします。一方、vscode プラグインは通常の Node プロセスで実行されるため、UI を操作する機能は比較的弱く、Atom ほどではありません。
VSCode Loader は、RequireJS に似た非同期読み込みモジュール (AMD) です。すべての TypeScript ソース コードは、AMD 仕様を使用して js ファイルにコンパイルされ、使用時にこのローダーを通じてロードされます。
メインプロセス (Node プロセス) は CommonJS 仕様を使用しますが、ブラウザーでのコードの読み込みは非同期であるため、AMD の使用は議論の余地はありません。 vscode の一部のコア コードと基本ライブラリは TypeScript で書かれており、AMD 標準の js にもコンパイルされます。これらの基本コードはメイン プロセスでも使用されるため、このローダーもメイン プロセスで使用されます。同様に、プラグイン プロセスとワーカーの両方がこのローダーを使用してコードを読み込みます。
VSCode Loader は、RequireJS と同様のモジュール読み込み機能を実装するだけでなく、CSS (css.js) やドキュメントを読み込み、多言語を実装できるいくつかのプラグインが付属しています。
vscode のメイン ディレクトリ構造は次のとおりです。
├── build // gulp打包编译相关脚本├── node_modules // 依赖模块├── src // 源代码和素材(ts,js,css,svg,html等)│ ├── typings // 常用模块定义│ ├── vs│ │ ├── base // 核心模块,常用库和基本组件│ │ ├── editor // 编辑器模块│ │ ├── languages // 默认编辑器语言支持│ │ ├── platform // 核心功能接口定义和基本实现│ │ ├── workbench // 业务逻辑功能实现│ │ ├── loader.js // vscode loader│ │ └── vscode.d.ts // 插件API定义│ └── main.js // 主进程入口├── gulpfile.js // gulp打包编译入口├── product.json // 产品描述文件└── package.json
ベース パッケージには、多数の API がカプセル化されています。共通の機能を実装します。 vscodeではディレクトリ構造がブラウザ、common、node、electronごとに分かれています。
ブラウザ
ボタン、チェックボックス、リスト、スクロールバー、その他の一般的なコンポーネントを含む、単純な UI ライブラリがブラウザに実装されています。また、JQuery に似た一連の DOM 操作 API をカプセル化します (dom.ts および builder.ts を参照)。
common
common パッケージは、多数のユーティリティ クラスをカプセル化します。たとえば、
他にも多くのツールクラスがあり、各モジュールの結合度は非常に低く、基本的には独立して使用でき、学習は簡単です。ここでは一つ一つ紹介しません。
ノード
ノード パッケージは、ノードによって実装されるいくつかの機能をカプセル化します。たとえば、
パーツ
このパッケージは、さらに、いくつかの複雑な UI コンポーネント、ツリーおよびクイックオープンを定義します。
この記事では主に vscode の基本フレームワークの構造を理解します。これら 2 つのパッケージはエディター機能の主要な実装として機能します。ここでのロジックは複雑すぎるため、説明します。詳細には立ち入らないでください。
基本的に、vscode の特定の関数実装コードはすべて、これら 2 つのパッケージに含まれています。プラットフォームは主にいくつかのサービス インターフェイスと単純な実装を定義しますが、ワークベンチはこれらのインターフェイスを実装し、ワークベンチを作成し、完全なインターフェイス構造を構築します。
下面从程序入口开始,从源码一步一步来看vscode是怎样运行起来的。
Eletron通过 package.json 中的 main字段 来定义应用入口。 main.js 是vscode的入口。
这个模块是一个壳,主要解析多语言配置,然后初始化loader,通过loader加载 main.ts 。
// Load our code once readyapp.once('ready', function () { var nlsConfig = getNLSConfiguration(); process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig); require('./bootstrap-amd').bootstrap('vs/workbench/electron-main/main');});
这里的 bootstrap-amd.js 负责创建一个loader,实现异步加载。
loader.config({ baseUrl: uriFromPath(path.join(__dirname)), catchError: true, nodeRequire: require, nodeMain: __filename, 'vs/nls': nlsConfig});......exports.bootstrap = function (entrypoint) { if (!entrypoint) { return; } loader([entrypoint], function () { }, function (err) { console.error(err); });};
在 main.ts 中依赖一个 env 的模块
import env = require('vs/workbench/electron-main/env');
该模块负责命令行参数的解析,以及读取 package.json 和 product.json 保存软件的一些基本信息,主要变量如下:
// 是否是发行版export const isBuilt = !process.env.VSCODE_DEV;// 应用程序根目录export const appRoot = path.dirname(uri.parse(require.toUrl('')).fsPath);// 产品配置export const product: IProductConfiguration = productContents;// 程序版本export const version = app.getVersion();// 命令行参数export const cliArgs = parseCli();// 数据文件目录export const appHome = app.getPath('userData');// setting文件路径export const appSettingsPath = path.join(appSettingsHome, 'settings.json');// keybindings文件路径export const appKeybindingsPath = path.join(appSettingsHome, 'keybindings.json');// 用户插件目录export const userExtensionsHome = cliArgs.extensionsHomePath || path.join(userHome, 'extensions');
在main.ts的main方法中,初始化了主进程中的各个管理器
// Lifecyclelifecycle.manager.ready();// Load settingssettings.manager.loadSync();// Propagate to clientswindows.manager.ready(userEnv);// Install Menumenu.manager.ready();.....// Setup auto updateUpdateManager.initialize();
以上管理器中大量使用了Eletron的 ipc 模块发送接收渲染进程的消息,来实现主进程和渲染进程的交互。
在main.ts的main方法的最后
// Open our first windowif (env.cliArgs.openNewWindow && env.cliArgs.pathArguments.length === 0) { windows.manager.open({ cli: env.cliArgs, forceNewWindow: true, forceEmpty: true }); // new window if "-n" was used without paths} else if (global.macOpenFiles && global.macOpenFiles.length && (!env.cliArgs.pathArguments || !env.cliArgs.pathArguments.length)) { windows.manager.open({ cli: env.cliArgs, pathsToOpen: global.macOpenFiles }); // mac: open-file event received on startup} else { windows.manager.open({ cli: env.cliArgs, forceNewWindow: env.cliArgs.openNewWindow, diffMode: env.cliArgs.diffMode }); // default: read paths from cli}
调用了 windows 模块的 open 方法打开了第一个窗口。这里调用了 env.cliArgs 获取命令行参数传递给 open 方法来实现不同的打开方式。
在 open 方法中创建一个了 VSCodeWindow 实例,并且通过 toConfiguration 方法创建了一个 IWindowConfiguration 的对象。
IWindowConfiguration 中定义了大量的 env 中的信息,包括环境变量,命令行参数,软件信息等。在之后 IWindowConfiguration 会作为参数传递给 VSCodeWindow 的 load 方法。
VSCodeWindow 包装了一个 BrowserWindow 对象。 load 方法调用 getUrl 加载了一个的 html文件 。
private getUrl(config: IWindowConfiguration): string { let url = require.toUrl('vs/workbench/electron-browser/index.html'); // Config url += '?config=' + encodeURIComponent(JSON.stringify(config)); return url;}
可以看到 IWindowConfiguration 被序列化成字符串作为参数传递给了 index.html 。由于在浏览器进程要获取主进程中 env 模块的数据比较复杂(需要使用 ipc 通讯)。所以这里直接将一些基本信息打包成config传递给了浏览器进程。这时浏览器窗口才正式打开并初始化。
浏览器的入口在 index.html 中。与主进程类似这里也对loader进行了初始化并加载浏览器主模块 main 。主要代码如下:
// 解析config参数var args = parseURLQueryArgs();var configuration = JSON.parse(args['config']);......// loader的加载根目录var rootUrl = uriFromPath(configuration.appRoot) + '/out';// 加载loadercreateScript(rootUrl + '/vs/loader.js', function() { // 多语言配置 var nlsConfig; try { var config = process.env['VSCODE_NLS_CONFIG']; if (config) { nlsConfig = JSON.parse(config); } } catch (e) { } if (!nlsConfig) { nlsConfig = { availableLanguages: {} }; } // 配置loader require.config({ baseUrl: rootUrl, 'vs/nls': nlsConfig, recordStats: configuration.enablePerformance }); ...... require([ // 项目正式发布后大多数的js都被合并进了workbench.main.js中 'vs/workbench/workbench.main', 'vs/nls!vs/workbench/workbench.main', 'vs/css!vs/workbench/workbench.main' ], function() { timers.afterLoad = new Date(); // 浏览器主模块 var main = require('vs/workbench/electron-browser/main'); // config作为参数,调用startup启动主模块 main.startup(configuration, globalSettings).then(function() { mainStarted = true; }, function(error) { onError(error, enableDeveloperTools) }); });});
在 main 模块的 startup 方法中进一步加工 config ,并创建了一个 workspace 。
export function startup(environment: IMainEnvironment, globalSettings: IGlobalSettings): winjs.TPromise<void> { // 将主进程中的环境变量合并到浏览器进程 assign(process.env, environment.userEnv); // Shell Configuration let shellConfiguration: IConfiguration = { env: environment }; ...... let shellOptions: IOptions = { ...... }; ...... // Open workbench return openWorkbench(getWorkspace(environment), shellConfiguration, shellOptions);}function getWorkspace(environment: IMainEnvironment): IWorkspace { if (!environment.workspacePath) { return null; } ...... let workspace: IWorkspace = { 'resource': workspaceResource, 'id': platform.isLinux ? realWorkspacePath : realWorkspacePath.toLowerCase(), 'name': folderName, 'uid': platform.isLinux ? folderStat.ino : folderStat.birthtime.getTime(), 'mtime': folderStat.mtime.getTime() }; return workspace;}
这里的 environment 就是上文的 config 。 IWorkspace 记录了当前打开的文件夹路径等信息(当打开单文件时 IWorkspace 不存在)。
function openWorkbench(workspace: IWorkspace, configuration: IConfiguration, options: IOptions): winjs.TPromise<void> { let eventService = new EventService(); let contextService = new WorkspaceContextService(eventService, workspace, configuration, options); let configurationService = new ConfigurationService(contextService, eventService); return configurationService.initialize().then(() => { ...... let shell = new WorkbenchShell(document.body, workspace, { configurationService, eventService, contextService }, configuration, options); shell.open(); ...... });}
在 openWorkbench 创建了三个基本服务(Service),并将 config , workspace 等参数传给 WorkbenchShell 。 WorkbenchShell 获取html文档的 body 节点准备创建界面。
WorkbenchShell 主要负责初始化各服务(Service),并创建了一个 Workbench 完成界面的初始化工作。
常用的Service比如
initInstantiationService 方法中创建了各个服务,并返回 IInstantiationService 。
在vscode中随处可见 IInstantiationService 的应用。以 CloseWindowAction 为例
export class CloseWindowAction extends Action { public static ID = 'workbench.action.closeWindow'; public static LABEL = nls.localize('closeWindow', "Close Window"); constructor(id: string, label: string, @IWindowService private windowService: IWindowService) { super(CloseWindowAction.ID, label); } public run(): TPromise<boolean> { this.windowService.getWindow().close(); return TPromise.as(true); }}
在构造函数( constructor )中,后面的参数写法比较特殊
@IWindowService private windowService: IWindowService
使用了 @IWindowService 这种decorate语法。当要创建 CloseWindowAction 这个实例时,可以使用 IInstantiationService 只需要传入前两个参数,在 IInstantiationService 中能获取所有的其他服务对象, windowService 这个参数由 IInstantiationService 传入。
this.instantiationService.createInstance(CloseWindowAction, CloseWindowAction.ID, CloseWindowAction.LABEL);
WorkbenchShell 的 createContents 方法还创建了一个 Workbench 负责整个界面的创建。
private createContents(parent: Builder): Builder { ...... // Instantiation service with services let instantiationService = this.initInstantiationService(); ...... // Workbench this.workbench = new Workbench(workbenchContainer.getHTMLElement(), this.workspace, this.configuration, this.options, instantiationService); this.workbench.startup({ onServicesCreated: () => { this.initExtensionSystem(); }, onWorkbenchStarted: () => { this.onWorkbenchStarted(); } }); ......}
Workbench 是 IPartService 的具体实现。vscode由多个Part组成。
下面的代码展示了各个part的创建,并添加到显示列表。
private renderWorkbench(): void { ...... // Create Parts this.createActivityBarPart(); this.createSidebarPart(); this.createEditorPart(); this.createPanelPart(); this.createStatusbarPart(); // Create QuickOpen this.createQuickOpen(); // Add Workbench to DOM this.workbenchContainer.build(this.container);}
vscode中几乎每个部分都是可扩展的。例如最常见的有快捷键命令的注册,编辑器类型的扩展,扩展输出面板Channel。下面以 ViewletRegistry 为例,分析 activitybar 和 sidebar 上面的 Explore 文件浏览器是如何显示的。
通常情况下以 .contribution 结尾的模块,都用作扩展点的注册。由于一般情况下这些模块不会被其他模块依赖,所以要提供一个入口来加载这些模块,这个入口就是 workbench.main 。
其中 Explore 文件浏览器的注册是在 files.contribution 中定义的。
// Register Viewlet(<ViewletRegistry>Registry.as(ViewletExtensions.Viewlets)).registerViewlet(new ViewletDescriptor( 'vs/workbench/parts/files/browser/explorerViewlet', 'ExplorerViewlet', VIEWLET_ID, nls.localize('explore', "Explorer"), 'explore', 0));(<ViewletRegistry>Registry.as(ViewletExtensions.Viewlets)).setDefaultViewletId(VIEWLET_ID);
explorerViewlet 模块是 Explore 的界面显示入口。
platform 中定义了 IRegistry 接口及实现。
export interface IRegistry { /** * Adds the extension functions and properties defined by data to the * platform. The provided id must be unique. * @param id a unique identifier * @param data a contribution */ add(id: string, data: any): void; /** * Returns true iff there is an extension with the provided id. * @param id an extension idenifier */ knows(id: string): boolean; /** * Returns the extension functions and properties defined by the specified key or null. * @param id an extension idenifier */ as(id: string): any; as<T>(id: string): T;}
add 添加一个注册点, as 方法获取一个注册点对象。
viewlet 模块添加了 ViewletRegistry 。
Registry.add(Extensions.Viewlets, new ViewletRegistry());
之后可以通过 Registry.as(Extensions.Viewlets) 获取 ViewletRegistry 注册不同的 Viewlet 。
所有的注册信息储存在 ViewletRegistry 中,使用时通过 getViewlet 或者 getViewlets 方法获取。 activitybarPart 实现了注册点的读取,并填充 ActionBar ,显示出来。
private createViewletSwitcher(div: Builder): void { // Viewlet switcher is on top this.viewletSwitcherBar = new ActionBar(div, { ....... }); this.viewletSwitcherBar.getContainer().addClass('position-top'); // Build Viewlet Actions in correct order let activeViewlet = this.viewletService.getActiveViewlet(); let registry = (<ViewletRegistry>Registry.as(ViewletExtensions.Viewlets)); let viewletActions: Action[] = registry.getViewlets() // 获取注册的viewlets .sort((v1: ViewletDescriptor, v2: ViewletDescriptor) => v1.order - v2.order) .map((viewlet: ViewletDescriptor) => { let action = this.instantiationService.createInstance(ViewletActivityAction, viewlet.id + '.activity-bar-action', viewlet); ...... return action; }); // Add to viewlet switcher this.viewletSwitcherBar.push(viewletActions, { label: true, icon: true });}
类似的这种扩展点还有很多,如:
这种通过注册扩展点的架构方式,使得vscode整体变得很容易扩展。
vscode整体架构给人一种很清晰明了的感觉。多进程从主进程到浏览器,从浏览器到插件系统,服务驱动,可扩展的结构。
另外无论是UI组件还是工具和加载器都是自身实现的,没有借助第三方模块,使得耦合性和性能都得到了很好的保障。这也是vscode速度比Atom快的原因。
尽管扩展vscode自身是很容易的,但是目前vscode开放的插件接口还是极其有限。由于为了保证渲染进程的安全和速度,插件是一个单独的Node进程,插件进程无法创建UI,这一点使得vscode的插件开放没有Atom灵活,很多需要借助UI的插件功能也无法实现。
微软大法好