├── README.md
├── all.md
├── chapter-1.md
├── chapter-2.md
├── chapter-3.md
├── chapter-4.md
├── chapter-5.md
├── chapter-6.md
├── vscode-debugg.png
├── vscode-framework.png
├── vscode-source.png
├── vscode-vside-zh.png
├── vscode-vside.png
└── vscode-welcome.png
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # VSCode源码分析
3 |
4 | ## 目录
5 | * 总览
6 | * (一)启动主进程
7 | * (二)实例化服务
8 | * (三)事件分发
9 | * (四)进程通信
10 | * (五)主要窗口
11 | * (六)开发调试
12 |
13 | ## 简介
14 | Visual Studio Code(简称VSCode) 是开源免费的IDE编辑器,原本是微软内部使用的云编辑器(Monaco)。
15 |
16 | git仓库地址: https://github.com/microsoft/vscode
17 |
18 | 通过Eletron集成了桌面应用,可以跨平台使用,开发语言主要采用微软自家的TypeScript。
19 | 整个项目结构比较清晰,方便阅读代码理解。成为了最流行跨平台的桌面IDE应用
20 |
21 | 微软希望VSCode在保持核心轻量级的基础上,增加项目支持,智能感知,编译调试。
22 | 
23 |
24 | ## 编译安装
25 | 下载最新版本,目前我用的是1.37.1版本
26 | 官方的wiki中有编译安装的说明 [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute?_blank)
27 |
28 | Linux, Window, MacOS三个系统编译时有些差别,参考官方文档,
29 | 在编译安装依赖时如果遇到connect timeout, 需要进行科学上网。
30 |
31 | > 需要注意的一点 运行环境依赖版本 Nodejs x64 version >= 10.16.0, < 11.0.0, python 2.7(3.0不能正常执行)
32 |
33 |
34 |
35 | ## 技术架构
36 | 
37 |
38 | ### Electron
39 |
40 | > [Electron](https://electronjs.org/?_blank) 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可 (Electron = Node.js + Chromium + Native API)
41 |
42 | ### Monaco Editor
43 | > [Monaco Editor](https://github.com/microsoft/monaco-editor?_blank)是微软开源项目, 为VS Code提供支持的代码编辑器,运行在浏览器环境中。编辑器提供代码提示,智能建议等功能。供开发人员远程更方便的编写代码,可独立运行。
44 |
45 | ### TypeScript
46 | > TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程
47 |
48 | ## 目录结构
49 | ```
50 | ├── build # gulp编译构建脚本
51 | ├── extensions # 内置插件
52 | ├── product.json # App meta信息
53 | ├── resources # 平台相关静态资源
54 | ├── scripts # 工具脚本,开发/测试
55 | ├── src # 源码目录
56 | └── typings # 函数语法补全定义
57 | └── vs
58 | ├── base # 通用工具/协议和UI库
59 | │ ├── browser # 基础UI组件,DOM操作
60 | │ ├── common # diff描述,markdown解析器,worker协议,各种工具函数
61 | │ ├── node # Node工具函数
62 | │ ├── parts # IPC协议(Electron、Node),quickopen、tree组件
63 | │ ├── test # base单测用例
64 | │ └── worker # Worker factory和main Worker(运行IDE Core:Monaco)
65 | ├── code # VSCode主运行窗口
66 | ├── editor # IDE代码编辑器
67 | | ├── browser # 代码编辑器核心
68 | | ├── common # 代码编辑器核心
69 | | ├── contrib # vscode 与独立 IDE共享的代码
70 | | └── standalone # 独立 IDE 独有的代码
71 | ├── platform # 支持注入服务和平台相关基础服务(文件、剪切板、窗体、状态栏)
72 | ├── workbench # 工作区UI布局,功能主界面
73 | │ ├── api #
74 | │ ├── browser #
75 | │ ├── common #
76 | │ ├── contrib #
77 | │ ├── electron-browser #
78 | │ ├── services #
79 | │ └── test #
80 | ├── css.build.js # 用于插件构建的CSS loader
81 | ├── css.js # CSS loader
82 | ├── editor # 对接IDE Core(读取编辑/交互状态),提供命令、上下文菜单、hover、snippet等支持
83 | ├── loader.js # AMD loader(用于异步加载AMD模块)
84 | ├── nls.build.js # 用于插件构建的NLS loader
85 | └── nls.js # NLS(National Language Support)多语言loader
86 | ```
87 | ### 核心层
88 | * base: 提供通用服务和构建用户界面
89 | * platform: 注入服务和基础服务代码
90 | * editor: 微软Monaco编辑器,也可独立运行使用
91 | * wrokbench: 配合Monaco并且给viewlets提供框架:如:浏览器状态栏,菜单栏利用electron实现桌面程序
92 |
93 | ### 核心环境
94 | 整个项目完全使用typescript实现,electron中运行主进程和渲染进程,使用的api有所不同,所以在core中每个目录组织也是按照使用的api来安排,
95 | 运行的环境分为几类:
96 | * common: 只使用javascritp api的代码,能在任何环境下运行
97 | * browser: 浏览器api, 如操作dom; 可以调用common
98 | * node: 需要使用node的api,比如文件io操作
99 | * electron-brower: 渲染进程api, 可以调用common, brower, node, 依赖[electron renderer-process API](https://github.com/electron/electron/tree/master/docs#modules-for-the-renderer-process-web-page)
100 | * electron-main: 主进程api, 可以调用: common, node 依赖于[electron main-process AP](https://github.com/electron/electron/tree/master/docs#modules-for-the-main-process)
101 |
102 | 
103 |
--------------------------------------------------------------------------------
/all.md:
--------------------------------------------------------------------------------
1 | # VSCode源码分析
2 | ## 目录
3 | * 简介
4 | * 技术架构
5 | * 启动主进程
6 | * 实例化服务
7 | * 事件分发
8 | * 进程通信
9 | * 主要窗口
10 | * 开发调试
11 |
12 | ## 简介
13 | Visual Studio Code(简称VSCode) 是开源免费的IDE编辑器,原本是微软内部使用的云编辑器(Monaco)。
14 |
15 | git仓库地址: https://github.com/microsoft/vscode
16 |
17 | 通过Eletron集成了桌面应用,可以跨平台使用,开发语言主要采用微软自家的TypeScript。
18 | 整个项目结构比较清晰,方便阅读代码理解。成为了最流行跨平台的桌面IDE应用
19 |
20 | 微软希望VSCode在保持核心轻量级的基础上,增加项目支持,智能感知,编译调试。
21 | 
22 |
23 | ## 编译安装
24 | 下载最新版本,目前我用的是1.37.1版本
25 | 官方的wiki中有编译安装的说明 [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute?_blank)
26 |
27 | Linux, Window, MacOS三个系统编译时有些差别,参考官方文档,
28 | 在编译安装依赖时如果遇到connect timeout, 需要进行科学上网。
29 |
30 | > 需要注意的一点 运行环境依赖版本 Nodejs x64 version >= 10.16.0, < 11.0.0, python 2.7(3.0不能正常执行)
31 |
32 |
33 | ## 技术架构
34 | 
35 |
36 | ### Electron
37 |
38 | > [Electron](https://electronjs.org/?_blank) 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可 (Electron = Node.js + Chromium + Native API)
39 |
40 | ### Monaco Editor
41 | > [Monaco Editor](https://github.com/microsoft/monaco-editor?_blank)是微软开源项目, 为VS Code提供支持的代码编辑器,运行在浏览器环境中。编辑器提供代码提示,智能建议等功能。供开发人员远程更方便的编写代码,可独立运行。
42 |
43 | ### TypeScript
44 | > TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程
45 |
46 | ## 目录结构
47 | ```
48 | ├── build # gulp编译构建脚本
49 | ├── extensions # 内置插件
50 | ├── product.json # App meta信息
51 | ├── resources # 平台相关静态资源
52 | ├── scripts # 工具脚本,开发/测试
53 | ├── src # 源码目录
54 | └── typings # 函数语法补全定义
55 | └── vs
56 | ├── base # 通用工具/协议和UI库
57 | │ ├── browser # 基础UI组件,DOM操作
58 | │ ├── common # diff描述,markdown解析器,worker协议,各种工具函数
59 | │ ├── node # Node工具函数
60 | │ ├── parts # IPC协议(Electron、Node),quickopen、tree组件
61 | │ ├── test # base单测用例
62 | │ └── worker # Worker factory和main Worker(运行IDE Core:Monaco)
63 | ├── code # VSCode主运行窗口
64 | ├── editor # IDE代码编辑器
65 | | ├── browser # 代码编辑器核心
66 | | ├── common # 代码编辑器核心
67 | | ├── contrib # vscode 与独立 IDE共享的代码
68 | | └── standalone # 独立 IDE 独有的代码
69 | ├── platform # 支持注入服务和平台相关基础服务(文件、剪切板、窗体、状态栏)
70 | ├── workbench # 工作区UI布局,功能主界面
71 | │ ├── api #
72 | │ ├── browser #
73 | │ ├── common #
74 | │ ├── contrib #
75 | │ ├── electron-browser #
76 | │ ├── services #
77 | │ └── test #
78 | ├── css.build.js # 用于插件构建的CSS loader
79 | ├── css.js # CSS loader
80 | ├── editor # 对接IDE Core(读取编辑/交互状态),提供命令、上下文菜单、hover、snippet等支持
81 | ├── loader.js # AMD loader(用于异步加载AMD模块)
82 | ├── nls.build.js # 用于插件构建的NLS loader
83 | └── nls.js # NLS(National Language Support)多语言loader
84 | ```
85 | ### 核心层
86 | * base: 提供通用服务和构建用户界面
87 | * platform: 注入服务和基础服务代码
88 | * editor: 微软Monaco编辑器,也可独立运行使用
89 | * wrokbench: 配合Monaco并且给viewlets提供框架:如:浏览器状态栏,菜单栏利用electron实现桌面程序
90 |
91 | ### 核心环境
92 | 整个项目完全使用typescript实现,electron中运行主进程和渲染进程,使用的api有所不同,所以在core中每个目录组织也是按照使用的api来安排,
93 | 运行的环境分为几类:
94 | * common: 只使用javascritp api的代码,能在任何环境下运行
95 | * browser: 浏览器api, 如操作dom; 可以调用common
96 | * node: 需要使用node的api,比如文件io操作
97 | * electron-brower: 渲染进程api, 可以调用common, brower, node, 依赖[electron renderer-process API](https://github.com/electron/electron/tree/master/docs#modules-for-the-renderer-process-web-page)
98 | * electron-main: 主进程api, 可以调用: common, node 依赖于[electron main-process AP](https://github.com/electron/electron/tree/master/docs#modules-for-the-main-process)
99 |
100 | 
101 |
102 | ## 启动主进程
103 |
104 | ### Electron通过package.json中的main字段来定义应用入口。
105 | main.js是vscode的入口。
106 |
107 | * src/main.js
108 | * vs/code/electron-main/main.ts
109 | * vs/code/electron-main/app.ts
110 | * vs/code/electron-main/windows.ts
111 | * vs/workbench/electron-browser/desktop.main.ts
112 | * vs/workbench/browser/workbench.ts
113 |
114 | ```js
115 | app.once('ready', function () {
116 | //启动追踪,后面会讲到,跟性能检测优化相关。
117 | if (args['trace']) {
118 | // @ts-ignore
119 | const contentTracing = require('electron').contentTracing;
120 |
121 | const traceOptions = {
122 | categoryFilter: args['trace-category-filter'] || '*',
123 | traceOptions: args['trace-options'] || 'record-until-full,enable-sampling'
124 | };
125 |
126 | contentTracing.startRecording(traceOptions, () => onReady());
127 | } else {
128 | onReady();
129 | }
130 | });
131 | function onReady() {
132 | perf.mark('main:appReady');
133 |
134 | Promise.all([nodeCachedDataDir.ensureExists(), userDefinedLocale]).then(([cachedDataDir, locale]) => {
135 | //1. 这里尝试获取本地配置信息,如果有的话会传递到startup
136 | if (locale && !nlsConfiguration) {
137 | nlsConfiguration = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale);
138 | }
139 |
140 | if (!nlsConfiguration) {
141 | nlsConfiguration = Promise.resolve(undefined);
142 | }
143 |
144 |
145 | nlsConfiguration.then(nlsConfig => {
146 |
147 | //4. 首先会检查用户语言环境配置,如果没有设置默认使用英语
148 | const startup = nlsConfig => {
149 | nlsConfig._languagePackSupport = true;
150 | process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
151 | process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';
152 |
153 | perf.mark('willLoadMainBundle');
154 | //使用微软的loader组件加载electron-main/main文件
155 | require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
156 | perf.mark('didLoadMainBundle');
157 | });
158 | };
159 |
160 | // 2. 接收到有效的配置传入是其生效,调用startup
161 | if (nlsConfig) {
162 | startup(nlsConfig);
163 | }
164 |
165 | // 3. 这里尝试使用本地的应用程序
166 | // 应用程序设置区域在ready事件后才有效
167 | else {
168 | let appLocale = app.getLocale();
169 | if (!appLocale) {
170 | startup({ locale: 'en', availableLanguages: {} });
171 | } else {
172 |
173 | // 配置兼容大小写敏感,所以统一转换成小写
174 | appLocale = appLocale.toLowerCase();
175 | // 这里就会调用config服务,把本地配置加载进来再调用startup
176 | lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale).then(nlsConfig => {
177 | if (!nlsConfig) {
178 | nlsConfig = { locale: appLocale, availableLanguages: {} };
179 | }
180 |
181 | startup(nlsConfig);
182 | });
183 | }
184 | }
185 | });
186 | }, console.error);
187 | }
188 | ```
189 |
190 | ### vs/code/electron-main/main.ts
191 | electron-main/main 是程序真正启动的入口,进入main process初始化流程.
192 | #### 这里主要做了两件事情:
193 | 1. 初始化Service
194 | 2. 启动主实例
195 |
196 | 直接看startup方法的实现,基础服务初始化完成后会加载 CodeApplication, mainIpcServer, instanceEnvironment,调用 startup 方法启动APP
197 | ```js
198 | private async startup(args: ParsedArgs): Promise {
199 |
200 | //spdlog 日志服务
201 | const bufferLogService = new BufferLogService();
202 |
203 | // 1. 调用 createServices
204 | const [instantiationService, instanceEnvironment] = this.createServices(args, bufferLogService);
205 | try {
206 |
207 | // 1.1 初始化Service服务
208 | await instantiationService.invokeFunction(async accessor => {
209 | // 基础服务,包括一些用户数据,缓存目录
210 | const environmentService = accessor.get(IEnvironmentService);
211 | // 配置服务
212 | const configurationService = accessor.get(IConfigurationService);
213 | // 持久化数据
214 | const stateService = accessor.get(IStateService);
215 |
216 | try {
217 | await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService);
218 | } catch (error) {
219 |
220 | // 抛出错误对话框
221 | this.handleStartupDataDirError(environmentService, error);
222 |
223 | throw error;
224 | }
225 | });
226 |
227 | // 1.2 启动实例
228 | await instantiationService.invokeFunction(async accessor => {
229 | const environmentService = accessor.get(IEnvironmentService);
230 | const logService = accessor.get(ILogService);
231 | const lifecycleService = accessor.get(ILifecycleService);
232 | const configurationService = accessor.get(IConfigurationService);
233 |
234 | const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleService, instantiationService, true);
235 |
236 | bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel());
237 | once(lifecycleService.onWillShutdown)(() => (configurationService as ConfigurationService).dispose());
238 |
239 | return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
240 | });
241 | } catch (error) {
242 | instantiationService.invokeFunction(this.quit, error);
243 | }
244 | }
245 | ```
246 |
247 | ### Service
248 | 这里通过createService创建一些基础的Service
249 |
250 | ### 运行环境服务 EnvironmentService
251 | src/vs/platform/environment/node/environmentService.ts
252 |
253 | 通过这个服务获取当前启动目录,日志目录,操作系统信息,配置文件目录,用户目录等。
254 |
255 | ### 日志服务 MultiplexLogService
256 | src/vs/platform/log/common/log.ts
257 |
258 | 默认使用控制台日志ConsoleLogMainService
259 | 其中包含性能追踪和释放信息,日志输出级别
260 |
261 | ### 配置服务 ConfigurationService
262 | src/vs/platform/configuration/node/configurationService.ts
263 |
264 | 从运行环境服务获取内容
265 |
266 |
267 | ### 生命周期服务 LifecycleService
268 | src/vs/platform/lifecycle/common/lifecycleService.ts
269 |
270 | 监听事件,electron app模块 比如:ready, window-all-closed,before-quit
271 |
272 | 可以参考官方[electron app文档](https://www.w3cschool.cn/electronmanual/electronmanual-electronapp.html)
273 |
274 | ### 状态服务 StateService
275 | src/vs/platform/state/node/stateService.ts
276 |
277 | 通过FileStorage读写storage.json存储,里记录一些与程序运行状态有关的键值对
278 |
279 | ### 请求服务 RequestService
280 | src/vs/platform/request/browser/requestService.ts
281 |
282 | 这里使用的是原生ajax请求,实现了request方法
283 |
284 | ### 主题服务 ThemeMainService
285 | src/vs/platform/theme/electron-main/themeMainService.ts
286 |
287 | 这里只设置背景颜色,通过getBackgroundColor方法 IStateService存储
288 |
289 | ### 签名服务 SignService
290 | src/vs/platform/sign/node/signService.ts
291 |
292 | ```js
293 | private createServices(args: ParsedArgs, bufferLogService: BufferLogService): [IInstantiationService, typeof process.env] {
294 |
295 | //服务注册容器
296 | const services = new ServiceCollection();
297 |
298 | const environmentService = new EnvironmentService(args, process.execPath);
299 | const instanceEnvironment = this.patchEnvironment(environmentService); // Patch `process.env` with the instance's environment
300 | services.set(IEnvironmentService, environmentService);
301 |
302 | const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]);
303 | process.once('exit', () => logService.dispose());
304 | //日志服务
305 | services.set(ILogService, logService);
306 | //配置服务
307 | services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource));
308 | //生命周期
309 | services.set(ILifecycleService, new SyncDescriptor(LifecycleService));
310 | //状态存储
311 | services.set(IStateService, new SyncDescriptor(StateService));
312 | //网络请求
313 | services.set(IRequestService, new SyncDescriptor(RequestService));
314 | //主题设定
315 | services.set(IThemeMainService, new SyncDescriptor(ThemeMainService));
316 | //签名服务
317 | services.set(ISignService, new SyncDescriptor(SignService));
318 |
319 | return [new InstantiationService(services, true), instanceEnvironment];
320 | }
321 | ```
322 |
323 | ## 实例化服务
324 |
325 | SyncDescriptor负责注册这些服务,当用到该服务时进程实例化使用
326 |
327 | src/vs/platform/instantiation/common/descriptors.ts
328 |
329 | ```js
330 | export class SyncDescriptor {
331 | readonly ctor: any;
332 | readonly staticArguments: any[];
333 | readonly supportsDelayedInstantiation: boolean;
334 | constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) {
335 | this.ctor = ctor;
336 | this.staticArguments = staticArguments;
337 | this.supportsDelayedInstantiation = supportsDelayedInstantiation;
338 | }
339 | }
340 | ```
341 |
342 | main.ts中startup方法调用invokeFunction.get实例化服务
343 | ```js
344 | await instantiationService.invokeFunction(async accessor => {
345 | const environmentService = accessor.get(IEnvironmentService);
346 | const configurationService = accessor.get(IConfigurationService);
347 | const stateService = accessor.get(IStateService);
348 | try {
349 | await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService);
350 | } catch (error) {
351 |
352 | // Show a dialog for errors that can be resolved by the user
353 | this.handleStartupDataDirError(environmentService, error);
354 |
355 | throw error;
356 | }
357 | });
358 | ```
359 |
360 | get方法调用_getOrCreateServiceInstance,这里第一次创建会存入缓存中
361 | 下次实例化对象时会优先从缓存中获取对象。
362 |
363 | src/vs/platform/instantiation/common/instantiationService.ts
364 |
365 | ```js
366 | invokeFunction(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R {
367 | let _trace = Trace.traceInvocation(fn);
368 | let _done = false;
369 | try {
370 | const accessor: ServicesAccessor = {
371 | get: (id: ServiceIdentifier, isOptional?: typeof optional) => {
372 |
373 | if (_done) {
374 | throw illegalState('service accessor is only valid during the invocation of its target method');
375 | }
376 |
377 | const result = this._getOrCreateServiceInstance(id, _trace);
378 | if (!result && isOptional !== optional) {
379 | throw new Error(`[invokeFunction] unknown service '${id}'`);
380 | }
381 | return result;
382 | }
383 | };
384 | return fn.apply(undefined, [accessor, ...args]);
385 | } finally {
386 | _done = true;
387 | _trace.stop();
388 | }
389 | }
390 | private _getOrCreateServiceInstance(id: ServiceIdentifier, _trace: Trace): T {
391 | let thing = this._getServiceInstanceOrDescriptor(id);
392 | if (thing instanceof SyncDescriptor) {
393 | return this._createAndCacheServiceInstance(id, thing, _trace.branch(id, true));
394 | } else {
395 | _trace.branch(id, false);
396 | return thing;
397 | }
398 | }
399 | ```
400 |
401 | #### vs/code/electron-main/app.ts
402 | 这里首先触发CodeApplication.startup()方法, 在第一个窗口打开3秒后成为共享进程,
403 | ```js
404 | async startup(): Promise {
405 | ...
406 |
407 | // 1. 第一个窗口创建共享进程
408 | const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv);
409 | const sharedProcessClient = sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main'));
410 | this.lifecycleService.when(LifecycleMainPhase.AfterWindowOpen).then(() => {
411 | this._register(new RunOnceScheduler(async () => {
412 | const userEnv = await getShellEnvironment(this.logService, this.environmentService);
413 |
414 | sharedProcess.spawn(userEnv);
415 | }, 3000)).schedule();
416 | });
417 | // 2. 创建app实例
418 | const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient);
419 |
420 |
421 | // 3. 打开一个窗口 调用
422 |
423 | const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));
424 |
425 | // 4. 窗口打开后执行生命周期和授权操作
426 | this.afterWindowOpen();
427 | ...
428 |
429 | //vscode结束了性能问题的追踪
430 | if (this.environmentService.args.trace) {
431 | this.stopTracingEventually(windows);
432 | }
433 | }
434 | ```
435 |
436 |
437 | openFirstWindow 主要实现
438 | CodeApplication.openFirstWindow 首次开启窗口时,创建 Electron 的 IPC,使主进程和渲染进程间通信。
439 | window会被注册到sharedProcessClient,主进程和共享进程通信
440 | 根据 environmentService 提供的参数(path,uri)调用windowsMainService.open 方法打开窗口
441 | ```js
442 | private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>): ICodeWindow[] {
443 |
444 | ...
445 | // 1. 注入Electron IPC Service, windows窗口管理,菜单栏等服务
446 |
447 | // 2. 根据environmentService进行参数配置
448 | const macOpenFiles: string[] = (global).macOpenFiles;
449 | const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
450 | const hasCliArgs = hasArgs(args._);
451 | const hasFolderURIs = hasArgs(args['folder-uri']);
452 | const hasFileURIs = hasArgs(args['file-uri']);
453 | const noRecentEntry = args['skip-add-to-recently-opened'] === true;
454 | const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;
455 |
456 | ...
457 |
458 | // 打开主窗口,默认从执行命令行中读取参数
459 | return windowsMainService.open({
460 | context,
461 | cli: args,
462 | forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
463 | diffMode: args.diff,
464 | noRecentEntry,
465 | waitMarkerFileURI,
466 | gotoLineMode: args.goto,
467 | initialStartup: true
468 | });
469 | }
470 | ```
471 |
472 |
473 |
474 | #### vs/code/electron-main/windows.ts
475 | 接下来到了electron的windows窗口,open方法在doOpen中执行窗口配置初始化,最终调用openInBrowserWindow -> 执行doOpenInBrowserWindow是其打开window,主要步骤如下:
476 | ```js
477 | private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow {
478 |
479 | ...
480 | // New window
481 | if (!window) {
482 | //1.判断是否全屏创建窗口
483 | ...
484 | // 2. 创建实例窗口
485 | window = this.instantiationService.createInstance(CodeWindow, {
486 | state,
487 | extensionDevelopmentPath: configuration.extensionDevelopmentPath,
488 | isExtensionTestHost: !!configuration.extensionTestsPath
489 | });
490 |
491 | // 3.添加到当前窗口控制器
492 | WindowsManager.WINDOWS.push(window);
493 |
494 | // 4.窗口监听器
495 | window.win.webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own
496 | window.win.webContents.on('devtools-reload-page', () => this.reload(window!));
497 | window.win.webContents.on('crashed', () => this.onWindowError(window!, WindowError.CRASHED));
498 | window.win.on('unresponsive', () => this.onWindowError(window!, WindowError.UNRESPONSIVE));
499 | window.win.on('closed', () => this.onWindowClosed(window!));
500 |
501 | // 5.注册窗口生命周期
502 | (this.lifecycleService as LifecycleService).registerWindow(window);
503 | }
504 |
505 | ...
506 |
507 | return window;
508 | }
509 | ```
510 | doOpenInBrowserWindow会调用window.load方法 在window.ts中实现
511 | ```js
512 | load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void {
513 |
514 | ...
515 |
516 | // Load URL
517 | perf.mark('main:loadWindow');
518 | this._win.loadURL(this.getUrl(configuration));
519 |
520 | ...
521 | }
522 |
523 | private getUrl(windowConfiguration: IWindowConfiguration): string {
524 |
525 | ...
526 | //加载欢迎屏幕的html
527 | let configUrl = this.doGetUrl(config);
528 | ...
529 | return configUrl;
530 | }
531 |
532 | //默认加载 vs/code/electron-browser/workbench/workbench.html
533 | private doGetUrl(config: object): string {
534 | return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
535 | }
536 | ```
537 | main process的使命完成, 主界面进行构建布局。
538 |
539 |
540 | 在workbench.html中加载了workbench.js,
541 | 这里调用return require('vs/workbench/electron-browser/desktop.main').main(configuration);实现对主界面的展示
542 |
543 |
544 | #### vs/workbench/electron-browser/desktop.main.ts
545 | 创建工作区,调用workbench.startup()方法,构建主界面展示布局
546 | ```js
547 | ...
548 | async open(): Promise {
549 | const services = await this.initServices();
550 | await domContentLoaded();
551 | mark('willStartWorkbench');
552 |
553 | // 1.创建工作区
554 | const workbench = new Workbench(document.body, services.serviceCollection, services.logService);
555 |
556 | // 2.监听窗口变化
557 | this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true, workbench)));
558 |
559 | // 3.工作台生命周期
560 | this._register(workbench.onShutdown(() => this.dispose()));
561 | this._register(workbench.onWillShutdown(event => event.join(services.storageService.close())));
562 |
563 | // 3.启动工作区
564 | const instantiationService = workbench.startup();
565 |
566 | ...
567 | }
568 | ...
569 | ```
570 |
571 | #### vs/workbench/browser/workbench.ts
572 | 工作区继承自layout类,主要作用是构建工作区,创建界面布局。
573 | ```js
574 | export class Workbench extends Layout {
575 | ...
576 | startup(): IInstantiationService {
577 | try {
578 | ...
579 |
580 | // Services
581 | const instantiationService = this.initServices(this.serviceCollection);
582 |
583 | instantiationService.invokeFunction(async accessor => {
584 | const lifecycleService = accessor.get(ILifecycleService);
585 | const storageService = accessor.get(IStorageService);
586 | const configurationService = accessor.get(IConfigurationService);
587 |
588 | // Layout
589 | this.initLayout(accessor);
590 |
591 | // Registries
592 | this.startRegistries(accessor);
593 |
594 | // Context Keys
595 | this._register(instantiationService.createInstance(WorkbenchContextKeysHandler));
596 |
597 | // 注册监听事件
598 | this.registerListeners(lifecycleService, storageService, configurationService);
599 |
600 | // 渲染工作区
601 | this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);
602 |
603 | // 创建工作区布局
604 | this.createWorkbenchLayout(instantiationService);
605 |
606 | // 布局构建
607 | this.layout();
608 |
609 | // Restore
610 | try {
611 | await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService);
612 | } catch (error) {
613 | onUnexpectedError(error);
614 | }
615 | });
616 |
617 | return instantiationService;
618 | } catch (error) {
619 | onUnexpectedError(error);
620 |
621 | throw error; // rethrow because this is a critical issue we cannot handle properly here
622 | }
623 | }
624 | ...
625 | }
626 | ```
627 |
628 | ## 事件分发
629 | ### event
630 | src/vs/base/common/event.ts
631 |
632 | 程序中常见使用once方法进行事件绑定, 给定一个事件,返回一个只触发一次的事件,放在匿名函数返回
633 | ```js
634 | export function once(event: Event): Event {
635 | return (listener, thisArgs = null, disposables?) => {
636 | // 设置次变量,防止事件重复触发造成事件污染
637 | let didFire = false;
638 | let result: IDisposable;
639 | result = event(e => {
640 | if (didFire) {
641 | return;
642 | } else if (result) {
643 | result.dispose();
644 | } else {
645 | didFire = true;
646 | }
647 |
648 | return listener.call(thisArgs, e);
649 | }, null, disposables);
650 |
651 | if (didFire) {
652 | result.dispose();
653 | }
654 |
655 | return result;
656 | };
657 | }
658 | ```
659 | 循环派发了所有注册的事件, 事件会存储到一个事件队列,通过fire方法触发事件
660 |
661 | private _deliveryQueue?: LinkedList<[Listener, T]>;//事件存储队列
662 |
663 | ```js
664 | fire(event: T): void {
665 | if (this._listeners) {
666 | // 将所有事件传入 delivery queue
667 | // 内部/嵌套方式通过emit发出.
668 | // this调用事件驱动
669 |
670 | if (!this._deliveryQueue) {
671 | this._deliveryQueue = new LinkedList();
672 | }
673 |
674 | for (let iter = this._listeners.iterator(), e = iter.next(); !e.done; e = iter.next()) {
675 | this._deliveryQueue.push([e.value, event]);
676 | }
677 |
678 | while (this._deliveryQueue.size > 0) {
679 | const [listener, event] = this._deliveryQueue.shift()!;
680 | try {
681 | if (typeof listener === 'function') {
682 | listener.call(undefined, event);
683 | } else {
684 | listener[0].call(listener[1], event);
685 | }
686 | } catch (e) {
687 | onUnexpectedError(e);
688 | }
689 | }
690 | }
691 | }
692 | ```
693 |
694 | ## 进程通信
695 |
696 | ### 主进程
697 | src/vs/code/electron-main/main.ts
698 |
699 | main.ts在启动应用后就创建了一个主进程 main process,它可以通过electron中的一些模块直接与原生GUI交互。
700 | ```js
701 | server = await serve(environmentService.mainIPCHandle);
702 | once(lifecycleService.onWillShutdown)(() => server.dispose());
703 | ```
704 | ### 渲染进程
705 | 仅启动主进程并不能给你的应用创建应用窗口。窗口是通过main文件里的主进程调用叫BrowserWindow的模块创建的。
706 | ### 主进程与渲染进程之间的通信
707 | 在electron中,主进程与渲染进程有很多通信的方法。比如ipcRenderer和ipcMain,还可以在渲染进程使用remote模块。
708 |
709 | ### ipcMain & ipcRenderer
710 | * 主进程:ipcMain
711 | * 渲染进程:ipcRenderer
712 |
713 | ipcMain模块和ipcRenderer是类EventEmitter的实例。
714 |
715 | 在主进程中使用ipcMain接收渲染线程发送过来的异步或同步消息,发送过来的消息将触发事件。
716 |
717 | 在渲染进程中使用ipcRenderer向主进程发送同步或异步消息,也可以接收到主进程的消息。
718 |
719 | * 发送消息,事件名为 channel .
720 | * 回应同步消息, 你可以设置 event.returnValue .
721 | * 回应异步消息, 你可以使用 event.sender.send(...)
722 |
723 | 创建IPC服务
724 | src/vs/base/parts/ipc/node/ipc.net.ts
725 |
726 | 这里返回一个promise对象,成功则createServer
727 |
728 | ```js
729 | export function serve(hook: any): Promise {
730 | return new Promise((c, e) => {
731 | const server = createServer();
732 |
733 | server.on('error', e);
734 | server.listen(hook, () => {
735 | server.removeListener('error', e);
736 | c(new Server(server));
737 | });
738 | });
739 | }
740 | ```
741 |
742 | #### 创建信道
743 |
744 | src/vs/code/electron-main/app.ts
745 |
746 | * mainIpcServer
747 | * launchChannel
748 |
749 | * electronIpcServer
750 | * updateChannel
751 | * issueChannel
752 | * workspacesChannel
753 | * windowsChannel
754 | * menubarChannel
755 | * urlChannel
756 | * storageChannel
757 | * logLevelChannel
758 |
759 | ```js
760 | private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>): ICodeWindow[] {
761 |
762 | // Register more Main IPC services
763 | const launchService = accessor.get(ILaunchService);
764 | const launchChannel = new LaunchChannel(launchService);
765 | this.mainIpcServer.registerChannel('launch', launchChannel);
766 |
767 | // Register more Electron IPC services
768 | const updateService = accessor.get(IUpdateService);
769 | const updateChannel = new UpdateChannel(updateService);
770 | electronIpcServer.registerChannel('update', updateChannel);
771 |
772 | const issueService = accessor.get(IIssueService);
773 | const issueChannel = new IssueChannel(issueService);
774 | electronIpcServer.registerChannel('issue', issueChannel);
775 |
776 | const workspacesService = accessor.get(IWorkspacesMainService);
777 | const workspacesChannel = new WorkspacesChannel(workspacesService);
778 | electronIpcServer.registerChannel('workspaces', workspacesChannel);
779 |
780 | const windowsService = accessor.get(IWindowsService);
781 | const windowsChannel = new WindowsChannel(windowsService);
782 | electronIpcServer.registerChannel('windows', windowsChannel);
783 | sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel));
784 |
785 | const menubarService = accessor.get(IMenubarService);
786 | const menubarChannel = new MenubarChannel(menubarService);
787 | electronIpcServer.registerChannel('menubar', menubarChannel);
788 |
789 | const urlService = accessor.get(IURLService);
790 | const urlChannel = new URLServiceChannel(urlService);
791 | electronIpcServer.registerChannel('url', urlChannel);
792 |
793 | const storageMainService = accessor.get(IStorageMainService);
794 | const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService));
795 | electronIpcServer.registerChannel('storage', storageChannel);
796 |
797 | // Log level management
798 | const logLevelChannel = new LogLevelSetterChannel(accessor.get(ILogService));
799 | electronIpcServer.registerChannel('loglevel', logLevelChannel);
800 | sharedProcessClient.then(client => client.registerChannel('loglevel', logLevelChannel));
801 |
802 | ...
803 |
804 | // default: read paths from cli
805 | return windowsMainService.open({
806 | context,
807 | cli: args,
808 | forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
809 | diffMode: args.diff,
810 | noRecentEntry,
811 | waitMarkerFileURI,
812 | gotoLineMode: args.goto,
813 | initialStartup: true
814 | });
815 | }
816 | ```
817 |
818 | 每一个信道,内部实现两个方法 listen和call
819 |
820 | 例如:src/vs/platform/localizations/node/localizationsIpc.ts
821 |
822 | 构造函数绑定事件
823 | ```js
824 | export class LocalizationsChannel implements IServerChannel {
825 |
826 | onDidLanguagesChange: Event;
827 |
828 | constructor(private service: ILocalizationsService) {
829 | this.onDidLanguagesChange = Event.buffer(service.onDidLanguagesChange, true);
830 | }
831 |
832 | listen(_: unknown, event: string): Event {
833 | switch (event) {
834 | case 'onDidLanguagesChange': return this.onDidLanguagesChange;
835 | }
836 |
837 | throw new Error(`Event not found: ${event}`);
838 | }
839 |
840 | call(_: unknown, command: string, arg?: any): Promise {
841 | switch (command) {
842 | case 'getLanguageIds': return this.service.getLanguageIds(arg);
843 | }
844 |
845 | throw new Error(`Call not found: ${command}`);
846 | }
847 | }
848 |
849 |
850 | ```
851 |
852 | ## 主要窗口
853 |
854 | workbench.ts中startup里面Workbench负责创建主界面
855 | src/vs/workbench/browser/workbench.ts
856 |
857 | ```js
858 | startup(): IInstantiationService {
859 | try {
860 |
861 | ...
862 |
863 | instantiationService.invokeFunction(async accessor => {
864 |
865 | // 渲染主工作界面
866 | this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);
867 |
868 | // 界面布局
869 | this.createWorkbenchLayout(instantiationService);
870 |
871 | // Layout
872 | this.layout();
873 |
874 | // Restore
875 | try {
876 | await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService);
877 | } catch (error) {
878 | onUnexpectedError(error);
879 | }
880 | });
881 |
882 | return instantiationService;
883 | } catch (error) {
884 | onUnexpectedError(error);
885 |
886 | throw error; // rethrow because this is a critical issue we cannot handle properly here
887 | }
888 | }
889 | ```
890 |
891 | 渲染主工作台,渲染完之后加入到container中,container加入到parent, parent就是body了。
892 |
893 | this.parent.appendChild(this.container);
894 |
895 | ```js
896 | private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void {
897 |
898 | ...
899 |
900 | //TITLEBAR_PART 顶部操作栏
901 | //ACTIVITYBAR_PART 最左侧菜单选项卡
902 | //SIDEBAR_PART 左侧边栏,显示文件,结果展示等
903 | //EDITOR_PART 右侧窗口,代码编写,欢迎界面等
904 | //STATUSBAR_PART 底部状态栏
905 | [
906 | { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] },
907 | { id: Parts.ACTIVITYBAR_PART, role: 'navigation', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] },
908 | { id: Parts.SIDEBAR_PART, role: 'complementary', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] },
909 | { id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.state.editor.restoreEditors } },
910 | { id: Parts.PANEL_PART, role: 'complementary', classes: ['panel', this.state.panel.position === Position.BOTTOM ? 'bottom' : 'right'] },
911 | { id: Parts.STATUSBAR_PART, role: 'contentinfo', classes: ['statusbar'] }
912 | ].forEach(({ id, role, classes, options }) => {
913 | const partContainer = this.createPart(id, role, classes);
914 |
915 | if (!configurationService.getValue('workbench.useExperimentalGridLayout')) {
916 | // TODO@Ben cleanup once moved to grid
917 | // Insert all workbench parts at the beginning. Issue #52531
918 | // This is primarily for the title bar to allow overriding -webkit-app-region
919 | this.container.insertBefore(partContainer, this.container.lastChild);
920 | }
921 |
922 | this.getPart(id).create(partContainer, options);
923 | });
924 |
925 | // 将工作台添加至container dom渲染
926 | this.parent.appendChild(this.container);
927 | }
928 | ```
929 |
930 | workbench最后调用this.layout()方法,将窗口占据整个界面,渲染完成
931 | ```js
932 | layout(options?: ILayoutOptions): void {
933 | if (!this.disposed) {
934 | this._dimension = getClientArea(this.parent);
935 |
936 | if (this.workbenchGrid instanceof Grid) {
937 | position(this.container, 0, 0, 0, 0, 'relative');
938 | size(this.container, this._dimension.width, this._dimension.height);
939 |
940 | // Layout the grid widget
941 | this.workbenchGrid.layout(this._dimension.width, this._dimension.height);
942 | } else {
943 | this.workbenchGrid.layout(options);
944 | }
945 |
946 | // Emit as event
947 | this._onLayout.fire(this._dimension);
948 | }
949 | }
950 | ```
951 |
952 | ## 开发调试
953 |
954 | ```js
955 | app.once('ready', function () {
956 | //启动追踪
957 | if (args['trace']) {
958 | // @ts-ignore
959 | const contentTracing = require('electron').contentTracing;
960 |
961 | const traceOptions = {
962 | categoryFilter: args['trace-category-filter'] || '*',
963 | traceOptions: args['trace-options'] || 'record-until-full,enable-sampling'
964 | };
965 |
966 | contentTracing.startRecording(traceOptions, () => onReady());
967 | } else {
968 | onReady();
969 | }
970 | });
971 | ```
972 | ### 启动追踪
973 | 这里如果传入trace参数,在onReady启动之前会调用chromium的收集跟踪数据,
974 | 提供的底层的追踪工具允许我们深度了解 V8 的解析以及其他时间消耗情况,
975 |
976 | 一旦收到可以开始记录的请求,记录将会立马启动并且在子进程是异步记录听的. 当所有的子进程都收到 startRecording 请求的时候,callback 将会被调用.
977 |
978 | categoryFilter是一个过滤器,它用来控制那些分类组应该被用来查找.过滤器应当有一个可选的 - 前缀来排除匹配的分类组.不允许同一个列表既是包含又是排斥.
979 |
980 | #### contentTracing.startRecording(options, callback)
981 | * options Object
982 | * categoryFilter String
983 | * traceOptions String
984 | * callback Function
985 |
986 | [关于trace的详细介绍](https://www.w3cschool.cn/electronmanual/electronmanual-content-tracing.html)
987 |
988 | ### 结束追踪
989 | #### contentTracing.stopRecording(resultFilePath, callback)
990 | * resultFilePath String
991 | * callback Function
992 | 在成功启动窗口后,程序结束性能追踪,停止对所有子进程的记录.
993 |
994 | 子进程通常缓存查找数据,并且仅仅将数据截取和发送给主进程.这有利于在通过 IPC 发送查找数据之前减小查找时的运行开销,这样做很有价值.因此,发送查找数据,我们应当异步通知所有子进程来截取任何待查找的数据.
995 |
996 | 一旦所有子进程接收到了 stopRecording 请求,将调用 callback ,并且返回一个包含查找数据的文件.
997 |
998 | 如果 resultFilePath 不为空,那么将把查找数据写入其中,否则写入一个临时文件.实际文件路径如果不为空,则将调用 callback .
999 |
1000 | ### debug
1001 | 调试界面在菜单栏找到 Help->Toggle Developers Tools
1002 |
1003 | 调出Chrome开发者调试工具进行调试
1004 | 
1005 |
1006 |
1007 | ## 参考
1008 | https://electronjs.org/docs
1009 |
1010 | https://github.com/microsoft/vscode/wiki/How-to-Contribute
1011 |
1012 | https://github.com/Microsoft/vscode/wiki/Code-Organization
1013 |
1014 | http://xzper.com/2016/04/17/vscode%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90/
1015 |
1016 | http://www.ayqy.net/blog/vs-code%E6%BA%90%E7%A0%81%E7%AE%80%E6%9E%90/
1017 |
1018 |
--------------------------------------------------------------------------------
/chapter-1.md:
--------------------------------------------------------------------------------
1 | # VSCode源码分析 - 主启动流程
2 | ## 目录
3 | * 启动主进程
4 | * 实例化服务
5 | * 事件分发
6 | * 进程通信
7 | * 主要窗口
8 | * 开发调试
9 |
10 | ## 简介
11 | Visual Studio Code(简称VSCode) 是开源免费的IDE编辑器,原本是微软内部使用的云编辑器(Monaco)。
12 |
13 | git仓库地址: https://github.com/microsoft/vscode
14 |
15 | 通过Eletron集成了桌面应用,可以跨平台使用,开发语言主要采用微软自家的TypeScript。
16 | 整个项目结构比较清晰,方便阅读代码理解。成为了最流行跨平台的桌面IDE应用
17 |
18 | 微软希望VSCode在保持核心轻量级的基础上,增加项目支持,智能感知,编译调试。
19 | 
20 |
21 | ## 编译安装
22 | 下载最新版本,目前我用的是1.37.1版本
23 | 官方的wiki中有编译安装的说明 [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute?_blank)
24 |
25 | Linux, Window, MacOS三个系统编译时有些差别,参考官方文档,
26 | 在编译安装依赖时如果遇到connect timeout, 需要进行科学上网。
27 |
28 | > 需要注意的一点 运行环境依赖版本 Nodejs x64 version >= 10.16.0, < 11.0.0, python 2.7(3.0不能正常执行)
29 |
30 |
31 | ## 技术架构
32 | 
33 |
34 | ### Electron
35 |
36 | > [Electron](https://electronjs.org/?_blank) 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可 (Electron = Node.js + Chromium + Native API)
37 |
38 | ### Monaco Editor
39 | > [Monaco Editor](https://github.com/microsoft/monaco-editor?_blank)是微软开源项目, 为VS Code提供支持的代码编辑器,运行在浏览器环境中。编辑器提供代码提示,智能建议等功能。供开发人员远程更方便的编写代码,可独立运行。
40 |
41 | ### TypeScript
42 | > TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程
43 |
44 | ## 目录结构
45 | ```
46 | ├── build # gulp编译构建脚本
47 | ├── extensions # 内置插件
48 | ├── product.json # App meta信息
49 | ├── resources # 平台相关静态资源
50 | ├── scripts # 工具脚本,开发/测试
51 | ├── src # 源码目录
52 | └── typings # 函数语法补全定义
53 | └── vs
54 | ├── base # 通用工具/协议和UI库
55 | │ ├── browser # 基础UI组件,DOM操作
56 | │ ├── common # diff描述,markdown解析器,worker协议,各种工具函数
57 | │ ├── node # Node工具函数
58 | │ ├── parts # IPC协议(Electron、Node),quickopen、tree组件
59 | │ ├── test # base单测用例
60 | │ └── worker # Worker factory和main Worker(运行IDE Core:Monaco)
61 | ├── code # VSCode主运行窗口
62 | ├── editor # IDE代码编辑器
63 | | ├── browser # 代码编辑器核心
64 | | ├── common # 代码编辑器核心
65 | | ├── contrib # vscode 与独立 IDE共享的代码
66 | | └── standalone # 独立 IDE 独有的代码
67 | ├── platform # 支持注入服务和平台相关基础服务(文件、剪切板、窗体、状态栏)
68 | ├── workbench # 工作区UI布局,功能主界面
69 | │ ├── api #
70 | │ ├── browser #
71 | │ ├── common #
72 | │ ├── contrib #
73 | │ ├── electron-browser #
74 | │ ├── services #
75 | │ └── test #
76 | ├── css.build.js # 用于插件构建的CSS loader
77 | ├── css.js # CSS loader
78 | ├── editor # 对接IDE Core(读取编辑/交互状态),提供命令、上下文菜单、hover、snippet等支持
79 | ├── loader.js # AMD loader(用于异步加载AMD模块)
80 | ├── nls.build.js # 用于插件构建的NLS loader
81 | └── nls.js # NLS(National Language Support)多语言loader
82 | ```
83 | ### 核心层
84 | * base: 提供通用服务和构建用户界面
85 | * platform: 注入服务和基础服务代码
86 | * editor: 微软Monaco编辑器,也可独立运行使用
87 | * wrokbench: 配合Monaco并且给viewlets提供框架:如:浏览器状态栏,菜单栏利用electron实现桌面程序
88 |
89 | ### 核心环境
90 | 整个项目完全使用typescript实现,electron中运行主进程和渲染进程,使用的api有所不同,所以在core中每个目录组织也是按照使用的api来安排,
91 | 运行的环境分为几类:
92 | * common: 只使用javascritp api的代码,能在任何环境下运行
93 | * browser: 浏览器api, 如操作dom; 可以调用common
94 | * node: 需要使用node的api,比如文件io操作
95 | * electron-brower: 渲染进程api, 可以调用common, brower, node, 依赖[electron renderer-process API](https://github.com/electron/electron/tree/master/docs#modules-for-the-renderer-process-web-page)
96 | * electron-main: 主进程api, 可以调用: common, node 依赖于[electron main-process AP](https://github.com/electron/electron/tree/master/docs#modules-for-the-main-process)
97 |
98 | 
99 |
100 |
101 | ## 启动主进程
102 |
103 | ### Electron通过package.json中的main字段来定义应用入口。
104 | main.js是vscode的入口。
105 |
106 | * src/main.js
107 | * vs/code/electron-main/main.ts
108 | * vs/code/electron-main/app.ts
109 | * vs/code/electron-main/windows.ts
110 | * vs/workbench/electron-browser/desktop.main.ts
111 | * vs/workbench/browser/workbench.ts
112 |
113 | ```js
114 | app.once('ready', function () {
115 | //启动追踪,后面会讲到,跟性能检测优化相关。
116 | if (args['trace']) {
117 | // @ts-ignore
118 | const contentTracing = require('electron').contentTracing;
119 |
120 | const traceOptions = {
121 | categoryFilter: args['trace-category-filter'] || '*',
122 | traceOptions: args['trace-options'] || 'record-until-full,enable-sampling'
123 | };
124 |
125 | contentTracing.startRecording(traceOptions, () => onReady());
126 | } else {
127 | onReady();
128 | }
129 | });
130 | function onReady() {
131 | perf.mark('main:appReady');
132 |
133 | Promise.all([nodeCachedDataDir.ensureExists(), userDefinedLocale]).then(([cachedDataDir, locale]) => {
134 | //1. 这里尝试获取本地配置信息,如果有的话会传递到startup
135 | if (locale && !nlsConfiguration) {
136 | nlsConfiguration = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale);
137 | }
138 |
139 | if (!nlsConfiguration) {
140 | nlsConfiguration = Promise.resolve(undefined);
141 | }
142 |
143 |
144 | nlsConfiguration.then(nlsConfig => {
145 |
146 | //4. 首先会检查用户语言环境配置,如果没有设置默认使用英语
147 | const startup = nlsConfig => {
148 | nlsConfig._languagePackSupport = true;
149 | process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
150 | process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';
151 |
152 | perf.mark('willLoadMainBundle');
153 | //使用微软的loader组件加载electron-main/main文件
154 | require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
155 | perf.mark('didLoadMainBundle');
156 | });
157 | };
158 |
159 | // 2. 接收到有效的配置传入是其生效,调用startup
160 | if (nlsConfig) {
161 | startup(nlsConfig);
162 | }
163 |
164 | // 3. 这里尝试使用本地的应用程序
165 | // 应用程序设置区域在ready事件后才有效
166 | else {
167 | let appLocale = app.getLocale();
168 | if (!appLocale) {
169 | startup({ locale: 'en', availableLanguages: {} });
170 | } else {
171 |
172 | // 配置兼容大小写敏感,所以统一转换成小写
173 | appLocale = appLocale.toLowerCase();
174 | // 这里就会调用config服务,把本地配置加载进来再调用startup
175 | lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale).then(nlsConfig => {
176 | if (!nlsConfig) {
177 | nlsConfig = { locale: appLocale, availableLanguages: {} };
178 | }
179 |
180 | startup(nlsConfig);
181 | });
182 | }
183 | }
184 | });
185 | }, console.error);
186 | }
187 | ```
188 |
189 | ### vs/code/electron-main/main.ts
190 | electron-main/main 是程序真正启动的入口,进入main process初始化流程.
191 | #### 这里主要做了两件事情:
192 | 1. 初始化Service
193 | 2. 启动主实例
194 |
195 | 直接看startup方法的实现,基础服务初始化完成后会加载 CodeApplication, mainIpcServer, instanceEnvironment,调用 startup 方法启动APP
196 | ```js
197 | private async startup(args: ParsedArgs): Promise {
198 |
199 | //spdlog 日志服务
200 | const bufferLogService = new BufferLogService();
201 |
202 | // 1. 调用 createServices
203 | const [instantiationService, instanceEnvironment] = this.createServices(args, bufferLogService);
204 | try {
205 |
206 | // 1.1 初始化Service服务
207 | await instantiationService.invokeFunction(async accessor => {
208 | // 基础服务,包括一些用户数据,缓存目录
209 | const environmentService = accessor.get(IEnvironmentService);
210 | // 配置服务
211 | const configurationService = accessor.get(IConfigurationService);
212 | // 持久化数据
213 | const stateService = accessor.get(IStateService);
214 |
215 | try {
216 | await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService);
217 | } catch (error) {
218 |
219 | // 抛出错误对话框
220 | this.handleStartupDataDirError(environmentService, error);
221 |
222 | throw error;
223 | }
224 | });
225 |
226 | // 1.2 启动实例
227 | await instantiationService.invokeFunction(async accessor => {
228 | const environmentService = accessor.get(IEnvironmentService);
229 | const logService = accessor.get(ILogService);
230 | const lifecycleService = accessor.get(ILifecycleService);
231 | const configurationService = accessor.get(IConfigurationService);
232 |
233 | const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleService, instantiationService, true);
234 |
235 | bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel());
236 | once(lifecycleService.onWillShutdown)(() => (configurationService as ConfigurationService).dispose());
237 |
238 | return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
239 | });
240 | } catch (error) {
241 | instantiationService.invokeFunction(this.quit, error);
242 | }
243 | }
244 | ```
245 |
246 |
247 | #### vs/code/electron-main/app.ts
248 | 这里首先触发CodeApplication.startup()方法, 在第一个窗口打开3秒后成为共享进程,
249 | ```js
250 | async startup(): Promise {
251 | ...
252 |
253 | // 1. 第一个窗口创建共享进程
254 | const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv);
255 | const sharedProcessClient = sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main'));
256 | this.lifecycleService.when(LifecycleMainPhase.AfterWindowOpen).then(() => {
257 | this._register(new RunOnceScheduler(async () => {
258 | const userEnv = await getShellEnvironment(this.logService, this.environmentService);
259 |
260 | sharedProcess.spawn(userEnv);
261 | }, 3000)).schedule();
262 | });
263 | // 2. 创建app实例
264 | const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient);
265 |
266 |
267 | // 3. 打开一个窗口 调用
268 |
269 | const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));
270 |
271 | // 4. 窗口打开后执行生命周期和授权操作
272 | this.afterWindowOpen();
273 | ...
274 |
275 | //vscode结束了性能问题的追踪
276 | if (this.environmentService.args.trace) {
277 | this.stopTracingEventually(windows);
278 | }
279 | }
280 | ```
281 |
282 |
283 | openFirstWindow 主要实现
284 | CodeApplication.openFirstWindow 首次开启窗口时,创建 Electron 的 IPC,使主进程和渲染进程间通信。
285 | window会被注册到sharedProcessClient,主进程和共享进程通信
286 | 根据 environmentService 提供的参数(path,uri)调用windowsMainService.open 方法打开窗口
287 | ```js
288 | private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>): ICodeWindow[] {
289 |
290 | ...
291 | // 1. 注入Electron IPC Service, windows窗口管理,菜单栏等服务
292 |
293 | // 2. 根据environmentService进行参数配置
294 | const macOpenFiles: string[] = (global).macOpenFiles;
295 | const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
296 | const hasCliArgs = hasArgs(args._);
297 | const hasFolderURIs = hasArgs(args['folder-uri']);
298 | const hasFileURIs = hasArgs(args['file-uri']);
299 | const noRecentEntry = args['skip-add-to-recently-opened'] === true;
300 | const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;
301 |
302 | ...
303 |
304 | // 打开主窗口,默认从执行命令行中读取参数
305 | return windowsMainService.open({
306 | context,
307 | cli: args,
308 | forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
309 | diffMode: args.diff,
310 | noRecentEntry,
311 | waitMarkerFileURI,
312 | gotoLineMode: args.goto,
313 | initialStartup: true
314 | });
315 | }
316 | ```
317 |
318 |
319 |
320 | #### vs/code/electron-main/windows.ts
321 | 接下来到了electron的windows窗口,open方法在doOpen中执行窗口配置初始化,最终调用openInBrowserWindow -> 执行doOpenInBrowserWindow是其打开window,主要步骤如下:
322 | ```js
323 | private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow {
324 |
325 | ...
326 | // New window
327 | if (!window) {
328 | //1.判断是否全屏创建窗口
329 | ...
330 | // 2. 创建实例窗口
331 | window = this.instantiationService.createInstance(CodeWindow, {
332 | state,
333 | extensionDevelopmentPath: configuration.extensionDevelopmentPath,
334 | isExtensionTestHost: !!configuration.extensionTestsPath
335 | });
336 |
337 | // 3.添加到当前窗口控制器
338 | WindowsManager.WINDOWS.push(window);
339 |
340 | // 4.窗口监听器
341 | window.win.webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own
342 | window.win.webContents.on('devtools-reload-page', () => this.reload(window!));
343 | window.win.webContents.on('crashed', () => this.onWindowError(window!, WindowError.CRASHED));
344 | window.win.on('unresponsive', () => this.onWindowError(window!, WindowError.UNRESPONSIVE));
345 | window.win.on('closed', () => this.onWindowClosed(window!));
346 |
347 | // 5.注册窗口生命周期
348 | (this.lifecycleService as LifecycleService).registerWindow(window);
349 | }
350 |
351 | ...
352 |
353 | return window;
354 | }
355 | ```
356 | doOpenInBrowserWindow会调用window.load方法 在window.ts中实现
357 | ```js
358 | load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void {
359 |
360 | ...
361 |
362 | // Load URL
363 | perf.mark('main:loadWindow');
364 | this._win.loadURL(this.getUrl(configuration));
365 |
366 | ...
367 | }
368 |
369 | private getUrl(windowConfiguration: IWindowConfiguration): string {
370 |
371 | ...
372 | //加载欢迎屏幕的html
373 | let configUrl = this.doGetUrl(config);
374 | ...
375 | return configUrl;
376 | }
377 |
378 | //默认加载 vs/code/electron-browser/workbench/workbench.html
379 | private doGetUrl(config: object): string {
380 | return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
381 | }
382 | ```
383 | main process的使命完成, 主界面进行构建布局。
384 |
385 |
386 | 在workbench.html中加载了workbench.js,
387 | 这里调用return require('vs/workbench/electron-browser/desktop.main').main(configuration);实现对主界面的展示
388 |
389 |
390 | #### vs/workbench/electron-browser/desktop.main.ts
391 | 创建工作区,调用workbench.startup()方法,构建主界面展示布局
392 | ```js
393 | ...
394 | async open(): Promise {
395 | const services = await this.initServices();
396 | await domContentLoaded();
397 | mark('willStartWorkbench');
398 |
399 | // 1.创建工作区
400 | const workbench = new Workbench(document.body, services.serviceCollection, services.logService);
401 |
402 | // 2.监听窗口变化
403 | this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true, workbench)));
404 |
405 | // 3.工作台生命周期
406 | this._register(workbench.onShutdown(() => this.dispose()));
407 | this._register(workbench.onWillShutdown(event => event.join(services.storageService.close())));
408 |
409 | // 3.启动工作区
410 | const instantiationService = workbench.startup();
411 |
412 | ...
413 | }
414 | ...
415 | ```
416 |
417 | #### vs/workbench/browser/workbench.ts
418 | 工作区继承自layout类,主要作用是构建工作区,创建界面布局。
419 | ```js
420 | export class Workbench extends Layout {
421 | ...
422 | startup(): IInstantiationService {
423 | try {
424 | ...
425 |
426 | // Services
427 | const instantiationService = this.initServices(this.serviceCollection);
428 |
429 | instantiationService.invokeFunction(async accessor => {
430 | const lifecycleService = accessor.get(ILifecycleService);
431 | const storageService = accessor.get(IStorageService);
432 | const configurationService = accessor.get(IConfigurationService);
433 |
434 | // Layout
435 | this.initLayout(accessor);
436 |
437 | // Registries
438 | this.startRegistries(accessor);
439 |
440 | // Context Keys
441 | this._register(instantiationService.createInstance(WorkbenchContextKeysHandler));
442 |
443 | // 注册监听事件
444 | this.registerListeners(lifecycleService, storageService, configurationService);
445 |
446 | // 渲染工作区
447 | this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);
448 |
449 | // 创建工作区布局
450 | this.createWorkbenchLayout(instantiationService);
451 |
452 | // 布局构建
453 | this.layout();
454 |
455 | // Restore
456 | try {
457 | await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService);
458 | } catch (error) {
459 | onUnexpectedError(error);
460 | }
461 | });
462 |
463 | return instantiationService;
464 | } catch (error) {
465 | onUnexpectedError(error);
466 |
467 | throw error; // rethrow because this is a critical issue we cannot handle properly here
468 | }
469 | }
470 | ...
471 | }
472 | ```
473 |
--------------------------------------------------------------------------------
/chapter-2.md:
--------------------------------------------------------------------------------
1 |
2 | # VSCode源码分析 - 实例化服务
3 |
4 | ## 目录
5 | * 启动主进程
6 | * 实例化服务
7 | * 事件分发
8 | * 进程通信
9 | * 主要窗口
10 | * 开发调试
11 |
12 |
13 | ## 实例化服务
14 |
15 | SyncDescriptor负责注册这些服务,当用到该服务时进程实例化使用
16 |
17 | src/vs/platform/instantiation/common/descriptors.ts
18 |
19 | ```js
20 | export class SyncDescriptor {
21 | readonly ctor: any;
22 | readonly staticArguments: any[];
23 | readonly supportsDelayedInstantiation: boolean;
24 | constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) {
25 | this.ctor = ctor;
26 | this.staticArguments = staticArguments;
27 | this.supportsDelayedInstantiation = supportsDelayedInstantiation;
28 | }
29 | }
30 | ```
31 |
32 | main.ts中startup方法调用invokeFunction.get实例化服务
33 | ```js
34 | await instantiationService.invokeFunction(async accessor => {
35 | const environmentService = accessor.get(IEnvironmentService);
36 | const configurationService = accessor.get(IConfigurationService);
37 | const stateService = accessor.get(IStateService);
38 | try {
39 | await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService);
40 | } catch (error) {
41 |
42 | // Show a dialog for errors that can be resolved by the user
43 | this.handleStartupDataDirError(environmentService, error);
44 |
45 | throw error;
46 | }
47 | });
48 | ```
49 |
50 | get方法调用_getOrCreateServiceInstance,这里第一次创建会存入缓存中
51 | 下次实例化对象时会优先从缓存中获取对象。
52 |
53 | src/vs/platform/instantiation/common/instantiationService.ts
54 |
55 | ```js
56 | invokeFunction(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R {
57 | let _trace = Trace.traceInvocation(fn);
58 | let _done = false;
59 | try {
60 | const accessor: ServicesAccessor = {
61 | get: (id: ServiceIdentifier, isOptional?: typeof optional) => {
62 |
63 | if (_done) {
64 | throw illegalState('service accessor is only valid during the invocation of its target method');
65 | }
66 |
67 | const result = this._getOrCreateServiceInstance(id, _trace);
68 | if (!result && isOptional !== optional) {
69 | throw new Error(`[invokeFunction] unknown service '${id}'`);
70 | }
71 | return result;
72 | }
73 | };
74 | return fn.apply(undefined, [accessor, ...args]);
75 | } finally {
76 | _done = true;
77 | _trace.stop();
78 | }
79 | }
80 | private _getOrCreateServiceInstance(id: ServiceIdentifier, _trace: Trace): T {
81 | let thing = this._getServiceInstanceOrDescriptor(id);
82 | if (thing instanceof SyncDescriptor) {
83 | return this._createAndCacheServiceInstance(id, thing, _trace.branch(id, true));
84 | } else {
85 | _trace.branch(id, false);
86 | return thing;
87 | }
88 | }
89 | ```
90 |
--------------------------------------------------------------------------------
/chapter-3.md:
--------------------------------------------------------------------------------
1 | # VSCode源码分析 - 事件分发
2 | ## 目录
3 | * 启动主进程
4 | * 实例化服务
5 | * 事件分发
6 | * 进程通信
7 | * 主要窗口
8 | * 开发调试
9 |
10 | ## 事件分发
11 | ### event
12 | src/vs/base/common/event.ts
13 |
14 | 程序中常见使用once方法进行事件绑定, 给定一个事件,返回一个只触发一次的事件,放在匿名函数返回
15 | ```js
16 | export function once(event: Event): Event {
17 | return (listener, thisArgs = null, disposables?) => {
18 | // 设置次变量,防止事件重复触发造成事件污染
19 | let didFire = false;
20 | let result: IDisposable;
21 | result = event(e => {
22 | if (didFire) {
23 | return;
24 | } else if (result) {
25 | result.dispose();
26 | } else {
27 | didFire = true;
28 | }
29 |
30 | return listener.call(thisArgs, e);
31 | }, null, disposables);
32 |
33 | if (didFire) {
34 | result.dispose();
35 | }
36 |
37 | return result;
38 | };
39 | }
40 | ```
41 | 循环派发了所有注册的事件, 事件会存储到一个事件队列,通过fire方法触发事件
42 |
43 | private _deliveryQueue?: LinkedList<[Listener, T]>;//事件存储队列
44 |
45 | ```js
46 | fire(event: T): void {
47 | if (this._listeners) {
48 | // 将所有事件传入 delivery queue
49 | // 内部/嵌套方式通过emit发出.
50 | // this调用事件驱动
51 |
52 | if (!this._deliveryQueue) {
53 | this._deliveryQueue = new LinkedList();
54 | }
55 |
56 | for (let iter = this._listeners.iterator(), e = iter.next(); !e.done; e = iter.next()) {
57 | this._deliveryQueue.push([e.value, event]);
58 | }
59 |
60 | while (this._deliveryQueue.size > 0) {
61 | const [listener, event] = this._deliveryQueue.shift()!;
62 | try {
63 | if (typeof listener === 'function') {
64 | listener.call(undefined, event);
65 | } else {
66 | listener[0].call(listener[1], event);
67 | }
68 | } catch (e) {
69 | onUnexpectedError(e);
70 | }
71 | }
72 | }
73 | }
74 | ```
75 |
--------------------------------------------------------------------------------
/chapter-4.md:
--------------------------------------------------------------------------------
1 | # VSCode源码分析 - 进程通信
2 | ## 目录
3 | * 启动主进程
4 | * 实例化服务
5 | * 事件分发
6 | * 进程通信
7 | * 主要窗口
8 | * 开发调试
9 |
10 | ## 进程通信
11 |
12 | ### 主进程
13 | src/vs/code/electron-main/main.ts
14 |
15 | main.ts在启动应用后就创建了一个主进程 main process,它可以通过electron中的一些模块直接与原生GUI交互。
16 | ```js
17 | server = await serve(environmentService.mainIPCHandle);
18 | once(lifecycleService.onWillShutdown)(() => server.dispose());
19 | ```
20 | ### 渲染进程
21 | 仅启动主进程并不能给你的应用创建应用窗口。窗口是通过main文件里的主进程调用叫BrowserWindow的模块创建的。
22 | ### 主进程与渲染进程之间的通信
23 | 在electron中,主进程与渲染进程有很多通信的方法。比如ipcRenderer和ipcMain,还可以在渲染进程使用remote模块。
24 |
25 | ### ipcMain & ipcRenderer
26 | * 主进程:ipcMain
27 | * 渲染进程:ipcRenderer
28 |
29 | ipcMain模块和ipcRenderer是类EventEmitter的实例。
30 |
31 | 在主进程中使用ipcMain接收渲染线程发送过来的异步或同步消息,发送过来的消息将触发事件。
32 |
33 | 在渲染进程中使用ipcRenderer向主进程发送同步或异步消息,也可以接收到主进程的消息。
34 |
35 | * 发送消息,事件名为 channel .
36 | * 回应同步消息, 你可以设置 event.returnValue .
37 | * 回应异步消息, 你可以使用 event.sender.send(...)
38 |
39 | 创建IPC服务
40 | src/vs/base/parts/ipc/node/ipc.net.ts
41 |
42 | 这里返回一个promise对象,成功则createServer
43 |
44 | ```js
45 | export function serve(hook: any): Promise {
46 | return new Promise((c, e) => {
47 | const server = createServer();
48 |
49 | server.on('error', e);
50 | server.listen(hook, () => {
51 | server.removeListener('error', e);
52 | c(new Server(server));
53 | });
54 | });
55 | }
56 | ```
57 |
58 | #### 创建信道
59 |
60 | src/vs/code/electron-main/app.ts
61 |
62 | * mainIpcServer
63 | * launchChannel
64 |
65 | * electronIpcServer
66 | * updateChannel
67 | * issueChannel
68 | * workspacesChannel
69 | * windowsChannel
70 | * menubarChannel
71 | * urlChannel
72 | * storageChannel
73 | * logLevelChannel
74 |
75 | ```js
76 | private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>): ICodeWindow[] {
77 |
78 | // Register more Main IPC services
79 | const launchService = accessor.get(ILaunchService);
80 | const launchChannel = new LaunchChannel(launchService);
81 | this.mainIpcServer.registerChannel('launch', launchChannel);
82 |
83 | // Register more Electron IPC services
84 | const updateService = accessor.get(IUpdateService);
85 | const updateChannel = new UpdateChannel(updateService);
86 | electronIpcServer.registerChannel('update', updateChannel);
87 |
88 | const issueService = accessor.get(IIssueService);
89 | const issueChannel = new IssueChannel(issueService);
90 | electronIpcServer.registerChannel('issue', issueChannel);
91 |
92 | const workspacesService = accessor.get(IWorkspacesMainService);
93 | const workspacesChannel = new WorkspacesChannel(workspacesService);
94 | electronIpcServer.registerChannel('workspaces', workspacesChannel);
95 |
96 | const windowsService = accessor.get(IWindowsService);
97 | const windowsChannel = new WindowsChannel(windowsService);
98 | electronIpcServer.registerChannel('windows', windowsChannel);
99 | sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel));
100 |
101 | const menubarService = accessor.get(IMenubarService);
102 | const menubarChannel = new MenubarChannel(menubarService);
103 | electronIpcServer.registerChannel('menubar', menubarChannel);
104 |
105 | const urlService = accessor.get(IURLService);
106 | const urlChannel = new URLServiceChannel(urlService);
107 | electronIpcServer.registerChannel('url', urlChannel);
108 |
109 | const storageMainService = accessor.get(IStorageMainService);
110 | const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService));
111 | electronIpcServer.registerChannel('storage', storageChannel);
112 |
113 | // Log level management
114 | const logLevelChannel = new LogLevelSetterChannel(accessor.get(ILogService));
115 | electronIpcServer.registerChannel('loglevel', logLevelChannel);
116 | sharedProcessClient.then(client => client.registerChannel('loglevel', logLevelChannel));
117 |
118 | ...
119 |
120 | // default: read paths from cli
121 | return windowsMainService.open({
122 | context,
123 | cli: args,
124 | forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
125 | diffMode: args.diff,
126 | noRecentEntry,
127 | waitMarkerFileURI,
128 | gotoLineMode: args.goto,
129 | initialStartup: true
130 | });
131 | }
132 | ```
133 |
134 | 每一个信道,内部实现两个方法 listen和call
135 |
136 | 例如:src/vs/platform/localizations/node/localizationsIpc.ts
137 |
138 | 构造函数绑定事件
139 | ```js
140 | export class LocalizationsChannel implements IServerChannel {
141 |
142 | onDidLanguagesChange: Event;
143 |
144 | constructor(private service: ILocalizationsService) {
145 | this.onDidLanguagesChange = Event.buffer(service.onDidLanguagesChange, true);
146 | }
147 |
148 | listen(_: unknown, event: string): Event {
149 | switch (event) {
150 | case 'onDidLanguagesChange': return this.onDidLanguagesChange;
151 | }
152 |
153 | throw new Error(`Event not found: ${event}`);
154 | }
155 |
156 | call(_: unknown, command: string, arg?: any): Promise {
157 | switch (command) {
158 | case 'getLanguageIds': return this.service.getLanguageIds(arg);
159 | }
160 |
161 | throw new Error(`Call not found: ${command}`);
162 | }
163 | }
164 |
165 |
166 | ```
167 |
--------------------------------------------------------------------------------
/chapter-5.md:
--------------------------------------------------------------------------------
1 | # VSCode源码分析 - 主要窗口
2 | ## 目录
3 | * 启动主进程
4 | * 实例化服务
5 | * 事件分发
6 | * 进程通信
7 | * 主要窗口
8 | * 开发调试
9 |
10 | ## 主要窗口
11 |
12 | workbench.ts中startup里面Workbench负责创建主界面
13 | src/vs/workbench/browser/workbench.ts
14 |
15 | ```js
16 | startup(): IInstantiationService {
17 | try {
18 |
19 | ...
20 |
21 | instantiationService.invokeFunction(async accessor => {
22 |
23 | // 渲染主工作界面
24 | this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);
25 |
26 | // 界面布局
27 | this.createWorkbenchLayout(instantiationService);
28 |
29 | // Layout
30 | this.layout();
31 |
32 | // Restore
33 | try {
34 | await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService);
35 | } catch (error) {
36 | onUnexpectedError(error);
37 | }
38 | });
39 |
40 | return instantiationService;
41 | } catch (error) {
42 | onUnexpectedError(error);
43 |
44 | throw error; // rethrow because this is a critical issue we cannot handle properly here
45 | }
46 | }
47 | ```
48 |
49 | 渲染主工作台,渲染完之后加入到container中,container加入到parent, parent就是body了。
50 |
51 | this.parent.appendChild(this.container);
52 |
53 | ```js
54 | private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void {
55 |
56 | ...
57 |
58 | //TITLEBAR_PART 顶部操作栏
59 | //ACTIVITYBAR_PART 最左侧菜单选项卡
60 | //SIDEBAR_PART 左侧边栏,显示文件,结果展示等
61 | //EDITOR_PART 右侧窗口,代码编写,欢迎界面等
62 | //STATUSBAR_PART 底部状态栏
63 | [
64 | { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] },
65 | { id: Parts.ACTIVITYBAR_PART, role: 'navigation', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] },
66 | { id: Parts.SIDEBAR_PART, role: 'complementary', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] },
67 | { id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.state.editor.restoreEditors } },
68 | { id: Parts.PANEL_PART, role: 'complementary', classes: ['panel', this.state.panel.position === Position.BOTTOM ? 'bottom' : 'right'] },
69 | { id: Parts.STATUSBAR_PART, role: 'contentinfo', classes: ['statusbar'] }
70 | ].forEach(({ id, role, classes, options }) => {
71 | const partContainer = this.createPart(id, role, classes);
72 |
73 | if (!configurationService.getValue('workbench.useExperimentalGridLayout')) {
74 | // TODO@Ben cleanup once moved to grid
75 | // Insert all workbench parts at the beginning. Issue #52531
76 | // This is primarily for the title bar to allow overriding -webkit-app-region
77 | this.container.insertBefore(partContainer, this.container.lastChild);
78 | }
79 |
80 | this.getPart(id).create(partContainer, options);
81 | });
82 |
83 | // 将工作台添加至container dom渲染
84 | this.parent.appendChild(this.container);
85 | }
86 | ```
87 |
88 | workbench最后调用this.layout()方法,将窗口占据整个界面,渲染完成
89 | ```js
90 | layout(options?: ILayoutOptions): void {
91 | if (!this.disposed) {
92 | this._dimension = getClientArea(this.parent);
93 |
94 | if (this.workbenchGrid instanceof Grid) {
95 | position(this.container, 0, 0, 0, 0, 'relative');
96 | size(this.container, this._dimension.width, this._dimension.height);
97 |
98 | // Layout the grid widget
99 | this.workbenchGrid.layout(this._dimension.width, this._dimension.height);
100 | } else {
101 | this.workbenchGrid.layout(options);
102 | }
103 |
104 | // Emit as event
105 | this._onLayout.fire(this._dimension);
106 | }
107 | }
108 | ```
109 |
--------------------------------------------------------------------------------
/chapter-6.md:
--------------------------------------------------------------------------------
1 |
2 | # VSCode源码分析 - 开发调试
3 |
4 | ## 目录
5 | * 启动主进程
6 | * 实例化服务
7 | * 事件分发
8 | * 进程通信
9 | * 主要窗口
10 | * 开发调试
11 |
12 | ## 开发调试
13 |
14 | ```js
15 | app.once('ready', function () {
16 | //启动追踪
17 | if (args['trace']) {
18 | // @ts-ignore
19 | const contentTracing = require('electron').contentTracing;
20 |
21 | const traceOptions = {
22 | categoryFilter: args['trace-category-filter'] || '*',
23 | traceOptions: args['trace-options'] || 'record-until-full,enable-sampling'
24 | };
25 |
26 | contentTracing.startRecording(traceOptions, () => onReady());
27 | } else {
28 | onReady();
29 | }
30 | });
31 | ```
32 | ### 启动追踪
33 | 这里如果传入trace参数,在onReady启动之前会调用chromium的收集跟踪数据,
34 | 提供的底层的追踪工具允许我们深度了解 V8 的解析以及其他时间消耗情况,
35 |
36 | 一旦收到可以开始记录的请求,记录将会立马启动并且在子进程是异步记录听的. 当所有的子进程都收到 startRecording 请求的时候,callback 将会被调用.
37 |
38 | categoryFilter是一个过滤器,它用来控制那些分类组应该被用来查找.过滤器应当有一个可选的 - 前缀来排除匹配的分类组.不允许同一个列表既是包含又是排斥.
39 |
40 | #### contentTracing.startRecording(options, callback)
41 | * options Object
42 | * categoryFilter String
43 | * traceOptions String
44 | * callback Function
45 |
46 | [关于trace的详细介绍](https://www.w3cschool.cn/electronmanual/electronmanual-content-tracing.html)
47 |
48 | ### 结束追踪
49 | #### contentTracing.stopRecording(resultFilePath, callback)
50 | * resultFilePath String
51 | * callback Function
52 | 在成功启动窗口后,程序结束性能追踪,停止对所有子进程的记录.
53 |
54 | 子进程通常缓存查找数据,并且仅仅将数据截取和发送给主进程.这有利于在通过 IPC 发送查找数据之前减小查找时的运行开销,这样做很有价值.因此,发送查找数据,我们应当异步通知所有子进程来截取任何待查找的数据.
55 |
56 | 一旦所有子进程接收到了 stopRecording 请求,将调用 callback ,并且返回一个包含查找数据的文件.
57 |
58 | 如果 resultFilePath 不为空,那么将把查找数据写入其中,否则写入一个临时文件.实际文件路径如果不为空,则将调用 callback .
59 |
60 | ### debug
61 | 调试界面在菜单栏找到 Help->Toggle Developers Tools
62 |
63 | 调出Chrome开发者调试工具进行调试
64 | 
65 |
--------------------------------------------------------------------------------
/vscode-debugg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-debugg.png
--------------------------------------------------------------------------------
/vscode-framework.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-framework.png
--------------------------------------------------------------------------------
/vscode-source.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-source.png
--------------------------------------------------------------------------------
/vscode-vside-zh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-vside-zh.png
--------------------------------------------------------------------------------
/vscode-vside.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-vside.png
--------------------------------------------------------------------------------
/vscode-welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-welcome.png
--------------------------------------------------------------------------------