Visual Studio Code(如下简称VSCode)是一个轻量且强大的跨平台开源代码编辑器(IDE),VSCode 采用了 Electron,使用的代码编辑器名为 Monaco、Monaco 也是 Visual Studio Team Service(Visual Studio Online)使用的代码编辑器,在语言上,VSCode 使用了自家的 TypeScript 语言开发。javascript
VSCode提供了强大的插件拓展机制,并提供 插件市场 供开发者发布、下载插件。VSCode提供了丰富的扩展能力模型,例如基础的语法高亮/API提示、引用跳转(转到定义)、文件搜索、主题定制,高级的debug协议等等。但不容许插件直接访问底层UI DOM(即很难定制VSCode外观),由于VSCode开发团队随着优化VSCode而频繁更改UI Dom,因此将UI定制能力限制起来。css
可是当你想要开发一款专用IDE时,不想从零开始撸,而是站在巨人的肩膀上作二次开发的话,那么VSCode将是你不二的选择,像 Weex Studio、白鹭Egret Wing、快应用IDE等IDE,都是基于VSCode扩展加强。html
本系列文章将带你了解VSCode源码的总体架构和定制方法,一步一步从源码入手,定制一款专用开发工具。前端
学习VSCode源码的同窗基本上都是作前端工做的,那么node.js和javascript都是基本功了,这里不用过度强调了。可是在阅读VSCode源码以前,仍是须要对VSCode使用相关技术框架有所了解。java
众所周知,VSCode是一款桌面编辑器应用,可是前端单纯用js是作不了桌面应用的,因此采用Electron来构建。Electron是基于 Chromium 和 Node.js,使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用,它兼容 Mac、Windows 和 Linux,能够构建出三个平台的应用程序。node
从实现上来看,Electron = Node.js + Chromium + Native APIpython
也就是说Electron拥有Node运行环境,赋予了用户与系统底层进行交互的能力,依靠Chromium提供基于Web技术(HTML、CSS、JS)的界面交互支持,另外还具备一些平台特性,好比桌面通知等。linux
从API设计上来看,Electron App通常都有1个Main Process和多个Renderer Process:git
在Electron应用中,经过执行package.json中的main字段所指向的文件,能够开启electron的主进程(main process)。在主进程中使用BrowserWindow 实例建立web页面,并且一个electron应用有且只能有一个主进程。
主进程通常用于:github
因为electron使用Chromium来展现web页面,Chromium多进程架构也会被用到。每一张web页面都运行在它本身的进程里,该进程称为渲染进程(renderer process)。渲染进程通常负责界面交互相关的,具体的业务功能。
在web页面里,调用系统底层的API是不被容许的,这是由于在web页面上处理底层GUI资源是很是危险的,很容易致使资源泄漏。若是你想要在web页面上执行GUI操做,相应web页面的渲染进程必须与主进程进行通讯,向主进程发起请求去执行那些操做.在electron中,有几种主进程与渲染进程通讯的方法,好比用ipcRenderer和ipcMain模块来发送信息,还有RPC风格的远程通讯模块。关于Electron进程间通信,这里不作过多的介绍,能够看Electron官网和网上资料来学习主进程和渲染进程间通信。
更多的了解能够参考Electron应用架构
微软以前有个项目叫作Monaco Workbench,后来这个项目变成了VSCode,而Monaco Editor(下文简称monaco)就是从这个项目中成长出来的一个web编辑器,他们很大一部分的代码(monaco-editor-core)都是共用的,因此monaco和VSCode在编辑代码,交互以及UI上几乎是一摸同样的,有点不一样的是,二者的平台不同,monaco基于浏览器,而VSCode基于electron,因此功能上VSCode更加健全,而且性能比较强大。
TypeScript是JavaScript类型的超集,它能够编译成纯JavaScript。TypeScript能够在任何浏览器、任何计算机和任何操做系统中运行,而且是开源的。TypeScript具备如下特色:
VSCode源码的编写主要用TypeScript,因此学习VSCode源码的时候仍是先对TypeScript的基本使用有所了解。
以上内容是学习VSCode源码所要了解的基本内容,能够先学习Electron作个简单的桌面应用,而后学习一下TypeScript的基本语法,就能够开始VSCode源码的学习。
VSCode中包含主进程,渲染进程,同时由于VSCode提供了插件的扩展能力,又出于安全稳定性的考虑,图中又多了一个Extension Host,其实这个Extension Host也是一个独立的进程,用于运行咱们的插件代码。而且同渲染进程同样,彼此都是独立互不影响的。Extension Host Process暴露了一些VSCode的API供插件开发者去使用。
VSCode采用多进程架构,启动后主要由下面几个进程:
编辑器窗口 - 由后台进程启动,也是多进程架构
HTML编写的UI
Nodejs异步IO
插件宿主进程
插件实例
后台进程是 VSCode 的入口,主要负责管理编辑器生命周期,进程间通讯,自动更新,菜单管理等。
咱们启动 VSCode 的时候,后台进程会首先启动,读取各类配置信息和历史记录,而后将这些信息和主窗口 UI 的 HTML 主文件路径整合成一个 URL,启动一个浏览器窗口来显示编辑器的 UI。后台进程会一直关注 UI 进程的状态,当全部 UI 进程被关闭的时候,整个编辑器退出。
此外后台进程还会开启一个本地的 Socket,当有新的 VSCode 进程启动的时候,会尝试链接这个 Socket,并将启动的参数信息传递给它,由已经存在的 VSCode 来执行相关的动做,这样可以保证 VSCode 的惟一性,避免出现多开文件夹带来的问题。
编辑器窗口进程负责整个 UI 的展现。也就是咱们所见的部分。UI 所有用 HTML 编写没有太多须要介绍的部分。
项目文件的读取和保存由主进程的 NodeJS API 完成,由于所有是异步操做,即使有比较大的文件,也不会对 UI 形成阻塞。IO 跟 UI 在一个进程,并采用异步操做,在保证 IO 性能的基础上也保证了 UI 的响应速度。
每个 UI 窗口会启动一个 NodeJS 子进程做为插件的宿主进程。全部的插件会共同运行在这个进程中。这样设计最主要的目的就是避免复杂的插件系统阻塞 UI 的响应。可是将插件放在一个单独进程也有很明显的缺点,由于是一个单独的进程,而不是 UI 进程,因此没有办法直接访问 DOM 树,想要实时高效的改变 UI 变得很难,在 VSCode 的扩展体系中几乎没有对 UI 进行扩展的 API。
Debugger 插件跟普通的插件有一点区别,它不运行在插件进程中,而是在每次 debug 的时候由UI单独新开一个进程。
搜索是一个十分耗时的任务,VSCode 也使用的单独的进程来实现这个功能,保证主窗口的效率。将耗时的任务分到多个进程中,有效的保证了主进程的响应速度。
以上环境安装相信你们都轻车熟路了,因为我电脑使用的Mac,因此相关的示例都在Mac系统中运行,windows上的大同小异,具体能够参考官网Wiki文档。
VSCode的源码每次更新都会优化UI部分,但总体架构是没有差异的,可能网上的关于VSCode的源码教程用的老版本的VSCode,在这里我采用目前最新的版本 - v1.39.2版原本讲解。
源码下载:VSCode Releases
下载后解压用VSCode编辑器打开,在命令行中输入 yarn 命令来安装依赖,中间会很耗时。
中间会安装不少依赖包,若是发现网络不通、下载失败等状况,首先须要检查上述开发环境版本是否正确,必要时须要科学上网。
依赖安装完成后,进入到项目中,执行 yarn watch 执行构建工做:
直到你看到 Finished compilation with 0 errors after 108726 ms 输出,说明构建成功了!
这时候不要关闭当前命令行,构建命令没有退出,它会监视vscode源码文件的变化,若是有变化,它会立刻执行增量的构建,实时反映源码变化的结果。
新起一个命令行,执行 ./scripts/code.sh ,windows下执行 scriptscode.bat,此时会下载Electron。
下载完成后,便可运行。运行界面以下:
总体文件目录结构以下所示:
├── build # gulp编译构建脚本 ├── extensions # 内置插件 ├── gulpfile.js # gulp task ├── out # 编译输出目录 ├── resources # 平台相关静态资源,图标等 ├── scripts # 工具脚本,开发/测试 ├── src # 源码目录 ├── test # 测试套件 └── product.json # App meta信息
src下文件目录结构,以下图:
├── bootstrap-amd.js # 子进程实际入口 ├── bootstrap-fork.js # ├── bootstrap-window.js # ├── bootstrap.js # 子进程环境初始化 ├── buildfile.js # 构建config ├── cli.js # CLI入口 ├── main.js # 主进程入口 ├── paths.js # AppDataPath与DefaultUserDataPath ├── typings │ └── xxx.d.ts # ts类型声明 └── vs ├── base # 定义基础的工具方法和基础的 DOM UI 控件 │ ├── browser # 基础UI组件,DOM操做、交互事件、DnD等 │ ├── common # diff描述,markdown解析器,worker协议,各类工具函数 │ ├── node # Node工具函数 │ ├── parts # IPC协议(Electron、Node),quickopen、tree组件 │ ├── test # base单测用例 │ └── worker # Worker factory 和 main Worker(运行IDE Core:Monaco) ├── code # VSCode Electron 应用的入口,包括 Electron 的主进程脚本入口 │ ├── electron-browser # 须要 Electron 渲染器处理API的源代码(可使用 common, browser, node) │ ├── electron-main # 须要Electron主进程API的源代码(可使用 common, node) │ ├── node # 须要Electron主进程API的源代码(可使用 common, node) │ ├── test │ └── code.main.ts ├── editor # Monaco Editor 代码编辑器:其中包含单独打包发布的 Monaco Editor 和只能在 VSCode 的使用的部分 │ ├── browser # 代码编辑器核心 │ ├── common # 代码编辑器核心 │ ├── contrib # vscode 与独立 IDE共享的代码 │ ├── standalone # 独立 IDE 独有的代码 │ ├── test │ ├── editor.all.ts │ ├── editor.api.ts │ ├── editor.main.ts │ └── editor.worker.ts ├── platform # 依赖注入的实现和 VSCode 使用的基础服务 Services ├── workbench # VSCode 桌面应用程序工做台的实现 ├── buildunit.json ├── css.build.js # 用于插件构建的CSS loader ├── css.js # CSS loader ├── loader.js # AMD loader(用于异步加载AMD模块,相似于require.js) ├── nls.build.js # 用于插件构建的 NLS loader └── nls.js # NLS(National Language Support)多语言loader
首先 VSCode 总体由其核心core和内置的扩展Extensions组成,core是实现了基本的代码编辑器和 VSCode 桌面应用程序,即 VSCode workbench,同时提供扩展 API,容许内置的扩展和第三方开发的扩展程序来扩展 VSCode Core 的能力。
其次,因为 VSCode 依赖 Electron,而 Electron 存在着主进程和渲染进程,它们能使用的 API 有所不到,因此 VSCode Core 中每一个目录的组织也按照它们能使用的 API 来组织安排。在 Core 下的每一个子目录下,按照代码所运行的目标环境分为如下几类:
按照上述规则,即src/vs/workbench/browser中的源代码只能使用基本的 JavaScript API 和浏览器提供的 API,而src/vs/workbench/electron-browser中的源代码则可使用 JavaScript API,浏览器提供的 API、Node.js提供的 API、和 Electron 渲染进程中的 API。
在 VSCode 代码仓库中,出了上述的src/vs的Core以外,还有一大块即 VSCode 内置的扩展,它们源代码位于extensions内。
VSCode 做为代码编辑器,与各类代码编辑的功能如语法高亮、补全提示、验证等都有扩展实现的。因此在 VSCode 的内置扩展内,一大部分都是各类编程语言的支持扩展,如:extensionshtml、extensionsjavascript、extensionscpp等等,大部分语言扩展中都会出现如.tmTheme、.tmLanguage等 TextMate 的语法定义。还有一类内置的扩展是 VSCode 主体扩展,如 VSCode 默认主体extensions/theme-defaults等。
因为VSCode是基于Electron开发的,Electron的启动入口在package.json中,其中的 main 字段所表示的脚本为应用的启动脚本,它将会在主进程中执行。
./out/main.js显然这就是主进程的入口程序,可是main.js是在out文件夹下,很明显是编译输出出来的,而后找到src下tsconfig.json文件中有如下配置:
"outDir": "../out",
因此很明显是将src下代码编译后输出到out文件夹中。因此真实入口在src下main.js中,接下来只需从main.js文件分析便可。
在main.js中,咱们能够看到下面一行引入
const app = require('electron').app;
electron.app负责管理Electron 应用程序的生命周期,运行在主进程中,而后找到 ready 监听事件
// Load our code once ready app.once('ready', function () { if (args['trace']) { // @ts-ignore const contentTracing = require('electron').contentTracing; const traceOptions = { categoryFilter: args['trace-category-filter'] || '*', traceOptions: args['trace-options'] || 'record-until-full,enable-sampling' }; contentTracing.startRecording(traceOptions, () => onReady()); } else { onReady(); } });
这个ready监听表示,Electron 会在初始化后并准备,部分 API 在 ready 事件触发后才能使用。建立窗口也须要在ready后建立。最后这个函数中调用 onReady() 函数。
function onReady() { perf.mark('main:appReady'); Promise.all([nodeCachedDataDir.ensureExists(), userDefinedLocale]).then(([cachedDataDir, locale]) => { if (locale && !nlsConfiguration) { nlsConfiguration = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale); } if (!nlsConfiguration) { nlsConfiguration = Promise.resolve(undefined); } // First, we need to test a user defined locale. If it fails we try the app locale. // If that fails we fall back to English. nlsConfiguration.then(nlsConfig => { const startup = nlsConfig => { nlsConfig._languagePackSupport = true; process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig); process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || ''; // Load main in AMD perf.mark('willLoadMainBundle'); require('./bootstrap-amd').load('vs/code/electron-main/main', () => { perf.mark('didLoadMainBundle'); }); }; // We received a valid nlsConfig from a user defined locale if (nlsConfig) { startup(nlsConfig); } // Try to use the app locale. Please note that the app locale is only // valid after we have received the app ready event. This is why the // code is here. else { let appLocale = app.getLocale(); if (!appLocale) { startup({ locale: 'en', availableLanguages: {} }); } else { // See above the comment about the loader and case sensitiviness appLocale = appLocale.toLowerCase(); lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale).then(nlsConfig => { if (!nlsConfig) { nlsConfig = { locale: appLocale, availableLanguages: {} }; } startup(nlsConfig); }); } } }); }, console.error); }
整个函数读取了用户语言设置,而后最终调用了 startup()。
const startup = nlsConfig => { nlsConfig._languagePackSupport = true; process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig); process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || ''; // Load main in AMD perf.mark('willLoadMainBundle'); require('./bootstrap-amd').load('vs/code/electron-main/main', () => { perf.mark('didLoadMainBundle'); }); };
startup中主要是引入了 boostrap-amd ,这个bootstrap-amd引入了/vs/loader,并建立了一个loader。
const loader = require('./vs/loader');
loader是微软自家的AMD模块加载开源项目:https://github.com/Microsoft/...
而后经过loader加载 vs/code/electron-main/main 模块,这是 VSCode 真正的入口,而后在 vs/code/electron-main/main.ts 中能够看到定义了一个 CodeMain 类,而后初始化这个CodeMain类,并调用了 main 函数。
// src/vs/code/electron-main/main class CodeMain { main(): void { ... // Launch this.startup(args); } private async startup(args: ParsedArgs): Promise<void> { // We need to buffer the spdlog logs until we are sure // we are the only instance running, otherwise we'll have concurrent // log file access on Windows (https://github.com/Microsoft/vscode/issues/41218) const bufferLogService = new BufferLogService(); const [instantiationService, instanceEnvironment] = this.createServices(args, bufferLogService); try { // Init services await instantiationService.invokeFunction(async accessor => { const environmentService = accessor.get(IEnvironmentService); const configurationService = accessor.get(IConfigurationService); const stateService = accessor.get(IStateService); try { await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService); } catch (error) { // Show a dialog for errors that can be resolved by the user this.handleStartupDataDirError(environmentService, error); throw error; } }); // Startup await instantiationService.invokeFunction(async accessor => { const environmentService = accessor.get(IEnvironmentService); const logService = accessor.get(ILogService); const lifecycleMainService = accessor.get(ILifecycleMainService); const configurationService = accessor.get(IConfigurationService); const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleMainService, instantiationService, true); bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel()); once(lifecycleMainService.onWillShutdown)(() => (configurationService as ConfigurationService).dispose()); return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup(); }); } catch (error) { instantiationService.invokeFunction(this.quit, error); } } private createServices(args: ParsedArgs, bufferLogService: BufferLogService): [IInstantiationService, typeof process.env] { const services = new ServiceCollection(); const environmentService = new EnvironmentService(args, process.execPath); const instanceEnvironment = this.patchEnvironment(environmentService); // Patch `process.env` with the instance's environment services.set(IEnvironmentService, environmentService); const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]); process.once('exit', () => logService.dispose()); services.set(ILogService, logService); services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource)); services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService)); services.set(IStateService, new SyncDescriptor(StateService)); services.set(IRequestService, new SyncDescriptor(RequestMainService)); services.set(IThemeMainService, new SyncDescriptor(ThemeMainService)); services.set(ISignService, new SyncDescriptor(SignService)); return [new InstantiationService(services, true), instanceEnvironment]; } ... } // Main Startup const code = new CodeMain(); code.main();
能够看到 main() 函数最终调用了 startup() 函数。
在 startup() 函数中,先调用了 this.createServices() 函数来建立依赖的Services。
Services(服务) 是 VSCode 中一系列能够被注入的公共模块,这些 Services 分别负责不一样的功能,在这里建立了几个基本服务。除了这些基本服务,VSCode 内还包含了大量的服务,如 IModeService、ICodeEditorService、IPanelService 等,经过 VSCode 实现的「依赖注入」模式,能够在须要用到这些服务的地方以 Decorator 的方式作为构造函数参数声明依赖,会被自动注入到类中。关于服务的依赖注入,后面的章节会重点讲解。
private createServices(args: ParsedArgs, bufferLogService: BufferLogService): [IInstantiationService, typeof process.env] { const services = new ServiceCollection(); const environmentService = new EnvironmentService(args, process.execPath); const instanceEnvironment = this.patchEnvironment(environmentService); // Patch `process.env` with the instance's environment // environmentService 一些基本配置,包括运行目录、用户数据目录、工做区缓存目录等 services.set(IEnvironmentService, environmentService); const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]); process.once('exit', () => logService.dispose()); // logService 日志服务 services.set(ILogService, logService); // ConfigurationService 配置项 services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource)); // LifecycleService 生命周期相关的一些方法 services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService)); // StateService 持久化数据 services.set(IStateService, new SyncDescriptor(StateService)); // RequestService 请求服务 services.set(IRequestService, new SyncDescriptor(RequestMainService)); services.set(IThemeMainService, new SyncDescriptor(ThemeMainService)); services.set(ISignService, new SyncDescriptor(SignService)); return [new InstantiationService(services, true), instanceEnvironment]; }
代码中能够看到 createServices() 最终实例化了一个 InstantiationService 实例并return回去,而后在 startup() 中调用 InstantiationService的createInstance方法并传参数CodeApplication,表示初始化CodeApplication实例,而后调用实例的 startup() 方法。
return instantiationService.createInstance(CodeApplication, mainIpcServer,instanceEnvironment).startup();
接下来咱们去看CodeApplication中的startup方法。
//src/vs/code/electron-main/app.ts export class CodeApplication extends Disposable { ... async startup(): Promise<void> { this.logService.debug('Starting VS Code'); this.logService.debug(`from: ${this.environmentService.appRoot}`); this.logService.debug('args:', this.environmentService.args); // Make sure we associate the program with the app user model id // This will help Windows to associate the running program with // any shortcut that is pinned to the taskbar and prevent showing // two icons in the taskbar for the same app. const win32AppUserModelId = product.win32AppUserModelId; if (isWindows && win32AppUserModelId) { app.setAppUserModelId(win32AppUserModelId); } // Fix native tabs on macOS 10.13 // macOS enables a compatibility patch for any bundle ID beginning with // "com.microsoft.", which breaks native tabs for VS Code when using this // identifier (from the official build). // Explicitly opt out of the patch here before creating any windows. // See: https://github.com/Microsoft/vscode/issues/35361#issuecomment-399794085 try { if (isMacintosh && this.configurationService.getValue<boolean>('window.nativeTabs') === true && !systemPreferences.getUserDefault('NSUseImprovedLayoutPass', 'boolean')) { systemPreferences.setUserDefault('NSUseImprovedLayoutPass', 'boolean', true as any); } } catch (error) { this.logService.error(error); } // Create Electron IPC Server const electronIpcServer = new ElectronIPCServer(); // Resolve unique machine ID this.logService.trace('Resolving machine identifier...'); const { machineId, trueMachineId } = await this.resolveMachineId(); this.logService.trace(`Resolved machine identifier: ${machineId} (trueMachineId: ${trueMachineId})`); // Spawn shared process after the first window has opened and 3s have passed const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); const sharedProcessClient = sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main')); this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { this._register(new RunOnceScheduler(async () => { const userEnv = await getShellEnvironment(this.logService, this.environmentService); sharedProcess.spawn(userEnv); }, 3000)).schedule(); }); // Services const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient); // Create driver if (this.environmentService.driverHandle) { const server = await serveDriver(electronIpcServer, this.environmentService.driverHandle!, this.environmentService, appInstantiationService); this.logService.info('Driver started at:', this.environmentService.driverHandle); this._register(server); } // Setup Auth Handler this._register(new ProxyAuthHandler()); // Open Windows const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient)); // Post Open Windows Tasks this.afterWindowOpen(); // Tracing: Stop tracing after windows are ready if enabled if (this.environmentService.args.trace) { this.stopTracingEventually(windows); } } ... private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>): ICodeWindow[] { // Register more Main IPC services const launchMainService = accessor.get(ILaunchMainService); const launchChannel = createChannelReceiver(launchMainService, { disableMarshalling: true }); this.mainIpcServer.registerChannel('launch', launchChannel); // Register more Electron IPC services const updateService = accessor.get(IUpdateService); const updateChannel = new UpdateChannel(updateService); electronIpcServer.registerChannel('update', updateChannel); const issueService = accessor.get(IIssueService); const issueChannel = createChannelReceiver(issueService); electronIpcServer.registerChannel('issue', issueChannel); const electronService = accessor.get(IElectronService); const electronChannel = createChannelReceiver(electronService); electronIpcServer.registerChannel('electron', electronChannel); sharedProcessClient.then(client => client.registerChannel('electron', electronChannel)); const sharedProcessMainService = accessor.get(ISharedProcessMainService); const sharedProcessChannel = createChannelReceiver(sharedProcessMainService); electronIpcServer.registerChannel('sharedProcess', sharedProcessChannel); const workspacesService = accessor.get(IWorkspacesService); const workspacesChannel = createChannelReceiver(workspacesService); electronIpcServer.registerChannel('workspaces', workspacesChannel); const menubarService = accessor.get(IMenubarService); const menubarChannel = createChannelReceiver(menubarService); electronIpcServer.registerChannel('menubar', menubarChannel); const urlService = accessor.get(IURLService); const urlChannel = createChannelReceiver(urlService); electronIpcServer.registerChannel('url', urlChannel); const storageMainService = accessor.get(IStorageMainService); const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService)); electronIpcServer.registerChannel('storage', storageChannel); const loggerChannel = new LoggerChannel(accessor.get(ILogService)); electronIpcServer.registerChannel('logger', loggerChannel); sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel)); // ExtensionHost Debug broadcast service electronIpcServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel()); // Signal phase: ready (services set) this.lifecycleMainService.phase = LifecycleMainPhase.Ready; // Propagate to clients const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService); this.dialogMainService = accessor.get(IDialogMainService); // Create a URL handler to open file URIs in the active window const environmentService = accessor.get(IEnvironmentService); urlService.registerHandler({ async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> { // Catch file URLs if (uri.authority === Schemas.file && !!uri.path) { const cli = assign(Object.create(null), environmentService.args); const urisToOpen = [{ fileUri: URI.file(uri.fsPath) }]; windowsMainService.open({ context: OpenContext.API, cli, urisToOpen, gotoLineMode: true }); return true; } return false; } }); // Create a URL handler which forwards to the last active window const activeWindowManager = new ActiveWindowManager(electronService); const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); const urlHandlerRouter = new URLHandlerRouter(activeWindowRouter); const urlHandlerChannel = electronIpcServer.getChannel('urlHandler', urlHandlerRouter); const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel); // On Mac, Code can be running without any open windows, so we must create a window to handle urls, // if there is none if (isMacintosh) { urlService.registerHandler({ async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> { if (windowsMainService.getWindowCount() === 0) { const cli = { ...environmentService.args }; const [window] = windowsMainService.open({ context: OpenContext.API, cli, forceEmpty: true, gotoLineMode: true }); await window.ready(); return urlService.open(uri); } return false; } }); } // Register the multiple URL handler urlService.registerHandler(multiplexURLHandler); // Watch Electron URLs and forward them to the UrlService const args = this.environmentService.args; const urls = args['open-url'] ? args._urls : []; const urlListener = new ElectronURLListener(urls || [], urlService, windowsMainService); this._register(urlListener); // Open our first window const macOpenFiles: string[] = (<any>global).macOpenFiles; const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; const hasCliArgs = args._.length; const hasFolderURIs = !!args['folder-uri']; const hasFileURIs = !!args['file-uri']; const noRecentEntry = args['skip-add-to-recently-opened'] === true; const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined; // new window if "-n" was used without paths if (args['new-window'] && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { return windowsMainService.open({ context, cli: args, forceNewWindow: true, forceEmpty: true, noRecentEntry, waitMarkerFileURI, initialStartup: true }); } // mac: open-file event received on startup if (macOpenFiles && macOpenFiles.length && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { return windowsMainService.open({ context: OpenContext.DOCK, cli: args, urisToOpen: macOpenFiles.map(file => this.getWindowOpenableFromPathSync(file)), noRecentEntry, waitMarkerFileURI, gotoLineMode: false, initialStartup: true }); } // default: read paths from cli return windowsMainService.open({ context, cli: args, forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), diffMode: args.diff, noRecentEntry, waitMarkerFileURI, gotoLineMode: args.goto, initialStartup: true }); } ... }
在CodeApplication.startup 中首先会启动 SharedProcess 共享进程,同时也建立了一些窗口相关的服务,包括 WindowsManager、WindowsService、MenubarService 等,负责窗口、多窗口管理及菜单等功能。而后调用 openFirstWindow 方法来开启窗口。
在openFirstWindow中,先建立一系列 Electron 的 IPC 频道,用于主进程和渲染进程间通讯,其中 window 和 logLevel 频道还会被注册到 sharedProcessClient ,sharedProcessClient 是主进程与共享进程(SharedProcess)进行通讯的 client,以后根据 environmentService 提供的相关参数(file_uri、folder_uri)调用了 windowsMainService.open 方法。
windowsMainService是WindowsManager实例化的服务,而WindowsManager是多窗体管理类(src/vs/code/electron-main/windows.ts)。接下来咱们看windowsMainService.open 方法,能够看到其调用了doOpen方法。
open(openConfig: IOpenConfiguration): ICodeWindow[] { ... // Open based on config const usedWindows = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, ... }
doOpen方法最终调用了openInBrowserWindow方法。
private doOpen( openConfig: IOpenConfiguration, workspacesToOpen: IWorkspacePathToOpen[], foldersToOpen: IFolderPathToOpen[], emptyToRestore: IEmptyWindowBackupInfo[], emptyToOpen: number, fileInputs: IFileInputs | undefined, foldersToAdd: IFolderPathToOpen[] ) { const usedWindows: ICodeWindow[] = []; ... // Handle empty to open (only if no other window opened) if (usedWindows.length === 0 || fileInputs) { if (fileInputs && !emptyToOpen) { emptyToOpen++; } const remoteAuthority = fileInputs ? fileInputs.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined); for (let i = 0; i < emptyToOpen; i++) { usedWindows.push(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, remoteAuthority, forceNewWindow: openFolderInNewWindow, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, fileInputs })); // Reset these because we handled them fileInputs = undefined; openFolderInNewWindow = true; // any other window to open must open in new window then } } return arrays.distinct(usedWindows); }
在openInBrowserWindow中,建立一个CodeWindow实例并返回,而且还调用了doOpenInBrowserWindow这个方法,这个方法看下文介绍。
private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { ... // Create the window window = this.instantiationService.createInstance(CodeWindow, { state, extensionDevelopmentPath: configuration.extensionDevelopmentPath, isExtensionTestHost: !!configuration.extensionTestsPath }); ... // If the window was already loaded, make sure to unload it // first and only load the new configuration if that was // not vetoed if (window.isReady) { this.lifecycleMainService.unload(window, UnloadReason.LOAD).then(veto => { if (!veto) { this.doOpenInBrowserWindow(window!, configuration, options); } }); } else { this.doOpenInBrowserWindow(window, configuration, options); } return window; }
接下来咱们找到CodeWindow定义在src/vs/code/electron-main/window.ts中,在CodeWindow的构造函数中调用了createBrowserWindow方法,而后在createBrowserWindow方法中看到实例化了一个BrowserWindow,这是Electron中浏览器窗口的定义。
//src/vs/code/electron-main/window.ts export class CodeWindow extends Disposable implements ICodeWindow { ... constructor( config: IWindowCreationOptions, @ILogService private readonly logService: ILogService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeMainService private readonly themeMainService: IThemeMainService, @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, @IBackupMainService private readonly backupMainService: IBackupMainService, ) { super(); this.touchBarGroups = []; this._lastFocusTime = -1; this._readyState = ReadyState.NONE; this.whenReadyCallbacks = []; // create browser window this.createBrowserWindow(config); // respect configured menu bar visibility this.onConfigurationUpdated(); // macOS: touch bar support this.createTouchBar(); // Request handling this.handleMarketplaceRequests(); // Eventing this.registerListeners(); } private createBrowserWindow(config: IWindowCreationOptions): void { ... // Create the browser window. this._win = new BrowserWindow(options); ... } }
如今窗口有了,那么何时加载页面呢?刚刚咱们在上文提到,在openInBrowserWindow中,建立一个CodeWindow实例并返回,而且还调用了doOpenInBrowserWindow这个方法,那么咱们看一下这个方法的定义。
private doOpenInBrowserWindow(window: ICodeWindow, configuration: IWindowConfiguration, options: IOpenBrowserWindowOptions): void { ... // Load it window.load(configuration); ... }
这个方法有调用CodeWindow的load方法,而后看一下load方法的定义。会看到调用了this._win.loadURL,这个this._win就是CodeWindow建立的BrowserWindow窗口,这就找到了窗口加载的URL时机。
load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { ... // Load URL perf.mark('main:loadWindow'); this._win.loadURL(this.getUrl(configuration)); ... }
而后看一下getUrl方法的定义,最终返回的configUrl是调用doGetUrl获取的。
private getUrl(windowConfiguration: IWindowConfiguration): string { ... let configUrl = this.doGetUrl(config); ... return configUrl; }
而后看一下doGetUrl方法,能够看到返回的Url路径为vs/code/electron-browser/workbench/workbench.html。
private doGetUrl(config: object): string { return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`; }
这是整个 Workbench 的入口,HTML出现了,主进程的使命完成,渲染进程登场。
//src/vs/code/electron-browser/workbench/workbench.html <!-- Copyright (C) Microsoft Corporation. All rights reserved. --> <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' https://*.vscode-webview-test.com; object-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https: vscode-remote-resource:;"> </head> <body class="vs-dark" aria-label=""> </body> <!-- Startup via workbench.js --> <script src="workbench.js"></script> </html>
workbench.html中加载了workbench.js文件,这个文件负责加载真正的 Workbench 模块并调用其 main 方法初始化主界面。
// src/vs/code/electron-browser/workbench/workbench.js const bootstrapWindow = require('../../../../bootstrap-window'); // Setup shell environment process['lazyEnv'] = getLazyEnv(); // Load workbench main JS, CSS and NLS all in parallel. This is an // optimization to prevent a waterfall of loading to happen, because // we know for a fact that workbench.desktop.main will depend on // the related CSS and NLS counterparts. bootstrapWindow.load([ 'vs/workbench/workbench.desktop.main', 'vs/nls!vs/workbench/workbench.desktop.main', 'vs/css!vs/workbench/workbench.desktop.main' ], function (workbench, configuration) { perf.mark('didLoadWorkbenchMain'); return process['lazyEnv'].then(function () { perf.mark('main/startup'); // @ts-ignore //加载 Workbench 并初始化主界面 return require('vs/workbench/electron-browser/desktop.main').main(configuration); }); }, { removeDeveloperKeybindingsAfterLoad: true, canModifyDOM: function (windowConfig) { showPartsSplash(windowConfig); }, beforeLoaderConfig: function (windowConfig, loaderConfig) { loaderConfig.recordStats = true; }, beforeRequire: function () { perf.mark('willLoadWorkbenchMain'); } });
咱们能够看到加载了vs/workbench/electron-browser/desktop.main模块,并调用了模块的main方法。main方法中实例化了一个DesktopMain,并调用了DesktopMain的open方法。
class DesktopMain extends Disposable { async open(): Promise<void> { ... // Create Workbench const workbench = new Workbench(document.body, services.serviceCollection, services.logService); // Listeners this.registerListeners(workbench, services.storageService); // Startup const instantiationService = workbench.startup(); ... } ... } export function main(configuration: IWindowConfiguration): Promise<void> { const renderer = new DesktopMain(configuration); return renderer.open(); }
咱们看到DesktopMain的open方法中实例化了Workbench类,并调用了Workbench的startup方法。接下来咱们看一下这个Workbench类。
export class Workbench extends Layout { ... startup(): IInstantiationService { try { // Configure emitter leak warning threshold setGlobalLeakWarningThreshold(175); // ARIA setARIAContainer(document.body); // Services const instantiationService = this.initServices(this.serviceCollection); instantiationService.invokeFunction(async accessor => { const lifecycleService = accessor.get(ILifecycleService); const storageService = accessor.get(IStorageService); const configurationService = accessor.get(IConfigurationService); // Layout this.initLayout(accessor); // Registries this.startRegistries(accessor); // Context Keys this._register(instantiationService.createInstance(WorkbenchContextKeysHandler)); // Register Listeners this.registerListeners(lifecycleService, storageService, configurationService); // Render Workbench this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService); // Workbench Layout this.createWorkbenchLayout(instantiationService); // Layout this.layout(); // Restore try { await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService); } catch (error) { onUnexpectedError(error); } }); return instantiationService; } catch (error) { onUnexpectedError(error); throw error; // rethrow because this is a critical issue we cannot handle properly here } } ... }
咱们能够看到Workbench继承Layout布局类,在 workbench.startup 方法中构建主界面布局、建立全局事件监听以及实例化一些依赖的服务,所有完成后会还原以前打开的编辑器,整个 Workbench 加载完成。
因此前文中的大量代码只是为这里最终建立主界面作铺垫,Workbench 模块主要代码都在 vs/workbench 目录下,主要负责界面元素的建立和具体业务功能的实现。
至此,从启动到加载到html,再到构建主界面布局,整个流程很清晰。
在VSCode源码根目录下有一个product.json文件,此文件用于配置应用的信息。
{ "nameShort": "Code - OSS", "nameLong": "Code - OSS", "applicationName": "code-oss", "dataFolderName": ".vscode-oss", "win32MutexName": "vscodeoss", "licenseName": "MIT", "licenseUrl": "https://github.com/Microsoft/vscode/blob/master/LICENSE.txt", "win32DirName": "Microsoft Code OSS", "win32NameVersion": "Microsoft Code OSS", "win32RegValueName": "CodeOSS", "win32AppId": "{{E34003BB-9E10-4501-8C11-BE3FAA83F23F}", "win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}", "win32UserAppId": "{{C6065F05-9603-4FC4-8101-B9781A25D88E}", "win32x64UserAppId": "{{C6065F05-9603-4FC4-8101-B9781A25D88E}", "win32AppUserModelId": "Microsoft.CodeOSS", "win32ShellNameShort": "C&ode - OSS", "darwinBundleIdentifier": "com.visualstudio.code.oss", "linuxIconName": "com.visualstudio.code.oss", "licenseFileName": "LICENSE.txt", "reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new", "urlProtocol": "code-oss", "extensionAllowedProposedApi": [ "ms-vscode.references-view" ] }
能够修改product.json的信息来更新定制VSCode的名称等信息。若是你在执行了./scripts/code.sh后修改了product.json的信息,好比修改了nameLong的配置,这时候从新运行./scripts/code.sh会报错。
错误信息是 ./scripts/code.sh: line 53: /Users/jiangshuaijie/Desktop/vscode-1.39.2/.build/electron/test.app/Contents/MacOS/Electron: No such file or directory ,能够看出是在code.sh中报错了,看一下code.sh中内容。
... function code() { cd "$ROOT" if [[ "$OSTYPE" == "darwin"* ]]; then NAME=`node -p "require('./product.json').nameLong"` CODE="./.build/electron/$NAME.app/Contents/MacOS/Electron" else NAME=`node -p "require('./product.json').applicationName"` CODE=".build/electron/$NAME" fi ... # Launch Code exec "$CODE" . "$@" } ...
最终根据product.json中的nameLong来运行根目录下.build/electron/下生成的app,这时候的应用是以前生成过的,因此会报错。咱们只需删除掉根目录下.build文件夹,从新执行./scripts/code.sh便可。
在VSCode源码根目录下resources文件夹主要用于存放VSCode平台的静态资源,例如应用图标等。
其中darwin、linux、win32对应三个不一样的平台,能够在不一样平台文件夹下替换图片资源。
更多精彩请关注 https://codeteenager.github.i... ,后面会更新更多内容。