• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

理解Electron架构三VSCode的窗口管理机制

武飞扬头像
孟健
帮助185

深入理解Electron架构(三)VSCode的窗口管理机制

背景

在 VS Code 中,窗口管理非常重要,它是工作台的核心之一。VS Code 的窗口管理通过将 UI 分为不同的部分来实现,每个部分都有不同的作用。例如,编辑器部分用于编辑文本,侧边栏用于显示不同的视图和面板,活动栏用于展示各种操作和功能。窗口管理还包括了窗口状态的管理,例如窗口的大小、位置、最大化、最小化等,以及对多个窗口的支持。

窗口管理对于用户体验和功能的实现都非常重要。良好的窗口管理可以提高用户的工作效率和舒适度,方便用户完成各种任务。在 VS Code 中,窗口管理也为插件开发提供了丰富的 API,可以实现各种功能和扩展。因此,我们接下来探寻一下窗口管理机制,加深对启动流程的理解。

CodeWindow的实现

export interface ICodeWindow extends IDisposable {

	readonly onWillLoad: Event<ILoadEvent>;
	readonly onDidSignalReady: Event<void>;
	readonly onDidTriggerSystemContextMenu: Event<{ x: number; y: number }>;
	readonly onDidClose: Event<void>;
	readonly onDidDestroy: Event<void>;

	readonly whenClosedOrLoaded: Promise<void>;

	readonly id: number;
	readonly win: BrowserWindow | null; /* `null` after being disposed */
	readonly config: INativeWindowConfiguration | undefined;

	readonly openedWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier;

	readonly profile?: IUserDataProfile;

	readonly backupPath?: string;

	readonly remoteAuthority?: string;

	readonly isExtensionDevelopmentHost: boolean;
	readonly isExtensionTestHost: boolean;

	readonly lastFocusTime: number;

	readonly isReady: boolean;
	ready(): Promise<ICodeWindow>;
	setReady(): void;

	readonly isSandboxed: boolean;

	addTabbedWindow(window: ICodeWindow): void;

	load(config: INativeWindowConfiguration, options?: { isReload?: boolean }): void;
	reload(cli?: NativeParsedArgs): void;

	focus(options?: { force: boolean }): void;
	close(): void;

	getBounds(): Rectangle;

	send(channel: string, ...args: any[]): void;
	sendWhenReady(channel: string, token: CancellationToken, ...args: any[]): void;

	readonly isFullScreen: boolean;
	toggleFullScreen(): void;

	isMinimized(): boolean;

	setRepresentedFilename(name: string): void;
	getRepresentedFilename(): string | undefined;

	setDocumentEdited(edited: boolean): void;
	isDocumentEdited(): boolean;

	handleTitleDoubleClick(): void;

	updateTouchBar(items: ISerializableCommandAction[][]): void;

	serializeWindowState(): IWindowState;

	updateWindowControls(options: { height?: number; backgroundColor?: string; foregroundColor?: string }): void;
}

CodeWindowVSCode主窗口的主要实现类,它控制了整个窗口的创建销毁、生命周期等等,具体来说,它上面暴露出的属性有:

  • onWillLoad:一个 ILoadEvent 类型的事件,当窗口开始加载时触发。
  • onDidSignalReady:一个没有参数的事件,当窗口准备就绪并且可以开始渲染时触发。
  • onDidTriggerSystemContextMenu:一个 {x: number, y: number} 类型的事件,当用户在窗口上右键点击时触发,返回右键点击位置的坐标。
  • onDidClose:一个没有参数的事件,当窗口关闭时触发。
  • onDidDestroy:一个没有参数的事件,当窗口销毁时触发。
  • whenClosedOrLoaded:一个 Promise 类型,当窗口关闭或准备就绪时会被resolve
  • id:一个 number 类型,表示窗口的唯一标识符。
  • win:一个 BrowserWindownull 类型,表示窗口的实例或者是 null 如果窗口已经被销毁。
  • config:一个 INativeWindowConfigurationundefined 类型,表示窗口的配置对象。
  • openedWorkspace:一个 IWorkspaceIdentifierISingleFolderWorkspaceIdentifier 类型,表示在窗口中打开的工作区或者单一文件夹。
  • profile:一个 IUserDataProfile 类型,表示用户数据的配置文件。
  • backupPath:一个 stringundefined 类型,表示与窗口关联的备份文件的路径。
  • remoteAuthority:一个 stringundefined 类型,表示与窗口关联的远程服务器的身份验证信息。
  • lastFocusTime:一个 number 类型,表示窗口最后获得焦点的时间。
  • isReady:一个 boolean 类型,表示窗口是否已经准备就绪。
  • isSandboxed:一个 boolean 类型,表示窗口是否运行在沙箱中。

让我们来看看创建Window的主要实现,它就是在CodeWindow的构造函数中创建:

let useSandbox = false;
		{
			// Load window state
			const [state, hasMultipleDisplays] = this.restoreWindowState(config.state);
			this.windowState = state;
			this.logService.trace('window#ctor: using window state', state);

			// In case we are maximized or fullscreen, only show later
			// after the call to maximize/fullscreen (see below)
			const isFullscreenOrMaximized = (this.windowState.mode === WindowMode.Maximized || this.windowState.mode === WindowMode.Fullscreen);

			if (typeof CodeWindow.sandboxState === 'undefined') {
				// we should only check this once so that we do not end up
				// with some windows in sandbox mode and some not!
				CodeWindow.sandboxState = this.stateService.getItem<boolean>('window.experimental.useSandbox', false);
			}

			const windowSettings = this.configurationService.getValue<IWindowSettings | undefined>('window');

			if (typeof windowSettings?.experimental?.useSandbox === 'boolean') {
				useSandbox = windowSettings.experimental.useSandbox;
			} else if (this.productService.quality === 'stable' && CodeWindow.sandboxState) {
				useSandbox = true;
			} else {
				useSandbox = typeof this.productService.quality === 'string' && this.productService.quality !== 'stable';
			}

			this._isSandboxed = useSandbox;

			const options: BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } = {
				width: this.windowState.width,
				height: this.windowState.height,
				x: this.windowState.x,
				y: this.windowState.y,
				backgroundColor: this.themeMainService.getBackgroundColor(),
				minWidth: WindowMinimumSize.WIDTH,
				minHeight: WindowMinimumSize.HEIGHT,
				show: !isFullscreenOrMaximized, // reduce flicker by showing later
				title: this.productService.nameLong,
				webPreferences: {
					preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js').fsPath,
					additionalArguments: [`--vscode-window-config=${this.configObjectUrl.resource.toString()}`],
					v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none',
					enableWebSQL: false,
					spellcheck: false,
					zoomFactor: zoomLevelToZoomFactor(windowSettings?.zoomLevel),
					autoplayPolicy: 'user-gesture-required',
					// Enable experimental css highlight api https://chromestatus.com/feature/5436441440026624
					// Refs https://github.com/microsoft/vscode/issues/140098
					enableBlinkFeatures: 'HighlightAPI',
					...useSandbox ?

						// Sandbox
						{
							sandbox: true
						} :

						// No Sandbox
						{
							nodeIntegration: true,
							contextIsolation: false
						}
				},
				experimentalDarkMode: true
			};

			// Apply icon to window
			// Linux: always
			// Windows: only when running out of sources, otherwise an icon is set by us on the executable
			if (isLinux) {
				options.icon = join(this.environmentMainService.appRoot, 'resources/linux/code.png');
			} else if (isWindows && !this.environmentMainService.isBuilt) {
				options.icon = join(this.environmentMainService.appRoot, 'resources/win32/code_150x150.png');
			}

			if (isMacintosh && !this.useNativeFullScreen()) {
				options.fullscreenable = false; // enables simple fullscreen mode
			}

			if (isMacintosh) {
				options.acceptFirstMouse = true; // enabled by default

				if (windowSettings?.clickThroughInactive === false) {
					options.acceptFirstMouse = false;
				}
			}

			const useNativeTabs = isMacintosh && windowSettings?.nativeTabs === true;
			if (useNativeTabs) {
				options.tabbingIdentifier = this.productService.nameShort; // this opts in to sierra tabs
			}

			const useCustomTitleStyle = getTitleBarStyle(this.configurationService) === 'custom';
			if (useCustomTitleStyle) {
				options.titleBarStyle = 'hidden';
				if (!isMacintosh) {
					options.frame = false;
				}

				if (useWindowControlsOverlay(this.configurationService)) {

					// This logic will not perfectly guess the right colors
					// to use on initialization, but prefer to keep things
					// simple as it is temporary and not noticeable

					const titleBarColor = this.themeMainService.getWindowSplash()?.colorInfo.titleBarBackground ?? this.themeMainService.getBackgroundColor();
					const symbolColor = Color.fromHex(titleBarColor).isDarker() ? '#FFFFFF' : '#000000';

					options.titleBarOverlay = {
						height: 29, // the smallest size of the title bar on windows accounting for the border on windows 11
						color: titleBarColor,
						symbolColor
					};

					this.hasWindowControlOverlay = true;
				}
			}

			// Create the browser window
			mark('code/willCreateCodeBrowserWindow');
			this._win = new BrowserWindow(options);
			mark('code/didCreateCodeBrowserWindow');

			this._id = this._win.id;

			if (isMacintosh && useCustomTitleStyle) {
				this._win.setSheetOffset(22); // offset dialogs by the height of the custom title bar if we have any
			}

			// Update the window controls immediately based on cached values
			if (useCustomTitleStyle && ((isWindows && useWindowControlsOverlay(this.configurationService)) || isMacintosh)) {
				const cachedWindowControlHeight = this.stateService.getItem<number>((CodeWindow.windowControlHeightStateStorageKey));
				if (cachedWindowControlHeight) {
					this.updateWindowControls({ height: cachedWindowControlHeight });
				}
			}

			// Windows Custom System Context Menu
			// See https://github.com/electron/electron/issues/24893
			//
			// The purpose of this is to allow for the context menu in the Windows Title Bar
			//
			// Currently, all mouse events in the title bar are captured by the OS
			// thus we need to capture them here with a window hook specific to Windows
			// and then forward them to the correct window.
			if (isWindows && useCustomTitleStyle) {
				const WM_INITMENU = 0x0116; // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-initmenu

				// This sets up a listener for the window hook. This is a Windows-only API provided by electron.
				this._win.hookWindowMessage(WM_INITMENU, () => {
					const [x, y] = this._win.getPosition();
					const cursorPos = screen.getCursorScreenPoint();
					const cx = cursorPos.x - x;
					const cy = cursorPos.y - y;

					// In some cases, show the default system context menu
					// 1) The mouse position is not within the title bar
					// 2) The mouse position is within the title bar, but over the app icon
					// We do not know the exact title bar height but we make an estimate based on window height
					const shouldTriggerDefaultSystemContextMenu = () => {
						// Use the custom context menu when over the title bar, but not over the app icon
						// The app icon is estimated to be 30px wide
						// The title bar is estimated to be the max of 35px and 15% of the window height
						if (cx > 30 && cy >= 0 && cy <= Math.max(this._win.getBounds().height * 0.15, 35)) {
							return false;
						}

						return true;
					};

					if (!shouldTriggerDefaultSystemContextMenu()) {
						// This is necessary to make sure the native system context menu does not show up.
						this._win.setEnabled(false);
						this._win.setEnabled(true);

						this._onDidTriggerSystemContextMenu.fire({ x: cx, y: cy });
					}

					return 0;
				});
			}

			// TODO@electron (Electron 4 regression): when running on multiple displays where the target display
			// to open the window has a larger resolution than the primary display, the window will not size
			// correctly unless we set the bounds again (https://github.com/microsoft/vscode/issues/74872)
			//
			// Extended to cover Windows as well as Mac (https://github.com/microsoft/vscode/issues/146499)
			//
			// However, when running with native tabs with multiple windows we cannot use this workaround
			// because there is a potential that the new window will be added as native tab instead of being
			// a window on its own. In that case calling setBounds() would cause https://github.com/microsoft/vscode/issues/75830
			if ((isMacintosh || isWindows) && hasMultipleDisplays && (!useNativeTabs || BrowserWindow.getAllWindows().length === 1)) {
				if ([this.windowState.width, this.windowState.height, this.windowState.x, this.windowState.y].every(value => typeof value === 'number')) {
					this._win.setBounds({
						width: this.windowState.width,
						height: this.windowState.height,
						x: this.windowState.x,
						y: this.windowState.y
					});
				}
			}

			if (isFullscreenOrMaximized) {
				mark('code/willMaximizeCodeWindow');

				// this call may or may not show the window, depends
				// on the platform: currently on Windows and Linux will
				// show the window as active. To be on the safe side,
				// we show the window at the end of this block.
				this._win.maximize();

				if (this.windowState.mode === WindowMode.Fullscreen) {
					this.setFullScreen(true);
				}

				// to reduce flicker from the default window size
				// to maximize or fullscreen, we only show after
				this._win.show();
				mark('code/didMaximizeCodeWindow');
			}

			this._lastFocusTime = Date.now(); // since we show directly, we need to set the last focus time too
		}

可以看到这里主要是VSCode对窗口的一些参数处理,有一些系统上的兼容处理,尤其是处理全屏相关的模式,它传给BrowserWindow的参数主要有:

  • width:窗口宽度。
  • height:窗口高度。
  • x:窗口左上角的 x 坐标位置。
  • y:窗口左上角的 y 坐标位置。
  • backgroundColor:窗口的背景颜色。
  • minWidth:窗口最小宽度。
  • minHeight:窗口最小高度。
  • show:是否在创建后立即显示窗口。
  • title:窗口的标题栏文本。
  • webPreferences:用于配置窗口的 Web 端口选项。具体来说,包括:
    • preload:预加载的脚本文件的路径。
    • additionalArguments:传递给预加载脚本的其他参数。
    • v8CacheOptions:V8 缓存选项。
    • enableWebSQL:是否启用 WebSQL。
    • spellcheck:是否启用拼写检查。
    • zoomFactor:窗口的缩放因子。
    • autoplayPolicy:自动播放策略。
    • enableBlinkFeatures:启用的 Blink 功能。
    • sandbox:是否启用沙箱模式。
    • nodeIntegration:是否启用 Node.js 集成。
    • contextIsolation:是否启用上下文隔离。
  • experimentalDarkMode:是否启用实验性的暗黑模式。
  • icon:窗口的图标文件路径。
  • fullscreenable:是否允许窗口全屏。
  • acceptFirstMouse:是否允许在窗口没有焦点的情况下接受第一个鼠标点击事件。
  • tabbingIdentifier:用于窗口选项卡的标识符。
  • frame:是否启用窗口的框架。
  • titleBarStyle:窗口标题栏的样式。
  • titleBarOverlay:窗口标题栏覆盖的选项

接下来我们看一下VSCode处理了哪些窗口类的事件:

private registerListeners(sandboxed: boolean): void {

		// Window error conditions to handle
		this._win.on('unresponsive', () => this.onWindowError(WindowError.UNRESPONSIVE, { sandboxed }));
		this._win.webContents.on('render-process-gone', (event, details) => this.onWindowError(WindowError.PROCESS_GONE, { ...details, sandboxed }));
		this._win.webContents.on('did-fail-load', (event, exitCode, reason) => this.onWindowError(WindowError.LOAD, { reason, exitCode, sandboxed }));

		// Prevent windows/iframes from blocking the unload
		// through DOM events. We have our own logic for
		// unloading a window that should not be confused
		// with the DOM way.
		// (https://github.com/microsoft/vscode/issues/122736)
		this._win.webContents.on('will-prevent-unload', event => {
			event.preventDefault();
		});

		// Window close
		this._win.on('closed', () => {
			this._onDidClose.fire();

			this.dispose();
		});

		// Remember that we loaded
		this._win.webContents.on('did-finish-load', () => {

			// Associate properties from the load request if provided
			if (this.pendingLoadConfig) {
				this._config = this.pendingLoadConfig;

				this.pendingLoadConfig = undefined;
			}
		});

		// Window Focus
		this._win.on('focus', () => {
			this._lastFocusTime = Date.now();
		});

		// Window (Un)Maximize
		this._win.on('maximize', (e: ElectronEvent) => {
			if (this._config) {
				this._config.maximized = true;
			}

			app.emit('browser-window-maximize', e, this._win);
		});

		this._win.on('unmaximize', (e: ElectronEvent) => {
			if (this._config) {
				this._config.maximized = false;
			}

			app.emit('browser-window-unmaximize', e, this._win);
		});

		// Window Fullscreen
		this._win.on('enter-full-screen', () => {
			this.sendWhenReady('vscode:enterFullScreen', CancellationToken.None);

			this.joinNativeFullScreenTransition?.complete();
			this.joinNativeFullScreenTransition = undefined;
		});

		this._win.on('leave-full-screen', () => {
			this.sendWhenReady('vscode:leaveFullScreen', CancellationToken.None);

			this.joinNativeFullScreenTransition?.complete();
			this.joinNativeFullScreenTransition = undefined;
		});

		// Handle configuration changes
		this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));

		// Handle Workspace events
		this._register(this.workspacesManagementMainService.onDidDeleteUntitledWorkspace(e => this.onDidDeleteUntitledWorkspace(e)));

		// Inject headers when requests are incoming
		const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*'];
		this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => {
			const headers = await this.getMarketplaceHeaders();

			cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) });
		});
	}

总体而言主要是以下几个事件:

  • unresponsive: 监听窗口是否失去响应,如果失去响应,则触发 onWindowError 事件并传递 WindowError.UNRESPONSIVE 作为错误类型。
  • render-process-gone: 监听窗口渲染进程是否异常退出,如果异常退出,则触发 onWindowError 事件并传递 WindowError.PROCESS_GONE 作为错误类型。
  • did-fail-load: 监听窗口是否加载失败,如果加载失败,则触发 onWindowError 事件并传递 WindowError.LOAD 作为错误类型。
  • will-prevent-unload: 阻止窗口通过 DOM 事件阻止卸载。这是因为 Electron 窗口卸载有自己的逻辑,不应该被 DOM 事件所混淆。
  • closed: 监听窗口关闭事件,当窗口关闭时,触发 onDidClose 事件,并调用 dispose 方法。
  • did-finish-load: 监听窗口是否加载完成,并在加载完成后将 pendingLoadConfig 中的配置与窗口关联。
  • focus: 监听窗口是否获得焦点,如果窗口获得焦点,则记录最后一次获取焦点的时间。
  • maximize / unmaximize: 监听窗口是否最大化或取消最大化,并将配置文件中的最大化状态更新为对应的状态。
  • enter-full-screen / leave-full-screen: 监听窗口是否进入或退出全屏模式,并分别触发 vsCode:enterFullScreen 和 vsCode:leaveFullScreen 事件。
  • onDidChangeConfiguration: 监听配置文件是否发生更改。
  • onDidDeleteUntitledWorkspace: 监听是否删除了无标题工作区。

我们发现前三个事件是关于错误处理的,分别三种情况:

  • unresponsive表示窗口长时间未响应。当窗口停止响应时,这个事件会被触发,表明需要进行处理,可能需要进行强制关闭等操作。
  • render-process-gone表示渲染进程意外崩溃或被杀死。当发生这种情况时,这个事件会被触发。处理方式可能包括重新加载窗口。
  • did-fail-load表示窗口加载失败。这个事件会在窗口加载过程中发生错误时触发。处理方式可能包括重新加载窗口或者显示错误提示。

我们看看VSCode是如何处理这三类错误的:

private async onWindowError(type: WindowError, details: { reason?: string; exitCode?: number; sandboxed: boolean }): Promise<void> {

		switch (type) {
			case WindowError.PROCESS_GONE:
				this.logService.error(`CodeWindow: renderer process gone (reason: ${details?.reason || '<unknown>'}, code: ${details?.exitCode || '<unknown>'})`);
				break;
			case WindowError.UNRESPONSIVE:
				this.logService.error('CodeWindow: detected unresponsive');
				break;
			case WindowError.LOAD:
				this.logService.error(`CodeWindow: failed to load (reason: ${details?.reason || '<unknown>'}, code: ${details?.exitCode || '<unknown>'})`);
				break;
		}

		// Telemetry
		type WindowErrorClassification = {
			type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The type of window error to understand the nature of the error better.' };
			reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The reason of the window error to understand the nature of the error better.' };
			sandboxed: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'If the window was sandboxed or not.' };
			code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The exit code of the window process to understand the nature of the error better' };
			owner: 'bpasero';
			comment: 'Provides insight into reasons the vscode window had an error.';
		};
		type WindowErrorEvent = {
			type: WindowError;
			reason: string | undefined;
			code: number | undefined;
			sandboxed: string;
		};
		this.telemetryService.publicLog2<WindowErrorEvent, WindowErrorClassification>('windowerror', {
			type,
			reason: details?.reason,
			code: details?.exitCode,
			sandboxed: details?.sandboxed ? '1' : '0'
		});

		// Inform User if non-recoverable
		switch (type) {
			case WindowError.UNRESPONSIVE:
			case WindowError.PROCESS_GONE:

				// If we run extension tests from CLI, we want to signal
				// back this state to the test runner by exiting with a
				// non-zero exit code.
				if (this.isExtensionDevelopmentTestFromCli) {
					this.lifecycleMainService.kill(1);
					return;
				}

				// If we run smoke tests, want to proceed with an orderly
				// shutdown as much as possible by destroying the window
				// and then calling the normal `quit` routine.
				if (this.environmentMainService.args['enable-smoke-test-driver']) {
					await this.destroyWindow(false, false);
					this.lifecycleMainService.quit(); // still allow for an orderly shutdown
					return;
				}

				// Unresponsive
				if (type === WindowError.UNRESPONSIVE) {
					if (this.isExtensionDevelopmentHost || this.isExtensionTestHost || (this._win && this._win.webContents && this._win.webContents.isDevToolsOpened())) {
						// TODO@electron Workaround for https://github.com/microsoft/vscode/issues/56994
						// In certain cases the window can report unresponsiveness because a breakpoint was hit
						// and the process is stopped executing. The most typical cases are:
						// - devtools are opened and debugging happens
						// - window is an extensions development host that is being debugged
						// - window is an extension test development host that is being debugged
						return;
					}

					// Show Dialog
					const { response, checkboxChecked } = await this.dialogMainService.showMessageBox({
						type: 'warning',
						buttons: [
							localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen"),
							localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"),
							localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")
						],
						message: localize('appStalled', "The window is not responding"),
						detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."),
						checkboxLabel: this._config?.workspace ? localize('doNotRestoreEditors', "Don't restore editors") : undefined
					}, this._win);

					// Handle choice
					if (response !== 2 /* keep waiting */) {
						const reopen = response === 0;
						await this.destroyWindow(reopen, checkboxChecked);
					}
				}

				// Process gone
				else if (type === WindowError.PROCESS_GONE) {

					// Windows: running as admin with AppLocker enabled is unsupported
					//          when sandbox: true.
					//          we cannot detect AppLocker use currently, but make a
					//          guess based on the reason and exit code.
					if (isWindows && details?.reason === 'launch-failed' && details.exitCode === 18 && await this.nativeHostMainService.isAdmin(undefined)) {
						await this.handleWindowsAdminCrash(details);
					}

					// Any other crash: offer to restart
					else {
						let message: string;
						if (!details) {
							message = localize('appGone', "The window terminated unexpectedly");
						} else {
							message = localize('appGoneDetails', "The window terminated unexpectedly (reason: '{0}', code: '{1}')", details.reason, details.exitCode ?? '<unknown>');
						}

						// Show Dialog
						const { response, checkboxChecked } = await this.dialogMainService.showMessageBox({
							type: 'warning',
							buttons: [
								this._config?.workspace ? localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen") : localize({ key: 'newWindow', comment: ['&& denotes a mnemonic'] }, "&&New Window"),
								localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close")
							],
							message,
							detail: this._config?.workspace ?
								localize('appGoneDetailWorkspace', "We are sorry for the inconvenience. You can reopen the window to continue where you left off.") :
								localize('appGoneDetailEmptyWindow', "We are sorry for the inconvenience. You can open a new empty window to start again."),
							checkboxLabel: this._config?.workspace ? localize('doNotRestoreEditors', "Don't restore editors") : undefined
						}, this._win);

						// Handle choice
						const reopen = response === 0;
						await this.destroyWindow(reopen, checkboxChecked);
					}
				}
				break;
		}
	}

首先记录log是必不可少的,这里还通过telemetry给Application Insights进行上报。

对于WindowError.PROCESS_GONE,表示浏览器窗口的渲染进程崩溃,该函数将记录错误信息并显示一个消息框,提供重新打开或关闭窗口的选项。

对于WindowError.UNRESPONSIVE,表示窗口无响应,该函数将记录错误信息并显示一个消息框,提供重新打开、关闭窗口或等待的选项。

对于WindowError.LOAD,表示窗口加载失败,该函数将记录错误信息并显示一个消息框,提供重新打开或关闭窗口的选项。

最后,真正启动窗口的代码在load函数中实现:

load(configuration: INativeWindowConfiguration, options: ILoadOptions = Object.create(null)): void {
		this.logService.trace(`window#load: attempt to load window (id: ${this._id})`);

		// Clear Document Edited if needed
		if (this.isDocumentEdited()) {
			if (!options.isReload || !this.backupMainService.isHotExitEnabled()) {
				this.setDocumentEdited(false);
			}
		}

		// Clear Title and Filename if needed
		if (!options.isReload) {
			if (this.getRepresentedFilename()) {
				this.setRepresentedFilename('');
			}

			this._win.setTitle(this.productService.nameLong);
		}

		// Update configuration values based on our window context
		// and set it into the config object URL for usage.
		this.updateConfiguration(configuration, options);

		// If this is the first time the window is loaded, we associate the paths
		// directly with the window because we assume the loading will just work
		if (this.readyState === ReadyState.NONE) {
			this._config = configuration;
		}

		// Otherwise, the window is currently showing a folder and if there is an
		// unload handler preventing the load, we cannot just associate the paths
		// because the loading might be vetoed. Instead we associate it later when
		// the window load event has fired.
		else {
			this.pendingLoadConfig = configuration;
		}

		// Indicate we are navigting now
		this.readyState = ReadyState.NAVIGATING;

		// Load URL
		this._win.loadURL(FileAccess.asBrowserUri(`vs/code/electron-sandbox/workbench/workbench${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true));

		// Remember that we did load
		const wasLoaded = this.wasLoaded;
		this.wasLoaded = true;

		// Make window visible if it did not open in N seconds because this indicates an error
		// Only do this when running out of sources and not when running tests
		if (!this.environmentMainService.isBuilt && !this.environmentMainService.extensionTestsLocationURI) {
			this._register(new RunOnceScheduler(() => {
				if (this._win && !this._win.isVisible() && !this._win.isMinimized()) {
					this._win.show();
					this.focus({ force: true });
					this._win.webContents.openDevTools();
				}
			}, 10000)).schedule();
		}

		// Event
		this._onWillLoad.fire({ workspace: configuration.workspace, reason: options.isReload ? LoadReason.RELOAD : wasLoaded ? LoadReason.LOAD : LoadReason.INITIAL });
	}

可以看到,这里加载的是vs/code/electron-sandbox/workbench/workbench.html,这样就打开了VSCode的主窗口。

windowsMainService的管理

windowsMainService暴露的接口如下:

export interface IWindowsMainService {

	readonly _serviceBrand: undefined;

	readonly onDidChangeWindowsCount: Event<IWindowsCountChangedEvent>;

	readonly onDidOpenWindow: Event<ICodeWindow>;
	readonly onDidSignalReadyWindow: Event<ICodeWindow>;
	readonly onDidTriggerSystemContextMenu: Event<{ window: ICodeWindow; x: number; y: number }>;
	readonly onDidDestroyWindow: Event<ICodeWindow>;

	open(openConfig: IOpenConfiguration): Promise<ICodeWindow[]>;
	openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): Promise<ICodeWindow[]>;
	openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): Promise<ICodeWindow[]>;

	openExistingWindow(window: ICodeWindow, openConfig: IOpenConfiguration): void;

	sendToFocused(channel: string, ...args: any[]): void;
	sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void;

	getWindows(): ICodeWindow[];
	getWindowCount(): number;

	getFocusedWindow(): ICodeWindow | undefined;
	getLastActiveWindow(): ICodeWindow | undefined;

	getWindowById(windowId: number): ICodeWindow | undefined;
	getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined;
}
  • onDidChangeWindowsCount: 当 Code 窗口的数量发生变化时,这个事件会被触发。可以通过这个事件来监视窗口数量的变化。
  • onDidOpenWindow: 当打开一个新的 Code 窗口时,这个事件会被触发。可以通过这个事件来监听新窗口的创建。
  • onDidSignalReadyWindow: 当一个 Code 窗口准备好时,这个事件会被触发。可以通过这个事件来监听窗口的 ready 事件,表示窗口已经完成了初始化。
  • onDidTriggerSystemContextMenu: 当在系统上下文菜单中点击菜单项时,这个事件会被触发。可以通过这个事件来监听系统上下文菜单的使用情况。
  • onDidDestroyWindow: 当一个 Code 窗口被销毁时,这个事件会被触发。可以通过这个事件来监听窗口的销毁事件。
  • open: 打开一个或多个 Code 窗口,并返回打开的窗口实例。传递一个配置对象 IOpenConfiguration,用于指定窗口的类型、工作区、文件夹等。
  • openEmptyWindow: 打开一个新的空白 Code 窗口,并返回窗口实例。可以通过 IOpenEmptyConfigurationIOpenEmptyWindowOptions 参数指定窗口的选项,如是否全屏、是否显示菜单等。
  • openExtensionDevelopmentHostWindow: 打开一个扩展开发主机窗口。这个窗口是一个特殊的窗口,用于在其中运行扩展程序,以便在开发扩展时进行测试和调试。传递一个配置对象 IOpenConfiguration,用于指定窗口的类型、工作区、文件夹等。
  • openExistingWindow: 在一个已经存在的 Code 窗口中打开一个或多个文件或文件夹。传递一个窗口实例和一个 IOpenConfiguration 对象,用于指定要在窗口中打开的文件或文件夹。
  • sendToFocused: 向当前焦点窗口的渲染进程发送一个消息。可以通过这个方法与渲染进程进行通信。
  • sendToAll: 向所有 Code 窗口的渲染进程发送一个消息。可以通过这个方法与所有渲染进程进行通信。
  • getWindows: 返回所有打开的 Code 窗口的实例数组。
  • getWindowCount: 返回打开的 Code 窗口的数量。
  • getFocusedWindow: 返回当前聚焦的窗口实例。如果没有窗口聚焦,则返回 undefined。
  • getLastActiveWindow: 返回上一个活动窗口

这个Service用来管理所有的CodeWindow实例,它的主要入口是open方法:

async open(openConfig: IOpenConfiguration): Promise<ICodeWindow[]> {
		this.logService.trace('windowsManager#open');

		if (openConfig.addMode && (openConfig.initialStartup || !this.getLastActiveWindow())) {
			openConfig.addMode = false; // Make sure addMode is only enabled if we have an active window
		}

		const foldersToAdd: ISingleFolderWorkspacePathToOpen[] = [];
		const foldersToOpen: ISingleFolderWorkspacePathToOpen[] = [];

		const workspacesToOpen: IWorkspacePathToOpen[] = [];
		const untitledWorkspacesToRestore: IWorkspacePathToOpen[] = [];

		const emptyWindowsWithBackupsToRestore: IEmptyWindowBackupInfo[] = [];

		let filesToOpen: IFilesToOpen | undefined;
		let emptyToOpen = 0;

		// Identify things to open from open config
		const pathsToOpen = await this.getPathsToOpen(openConfig);
		this.logService.trace('windowsManager#open pathsToOpen', pathsToOpen);
		for (const path of pathsToOpen) {
			if (isSingleFolderWorkspacePathToOpen(path)) {
				if (openConfig.addMode) {
					// When run with --add, take the folders that are to be opened as
					// folders that should be added to the currently active window.
					foldersToAdd.push(path);
				} else {
					foldersToOpen.push(path);
				}
			} else if (isWorkspacePathToOpen(path)) {
				workspacesToOpen.push(path);
			} else if (path.fileUri) {
				if (!filesToOpen) {
					filesToOpen = { filesToOpenOrCreate: [], filesToDiff: [], filesToMerge: [], remoteAuthority: path.remoteAuthority };
				}
				filesToOpen.filesToOpenOrCreate.push(path);
			} else if (path.backupPath) {
				emptyWindowsWithBackupsToRestore.push({ backupFolder: basename(path.backupPath), remoteAuthority: path.remoteAuthority });
			} else {
				emptyToOpen  ;
			}
		}

		// When run with --diff, take the first 2 files to open as files to diff
		if (openConfig.diffMode && filesToOpen && filesToOpen.filesToOpenOrCreate.length >= 2) {
			filesToOpen.filesToDiff = filesToOpen.filesToOpenOrCreate.slice(0, 2);
			filesToOpen.filesToOpenOrCreate = [];
		}

		// When run with --merge, take the first 4 files to open as files to merge
		if (openConfig.mergeMode && filesToOpen && filesToOpen.filesToOpenOrCreate.length === 4) {
			filesToOpen.filesToMerge = filesToOpen.filesToOpenOrCreate.slice(0, 4);
			filesToOpen.filesToOpenOrCreate = [];
			filesToOpen.filesToDiff = [];
		}

		// When run with --wait, make sure we keep the paths to wait for
		if (filesToOpen && openConfig.waitMarkerFileURI) {
			filesToOpen.filesToWait = { paths: coalesce([...filesToOpen.filesToDiff, filesToOpen.filesToMerge[3] /* [3] is the resulting merge file */, ...filesToOpen.filesToOpenOrCreate]), waitMarkerFileUri: openConfig.waitMarkerFileURI };
		}

		// These are windows to restore because of hot-exit or from previous session (only performed once on startup!)
		if (openConfig.initialStartup) {

			// Untitled workspaces are always restored
			untitledWorkspacesToRestore.push(...this.workspacesManagementMainService.getUntitledWorkspaces());
			workspacesToOpen.push(...untitledWorkspacesToRestore);

			// Empty windows with backups are always restored
			emptyWindowsWithBackupsToRestore.push(...this.backupMainService.getEmptyWindowBackups());
		} else {
			emptyWindowsWithBackupsToRestore.length = 0;
		}

		// Open based on config
		const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, emptyToOpen, filesToOpen, foldersToAdd);

		this.logService.trace(`windowsManager#open used window count ${usedWindows.length} (workspacesToOpen: ${workspacesToOpen.length}, foldersToOpen: ${foldersToOpen.length}, emptyToRestore: ${emptyWindowsWithBackupsToRestore.length}, emptyToOpen: ${emptyToOpen})`);

		// Make sure to pass focus to the most relevant of the windows if we open multiple
		if (usedWindows.length > 1) {

			// 1.) focus window we opened files in always with highest priority
			if (filesOpenedInWindow) {
				filesOpenedInWindow.focus();
			}

			// Otherwise, find a good window based on open params
			else {
				const focusLastActive = this.windowsStateHandler.state.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length);
				let focusLastOpened = true;
				let focusLastWindow = true;

				// 2.) focus last active window if we are not instructed to open any paths
				if (focusLastActive) {
					const lastActiveWindow = usedWindows.filter(window => this.windowsStateHandler.state.lastActiveWindow && window.backupPath === this.windowsStateHandler.state.lastActiveWindow.backupPath);
					if (lastActiveWindow.length) {
						lastActiveWindow[0].focus();
						focusLastOpened = false;
						focusLastWindow = false;
					}
				}

				// 3.) if instructed to open paths, focus last window which is not restored
				if (focusLastOpened) {
					for (let i = usedWindows.length - 1; i >= 0; i--) {
						const usedWindow = usedWindows[i];
						if (
							(usedWindow.openedWorkspace && untitledWorkspacesToRestore.some(workspace => usedWindow.openedWorkspace && workspace.workspace.id === usedWindow.openedWorkspace.id)) ||	// skip over restored workspace
							(usedWindow.backupPath && emptyWindowsWithBackupsToRestore.some(empty => usedWindow.backupPath && empty.backupFolder === basename(usedWindow.backupPath)))							// skip over restored empty window
						) {
							continue;
						}

						usedWindow.focus();
						focusLastWindow = false;
						break;
					}
				}

				// 4.) finally, always ensure to have at least last used window focused
				if (focusLastWindow) {
					usedWindows[usedWindows.length - 1].focus();
				}
			}
		}

		// Remember in recent document list (unless this opens for extension development)
		// Also do not add paths when files are opened for diffing or merging, only if opened individually
		const isDiff = filesToOpen && filesToOpen.filesToDiff.length > 0;
		const isMerge = filesToOpen && filesToOpen.filesToMerge.length > 0;
		if (!usedWindows.some(window => window.isExtensionDevelopmentHost) && !isDiff && !isMerge && !openConfig.noRecentEntry) {
			const recents: IRecent[] = [];
			for (const pathToOpen of pathsToOpen) {
				if (isWorkspacePathToOpen(pathToOpen) && !pathToOpen.transient /* never add transient workspaces to history */) {
					recents.push({ label: pathToOpen.label, workspace: pathToOpen.workspace, remoteAuthority: pathToOpen.remoteAuthority });
				} else if (isSingleFolderWorkspacePathToOpen(pathToOpen)) {
					recents.push({ label: pathToOpen.label, folderUri: pathToOpen.workspace.uri, remoteAuthority: pathToOpen.remoteAuthority });
				} else if (pathToOpen.fileUri) {
					recents.push({ label: pathToOpen.label, fileUri: pathToOpen.fileUri, remoteAuthority: pathToOpen.remoteAuthority });
				}
			}

			this.workspacesHistoryMainService.addRecentlyOpened(recents);
		}

		// Handle --wait
		this.handleWaitMarkerFile(openConfig, usedWindows);

		return usedWindows;
	}

整个open方法大致做了几件事情:

  1. 根据 openConfig 获取要打开的文件、文件夹、工作区、空白窗口等信息。
  2. 处理 openConfig 中的一些特殊参数,比如 --add--diff--merge--wait--no-recent-entry 等。
  3. 根据获取到的信息,调用 doOpen 方法打开新的窗口,并返回已经打开的窗口数组。
  4. 根据打开的窗口数量决定哪个窗口要被聚焦,并在 recent list 中添加相应的条目。
  5. 处理 --wait 参数。
  6. 返回已经打开的窗口数组。

首先在VSCode中打开窗口的途径很多,比如我们用VSCode单独打开一个文件、或文件夹、或工作区,这些的处理方式略有差异,在文件的情形下还有很多模式。经过一系列参数处理,最终通过doOpen方法来打开窗口:

private async doOpen(
		openConfig: IOpenConfiguration,
		workspacesToOpen: IWorkspacePathToOpen[],
		foldersToOpen: ISingleFolderWorkspacePathToOpen[],
		emptyToRestore: IEmptyWindowBackupInfo[],
		emptyToOpen: number,
		filesToOpen: IFilesToOpen | undefined,
		foldersToAdd: ISingleFolderWorkspacePathToOpen[]
	): Promise<{ windows: ICodeWindow[]; filesOpenedInWindow: ICodeWindow | undefined }> {

		// Keep track of used windows and remember
		// if files have been opened in one of them
		const usedWindows: ICodeWindow[] = [];
		let filesOpenedInWindow: ICodeWindow | undefined = undefined;
		function addUsedWindow(window: ICodeWindow, openedFiles?: boolean): void {
			usedWindows.push(window);

			if (openedFiles) {
				filesOpenedInWindow = window;
				filesToOpen = undefined; // reset `filesToOpen` since files have been opened
			}
		}

		// Settings can decide if files/folders open in new window or not
		let { openFolderInNewWindow, openFilesInNewWindow } = this.shouldOpenNewWindow(openConfig);

		// Handle folders to add by looking for the last active workspace (not on initial startup)
		if (!openConfig.initialStartup && foldersToAdd.length > 0) {
			const authority = foldersToAdd[0].remoteAuthority;
			const lastActiveWindow = this.getLastActiveWindowForAuthority(authority);
			if (lastActiveWindow) {
				addUsedWindow(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(folderToAdd => folderToAdd.workspace.uri)));
			}
		}

		// Handle files to open/diff/merge or to create when we dont open a folder and we do not restore any
		// folder/untitled from hot-exit by trying to open them in the window that fits best
		const potentialNewWindowsCount = foldersToOpen.length   workspacesToOpen.length   emptyToRestore.length;
		if (filesToOpen && potentialNewWindowsCount === 0) {

			// Find suitable window or folder path to open files in
			const fileToCheck: IPath<IEditorOptions> | undefined = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0] || filesToOpen.filesToMerge[3] /* [3] is the resulting merge file */;

			// only look at the windows with correct authority
			const windows = this.getWindows().filter(window => filesToOpen && isEqualAuthority(window.remoteAuthority, filesToOpen.remoteAuthority));

			// figure out a good window to open the files in if any
			// with a fallback to the last active window.
			//
			// in case `openFilesInNewWindow` is enforced, we skip
			// this step.
			let windowToUseForFiles: ICodeWindow | undefined = undefined;
			if (fileToCheck?.fileUri && !openFilesInNewWindow) {
				if (openConfig.context === OpenContext.DESKTOP || openConfig.context === OpenContext.CLI || openConfig.context === OpenContext.DOCK) {
					windowToUseForFiles = await findWindowOnFile(windows, fileToCheck.fileUri, async workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesManagementMainService.resolveLocalWorkspace(workspace.configPath) : undefined);
				}

				if (!windowToUseForFiles) {
					windowToUseForFiles = this.doGetLastActiveWindow(windows);
				}
			}

			// We found a window to open the files in
			if (windowToUseForFiles) {

				// Window is workspace
				if (isWorkspaceIdentifier(windowToUseForFiles.openedWorkspace)) {
					workspacesToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority });
				}

				// Window is single folder
				else if (isSingleFolderWorkspaceIdentifier(windowToUseForFiles.openedWorkspace)) {
					foldersToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority });
				}

				// Window is empty
				else {
					addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowToUseForFiles, filesToOpen), true);
				}
			}

			// Finally, if no window or folder is found, just open the files in an empty window
			else {
				addUsedWindow(await this.openInBrowserWindow({
					userEnv: openConfig.userEnv,
					cli: openConfig.cli,
					initialStartup: openConfig.initialStartup,
					filesToOpen,
					forceNewWindow: true,
					remoteAuthority: filesToOpen.remoteAuthority,
					forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
					forceProfile: openConfig.forceProfile,
					forceTempProfile: openConfig.forceTempProfile
				}), true);
			}
		}

		// Handle workspaces to open (instructed and to restore)
		const allWorkspacesToOpen = distinct(workspacesToOpen, workspace => workspace.workspace.id); // prevent duplicates
		if (allWorkspacesToOpen.length > 0) {

			// Check for existing instances
			const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), workspaceToOpen.workspace.configPath)));
			if (windowsOnWorkspace.length > 0) {
				const windowOnWorkspace = windowsOnWorkspace[0];
				const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, windowOnWorkspace.remoteAuthority) ? filesToOpen : undefined;

				// Do open files
				addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow), !!filesToOpenInWindow);

				openFolderInNewWindow = true; // any other folders to open must open in new window then
			}

			// Open remaining ones
			for (const workspaceToOpen of allWorkspacesToOpen) {
				if (windowsOnWorkspace.some(window => window.openedWorkspace && window.openedWorkspace.id === workspaceToOpen.workspace.id)) {
					continue; // ignore folders that are already open
				}

				const remoteAuthority = workspaceToOpen.remoteAuthority;
				const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, remoteAuthority) ? filesToOpen : undefined;

				// Do open folder
				addUsedWindow(await this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow);

				openFolderInNewWindow = true; // any other folders to open must open in new window then
			}
		}

		// Handle folders to open (instructed and to restore)
		const allFoldersToOpen = distinct(foldersToOpen, folder => extUriBiasedIgnorePathCase.getComparisonKey(folder.workspace.uri)); // prevent duplicates
		if (allFoldersToOpen.length > 0) {

			// Check for existing instances
			const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), folderToOpen.workspace.uri)));
			if (windowsOnFolderPath.length > 0) {
				const windowOnFolderPath = windowsOnFolderPath[0];
				const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, windowOnFolderPath.remoteAuthority) ? filesToOpen : undefined;

				// Do open files
				addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow), !!filesToOpenInWindow);

				openFolderInNewWindow = true; // any other folders to open must open in new window then
			}

			// Open remaining ones
			for (const folderToOpen of allFoldersToOpen) {
				if (windowsOnFolderPath.some(window => isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.uri, folderToOpen.workspace.uri))) {
					continue; // ignore folders that are already open
				}

				const remoteAuthority = folderToOpen.remoteAuthority;
				const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, remoteAuthority) ? filesToOpen : undefined;

				// Do open folder
				addUsedWindow(await this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow);

				openFolderInNewWindow = true; // any other folders to open must open in new window then
			}
		}

		// Handle empty to restore
		const allEmptyToRestore = distinct(emptyToRestore, info => info.backupFolder); // prevent duplicates
		if (allEmptyToRestore.length > 0) {
			for (const emptyWindowBackupInfo of allEmptyToRestore) {
				const remoteAuthority = emptyWindowBackupInfo.remoteAuthority;
				const filesToOpenInWindow = isEqualAuthority(filesToOpen?.remoteAuthority, remoteAuthority) ? filesToOpen : undefined;

				addUsedWindow(await this.doOpenEmpty(openConfig, true, remoteAuthority, filesToOpenInWindow, emptyWindowBackupInfo), !!filesToOpenInWindow);

				openFolderInNewWindow = true; // any other folders to open must open in new window then
			}
		}

		// Handle empty to open (only if no other window opened)
		if (usedWindows.length === 0 || filesToOpen) {
			if (filesToOpen && !emptyToOpen) {
				emptyToOpen  ;
			}

			const remoteAuthority = filesToOpen ? filesToOpen.remoteAuthority : openConfig.remoteAuthority;

			for (let i = 0; i < emptyToOpen; i  ) {
				addUsedWindow(await this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen), !!filesToOpen);

				// any other window to open must open in new window then
				openFolderInNewWindow = true;
			}
		}

		return { windows: distinct(usedWindows), filesOpenedInWindow };
	}

这里还是在处理打开文件、文件夹、工作区的事情,基本逻辑就是在当前找到一个合适的窗口打开,找不到就开个新窗口打开:

  1. doOpen 方法首先检查 foldersToAdd 数组,如果存在,则尝试在最后一个活动的工作区中添加这些文件夹。如果没有找到匹配的工作区,则打开一个空白窗口,并在其中打开文件夹。
  2. 如果存在要打开的文件,则 doOpen 方法尝试将这些文件打开到一个现有窗口中,或在新的窗口中打开。首先,它查找一个合适的窗口来打开文件,如果找到了一个窗口,那么文件将在该窗口中打开。否则,它会在最后一个活动窗口中打开文件,或者如果没有活动窗口,则会在一个新的空白窗口中打开文件。
  3. doOpen 方法接下来处理要打开的工作区和文件夹。首先,它尝试查找是否已经有打开的窗口,可以打开这些工作区和文件夹。对于每个找到的窗口,它将工作区或文件夹添加到现有窗口中,并将打开的文件打开到该窗口中。否则,它会在一个新的窗口中打开工作区或文件夹,以及要打开的文件。
  4. 最后,doOpen 方法处理 emptyToRestoreemptyToOpen,并在需要时打开空白窗口。如果存在要恢复的空窗口,则将它们恢复到它们以前的状态。否则,它将在空白窗口中打开要求的数量。

最后,我们来看一下doOpenInBrowserWindow的实现,其实它除了一些备份路径的设置外,核心就是调用load去加载窗口了:

private async doOpenInBrowserWindow(window: ICodeWindow, configuration: INativeWindowConfiguration, options: IOpenBrowserWindowOptions, defaultProfile: IUserDataProfile): Promise<void> {

		// Register window for backups unless the window
		// is for extension development, where we do not
		// keep any backups.

		if (!configuration.extensionDevelopmentPath) {
			if (isWorkspaceIdentifier(configuration.workspace)) {
				configuration.backupPath = this.backupMainService.registerWorkspaceBackup({
					workspace: configuration.workspace,
					remoteAuthority: configuration.remoteAuthority
				});
			} else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) {
				configuration.backupPath = this.backupMainService.registerFolderBackup({
					folderUri: configuration.workspace.uri,
					remoteAuthority: configuration.remoteAuthority
				});
			} else {

				// Empty windows are special in that they provide no workspace on
				// their configuration. To properly register them with the backup
				// service, we either use the provided associated `backupFolder`
				// in case we restore a previously opened empty window or we have
				// to generate a new empty window workspace identifier to be used
				// as `backupFolder`.

				configuration.backupPath = this.backupMainService.registerEmptyWindowBackup({
					backupFolder: options.emptyWindowBackupInfo?.backupFolder ?? createEmptyWorkspaceIdentifier().id,
					remoteAuthority: configuration.remoteAuthority
				});
			}
		}

		if (this.userDataProfilesMainService.isEnabled()) {
			const workspace = configuration.workspace ?? toWorkspaceIdentifier(configuration.backupPath, false);
			const profilePromise = this.resolveProfileForBrowserWindow(options, workspace, defaultProfile);
			const profile = profilePromise instanceof Promise ? await profilePromise : profilePromise;
			configuration.profiles.profile = profile;

			if (!configuration.extensionDevelopmentPath) {
				// Associate the configured profile to the workspace
				// unless the window is for extension development,
				// where we do not persist the associations
				await this.userDataProfilesMainService.setProfileForWorkspace(workspace, profile);
			}
		}

		// Load it
		window.load(configuration);
	}

到这里,整个窗口就已经开始加载了,CodeWindowload方法会去打开一个html,启动渲染进程:

this._win.loadURL(FileAccess.asBrowserUri(`vs/code/electron-sandbox/workbench/workbench${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true));

workbench主界面的实现

vscode加载的渲染进程入口HTML非常简单:

<!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 'self'; frame-src 'self' vscode-webview:; object-src 'self'; script-src 'self' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; connect-src 'self' https: ws:; font-src 'self' https: vscode-remote-resource:;">
		<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types amdLoader cellRendererEditorText defaultWorkerFactory diffEditorWidget stickyScrollViewLayer editorGhostText domLineBreaksComputer editorViewLayer diffReview dompurify notebookRenderer safeInnerHtml standaloneColorizer tokenizeToString;">
	</head>

	<body aria-label="">
	</body>

	<!-- Startup (do not modify order of script tags!) -->
	<script src="../../../../bootstrap.js"></script>
	<script src="../../../../vs/loader.js"></script>
	<script src="../../../../bootstrap-window.js"></script>
	<script src="workbench.js"></script>
</html>

可以看到,它在加载workbench.js前,还加载了bootstrap.jsloader.jsbootstrap-window.js

  • bootstrap.js主要暴露了几个全局函数,enableASARSupport用于node环境中启用ASAR,其实在沙箱浏览器中这个函数是没有用的 ,不过vscode至今还没有做到这里的treeshaking。setupNLS用于提供国际化支持,fileUriFromPath将一个本地文件路径转换为文件URI格式,以便在monaco编辑器中使用。
  • loader.js之前我们分析过,就是AMD模块加载器的主入口,在浏览器端尤其重要。
  • bootstrap-window.js主要提供一个load方法,用来启动模块加载,这里面包含了加载并配置一些必要的组件和模块的逻辑。其中的代码包括了加载配置信息、错误处理、开发人员设置、启用 ASAR 支持、设置语言环境、定义路径别名、配置 AMD 加载器等等。

接着就是加载workbench.js了,这个是整个页面的入口,它的功能非常简单:

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

/// <reference path="../../../../typings/require.d.ts" />

//@ts-check
(function () {
	'use strict';

	const bootstrapWindow = bootstrapWindowLib();

	// Add a perf entry right from the top
	performance.mark('code/didStartRenderer');

	// 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 (desktopMain, configuration) {

			// Mark start of workbench
			performance.mark('code/didLoadWorkbenchMain');

			return desktopMain.main(configuration);
		},
		{
			configureDeveloperSettings: function (windowConfig) {
				return {
					// disable automated devtools opening on error when running extension tests
					// as this can lead to nondeterministic test execution (devtools steals focus)
					forceDisableShowDevtoolsOnError: typeof windowConfig.extensionTestsPath === 'string' || windowConfig['enable-smoke-test-driver'] === true,
					// enable devtools keybindings in extension development window
					forceEnableDeveloperKeybindings: Array.isArray(windowConfig.extensionDevelopmentPath) && windowConfig.extensionDevelopmentPath.length > 0,
					removeDeveloperKeybindingsAfterLoad: true
				};
			},
			canModifyDOM: function (windowConfig) {
				showSplash(windowConfig);
			},
			beforeLoaderConfig: function (loaderConfig) {
				loaderConfig.recordStats = true;
			},
			beforeRequire: function () {
				performance.mark('code/willLoadWorkbenchMain');

				// It looks like browsers only lazily enable
				// the <canvas> element when needed. Since we
				// leverage canvas elements in our code in many
				// locations, we try to help the browser to
				// initialize canvas when it is idle, right
				// before we wait for the scripts to be loaded.
				// @ts-ignore
				window.requestIdleCallback(() => {
					const canvas = document.createElement('canvas');
					const context = canvas.getContext('2d');
					context?.clearRect(0, 0, canvas.width, canvas.height);
					canvas.remove();
				}, { timeout: 50 });
			}
		}
	);

	//#region Helpers

	/**
	 * @typedef {import('../../../platform/window/common/window').INativeWindowConfiguration} INativeWindowConfiguration
	 * @typedef {import('../../../platform/environment/common/argv').NativeParsedArgs} NativeParsedArgs
	 *
	 * @returns {{
	 *   load: (
	 *     modules: string[],
	 *     resultCallback: (result, configuration: INativeWindowConfiguration & NativeParsedArgs) => unknown,
	 *     options?: {
	 *       configureDeveloperSettings?: (config: INativeWindowConfiguration & NativeParsedArgs) => {
	 * 			forceDisableShowDevtoolsOnError?: boolean,
	 * 			forceEnableDeveloperKeybindings?: boolean,
	 * 			disallowReloadKeybinding?: boolean,
	 * 			removeDeveloperKeybindingsAfterLoad?: boolean
	 * 		 },
	 * 	     canModifyDOM?: (config: INativeWindowConfiguration & NativeParsedArgs) => void,
	 * 	     beforeLoaderConfig?: (loaderConfig: object) => void,
	 *       beforeRequire?: () => void
	 *     }
	 *   ) => Promise<unknown>
	 * }}
	 */
	function bootstrapWindowLib() {
		// @ts-ignore (defined in bootstrap-window.js)
		return window.MonacoBootstrapWindow;
	}

	/**
	 * @param {INativeWindowConfiguration & NativeParsedArgs} configuration
	 */
	function showSplash(configuration) {
		performance.mark('code/willShowPartsSplash');

		let data = configuration.partsSplash;

		if (data) {
			// high contrast mode has been turned by the OS -> ignore stored colors and layouts
			if (configuration.autoDetectHighContrast && configuration.colorScheme.highContrast) {
				if ((configuration.colorScheme.dark && data.baseTheme !== 'hc-black') || (!configuration.colorScheme.dark && data.baseTheme !== 'hc-light')) {
					data = undefined;
				}
			} else if (configuration.autoDetectColorScheme) {
				// OS color scheme is tracked and has changed
				if ((configuration.colorScheme.dark && data.baseTheme !== 'vs-dark') || (!configuration.colorScheme.dark && data.baseTheme !== 'vs')) {
					data = undefined;
				}
			}
		}

		// developing an extension -> ignore stored layouts
		if (data && configuration.extensionDevelopmentPath) {
			data.layoutInfo = undefined;
		}

		// minimal color configuration (works with or without persisted data)
		let baseTheme, shellBackground, shellForeground;
		if (data) {
			baseTheme = data.baseTheme;
			shellBackground = data.colorInfo.editorBackground;
			shellForeground = data.colorInfo.foreground;
		} else if (configuration.autoDetectHighContrast && configuration.colorScheme.highContrast) {
			if (configuration.colorScheme.dark) {
				baseTheme = 'hc-black';
				shellBackground = '#000000';
				shellForeground = '#FFFFFF';
			} else {
				baseTheme = 'hc-light';
				shellBackground = '#FFFFFF';
				shellForeground = '#000000';
			}
		} else if (configuration.autoDetectColorScheme) {
			if (configuration.colorScheme.dark) {
				baseTheme = 'vs-dark';
				shellBackground = '#1E1E1E';
				shellForeground = '#CCCCCC';
			} else {
				baseTheme = 'vs';
				shellBackground = '#FFFFFF';
				shellForeground = '#000000';
			}
		}

		const style = document.createElement('style');
		style.className = 'initialShellColors';
		document.head.appendChild(style);
		style.textContent = `body { background-color: ${shellBackground}; color: ${shellForeground}; margin: 0; padding: 0; }`;

		// set zoom level as soon as possible
		if (typeof data?.zoomLevel === 'number' && typeof globalThis.vscode?.webFrame?.setZoomLevel === 'function') {
			globalThis.vscode.webFrame.setZoomLevel(data.zoomLevel);
		}

		// restore parts if possible (we might not always store layout info)
		if (data?.layoutInfo) {
			const { layoutInfo, colorInfo } = data;

			const splash = document.createElement('div');
			splash.id = 'monaco-parts-splash';
			splash.className = baseTheme;

			if (layoutInfo.windowBorder) {
				splash.style.position = 'relative';
				splash.style.height = 'calc(100vh - 2px)';
				splash.style.width = 'calc(100vw - 2px)';
				splash.style.border = '1px solid var(--window-border-color)';
				splash.style.setProperty('--window-border-color', colorInfo.windowBorder);

				if (layoutInfo.windowBorderRadius) {
					splash.style.borderRadius = layoutInfo.windowBorderRadius;
				}
			}

			// ensure there is enough space
			layoutInfo.sideBarWidth = Math.min(layoutInfo.sideBarWidth, window.innerWidth - (layoutInfo.activityBarWidth   layoutInfo.editorPartMinWidth));

			// part: title
			const titleDiv = document.createElement('div');
			titleDiv.setAttribute('style', `position: absolute; width: 100%; left: 0; top: 0; height: ${layoutInfo.titleBarHeight}px; background-color: ${colorInfo.titleBarBackground}; -webkit-app-region: drag;`);
			splash.appendChild(titleDiv);

			// part: activity bar
			const activityDiv = document.createElement('div');
			activityDiv.setAttribute('style', `position: absolute; height: calc(100% - ${layoutInfo.titleBarHeight}px); top: ${layoutInfo.titleBarHeight}px; ${layoutInfo.sideBarSide}: 0; width: ${layoutInfo.activityBarWidth}px; background-color: ${colorInfo.activityBarBackground};`);
			splash.appendChild(activityDiv);

			// part: side bar (only when opening workspace/folder)
			// folder or workspace -> status bar color, sidebar
			if (configuration.workspace) {
				const sideDiv = document.createElement('div');
				sideDiv.setAttribute('style', `position: absolute; height: calc(100% - ${layoutInfo.titleBarHeight}px); top: ${layoutInfo.titleBarHeight}px; ${layoutInfo.sideBarSide}: ${layoutInfo.activityBarWidth}px; width: ${layoutInfo.sideBarWidth}px; background-color: ${colorInfo.sideBarBackground};`);
				splash.appendChild(sideDiv);
			}

			// part: statusbar
			const statusDiv = document.createElement('div');
			statusDiv.setAttribute('style', `position: absolute; width: 100%; bottom: 0; left: 0; height: ${layoutInfo.statusBarHeight}px; background-color: ${configuration.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`);
			splash.appendChild(statusDiv);

			document.body.appendChild(splash);
		}

		performance.mark('code/didShowPartsSplash');
	}

	//#endregion
}());

这里核心就是加载vs/workbench/workbench.desktop.main这个模块,对应的nlscss也一起加载,然后调用入口逻辑启动:

return desktopMain.main(configuration);

这里还做了一些模块的设置,其中在canModifyDOM回调里执行了showSplash

这里的showSplash其实是展示一个引导界面,相当于应用启动前的骨架屏loading,在canModifyDOM回调里确保此时可以安全的操纵DOM。

接下来我们来看一下界面入口文件:

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/


// #######################################################################
// ###                                                                 ###
// ### !!! PLEASE ADD COMMON IMPORTS INTO WORKBENCH.COMMON.MAIN.TS !!! ###
// ###                                                                 ###
// #######################################################################

//#region --- workbench common

import 'vs/workbench/workbench.common.main';

//#endregion


//#region --- workbench (desktop main)

import 'vs/workbench/electron-sandbox/desktop.main';
import 'vs/workbench/electron-sandbox/desktop.contribution';

//#endregion


//#region --- workbench parts

import 'vs/workbench/electron-sandbox/parts/dialogs/dialog.contribution';

//#endregion


//#region --- workbench services

import 'vs/workbench/services/textfile/electron-sandbox/nativeTextFileService';
import 'vs/workbench/services/dialogs/electron-sandbox/fileDialogService';
import 'vs/workbench/services/workspaces/electron-sandbox/workspacesService';
import 'vs/workbench/services/menubar/electron-sandbox/menubarService';
import 'vs/workbench/services/issue/electron-sandbox/issueService';
import 'vs/workbench/services/update/electron-sandbox/updateService';
import 'vs/workbench/services/url/electron-sandbox/urlService';
import 'vs/workbench/services/lifecycle/electron-sandbox/lifecycleService';
import 'vs/workbench/services/title/electron-sandbox/titleService';
import 'vs/workbench/services/host/electron-sandbox/nativeHostService';
import 'vs/workbench/services/request/electron-sandbox/requestService';
import 'vs/workbench/services/clipboard/electron-sandbox/clipboardService';
import 'vs/workbench/services/contextmenu/electron-sandbox/contextmenuService';
import 'vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService';
import 'vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService';
import 'vs/workbench/services/accessibility/electron-sandbox/accessibilityService';
import 'vs/workbench/services/keybinding/electron-sandbox/nativeKeyboardLayout';
import 'vs/workbench/services/path/electron-sandbox/pathService';
import 'vs/workbench/services/themes/electron-sandbox/nativeHostColorSchemeService';
import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService';
import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionUrlTrustService';
import 'vs/workbench/services/credentials/electron-sandbox/credentialsService';
import 'vs/workbench/services/encryption/electron-sandbox/encryptionService';
import 'vs/workbench/services/localization/electron-sandbox/languagePackService';
import 'vs/workbench/services/telemetry/electron-sandbox/telemetryService';
import 'vs/workbench/services/extensions/electron-sandbox/extensionHostStarter';
import 'vs/platform/extensionResourceLoader/common/extensionResourceLoaderService';
import 'vs/workbench/services/localization/electron-sandbox/localeService';
import 'vs/platform/extensionManagement/electron-sandbox/extensionsScannerService';
import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService';
import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionTipsService';
import 'vs/workbench/services/userDataSync/electron-sandbox/userDataSyncMachinesService';
import 'vs/workbench/services/userDataSync/electron-sandbox/userDataSyncService';
import 'vs/workbench/services/userDataSync/electron-sandbox/userDataSyncAccountService';
import 'vs/workbench/services/userDataSync/electron-sandbox/userDataSyncStoreManagementService';
import 'vs/workbench/services/userDataSync/electron-sandbox/userDataAutoSyncService';
import 'vs/workbench/services/timer/electron-sandbox/timerService';
import 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService';
import 'vs/workbench/services/integrity/electron-sandbox/integrityService';
import 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService';
import 'vs/workbench/services/checksum/electron-sandbox/checksumService';
import 'vs/platform/remote/electron-sandbox/sharedProcessTunnelService';
import 'vs/workbench/services/tunnel/electron-sandbox/tunnelService';
import 'vs/platform/diagnostics/electron-sandbox/diagnosticsService';
import 'vs/platform/profiling/electron-sandbox/profilingService';
import 'vs/platform/telemetry/electron-sandbox/customEndpointTelemetryService';
import 'vs/platform/remoteTunnel/electron-sandbox/remoteTunnelService';
import 'vs/workbench/services/files/electron-sandbox/elevatedFileService';
import 'vs/workbench/services/search/electron-sandbox/searchService';
import 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService';
import 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService';
import 'vs/workbench/services/extensions/electron-sandbox/nativeExtensionService';
import 'vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService';

import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit';
import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService';

registerSingleton(IUserDataInitializationService, UserDataInitializationService, InstantiationType.Delayed);
registerSingleton(IExtensionsProfileScannerService, ExtensionsProfileScannerService, InstantiationType.Delayed);


//#endregion


//#region --- workbench contributions

// Logs
import 'vs/workbench/contrib/logs/electron-sandbox/logs.contribution';

// Localizations
import 'vs/workbench/contrib/localization/electron-sandbox/localization.contribution';

// Explorer
import 'vs/workbench/contrib/files/electron-sandbox/files.contribution';
import 'vs/workbench/contrib/files/electron-sandbox/fileActions.contribution';

// CodeEditor Contributions
import 'vs/workbench/contrib/codeEditor/electron-sandbox/codeEditor.contribution';

// Debug
import 'vs/workbench/contrib/debug/electron-sandbox/extensionHostDebugService';

// Extensions Management
import 'vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution';

// Issues
import 'vs/workbench/contrib/issue/electron-sandbox/issue.contribution';

// Remote
import 'vs/workbench/contrib/remote/electron-sandbox/remote.contribution';

// Configuration Exporter
import 'vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.contribution';

// Terminal
import 'vs/workbench/contrib/terminal/electron-sandbox/terminal.contribution';

// Themes Support
import 'vs/workbench/contrib/themes/browser/themes.test.contribution';

// User Data Sync
import 'vs/workbench/contrib/userDataSync/electron-sandbox/userDataSync.contribution';

// Tags
import 'vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService';
import 'vs/workbench/contrib/tags/electron-sandbox/tags.contribution';

// Performance
import 'vs/workbench/contrib/performance/electron-sandbox/performance.contribution';

// Tasks
import 'vs/workbench/contrib/tasks/electron-sandbox/taskService';

// External terminal
import 'vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution';

// Webview
import 'vs/workbench/contrib/webview/electron-sandbox/webview.contribution';

// Splash
import 'vs/workbench/contrib/splash/electron-sandbox/splash.contribution';

// Local History
import 'vs/workbench/contrib/localHistory/electron-sandbox/localHistory.contribution';

// Merge Editor
import 'vs/workbench/contrib/mergeEditor/electron-sandbox/mergeEditor.contribution';

// Remote Tunnel
import 'vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution';

// Sandbox
import 'vs/workbench/contrib/sandbox/electron-sandbox/sandbox.contribution';

//#endregion


export { main } from 'vs/workbench/electron-sandbox/desktop.main';

这个入口文件引入了很多界面需要的模块,具体来说:

  • workbench.common.main 导入了 Workbench 的常见模块和服务。
  • electron-sandbox/desktop.main 导入了启动 Electron Sandbox 所需的模块和服务。
  • electron-sandbox/desktop.contribution 注册了与 Electron Sandbox 有关的扩展。
  • electron-sandbox/parts/dialogs/dialog.contribution 注册了对话框相关的扩展。
  • services 目录下的文件注册了 Workbench 的各种服务,例如文件服务、对话框服务、窗口管理服务、菜单服务、更新服务等等。
  • workbench.services.workspaces.electron-sandbox.workspaceEditingService 导入了 Workspace 编辑服务。
  • accessibility/electron-sandbox/accessibilityService 导入了无障碍服务。
  • extensions/electron-sandbox/extensionManagementService 导入了扩展管理服务。
  • services/localization/electron-sandbox/localeService 导入了本地化服务。
  • services/userDataSync/electron-sandbox/userDataSyncService 导入了用户数据同步服务。
  • services/timer/electron-sandbox/timerService 导入了计时器服务。
  • services/environment/electron-sandbox/shellEnvironmentService 导入了shell环境服务。
  • services/workingCopy/electron-sandbox/workingCopyBackupService 导入了工作副本备份服务。
  • services/checksum/electron-sandbox/checksumService 导入了校验和服务。
  • remote-sandbox/sharedProcessTunnelService 导入了远程共享服务隧道服务。
  • services/tunnel/electron-sandbox/tunnelService 导入了隧道服务。
  • diagnostics/electron-sandbox/diagnosticsService 导入了诊断服务。
  • profiling/electron-sandbox/profilingService 导入了性能分析服务。
  • services/files/electron-sandbox/elevatedFileService 导入了提权文件服务。
  • services/search/electron-sandbox/searchService 导入了搜索服务。
  • services/userDataSync/browser/userDataSyncEnablementService 导入了用户数据同步启用服务。
  • services/extensions/electron-sandbox/nativeExtensionService 导入了本机扩展服务。
  • userDataProfile/electron-sandbox/userDataProfileStorageService 导入了用户数据存储服务。
  • userDataInitextensionsProfileScannerService 注册了单例服务。
  • contributions 目录下的文件注册了各种工作台的扩展,例如日志、本地化、文件浏览器、编辑器、调试、扩展、问题、远程、终端等等。
  • main 函数是 Electron Sandbox 的入口函数,它将启动 Electron 进程,并创建 Electron Sandbox 窗口。

接下来看一下main函数的实现(它位于vs/workbench/electron-sandbox/desktop.main.ts):

export function main(configuration: INativeWindowConfiguration): Promise<void> {
	const workbench = new DesktopMain(configuration);

	return workbench.open();
}

可以看到这里调用了workbenchopen方法启动程序:

async open(): Promise<void> {

		// Init services and wait for DOM to be ready in parallel
		const [services] = await Promise.all([this.initServices(), domContentLoaded()]);

		// Create Workbench
		const workbench = new Workbench(document.body, { extraClasses: this.getExtraClasses() }, services.serviceCollection, services.logService);

		// Listeners
		this.registerListeners(workbench, services.storageService);

		// Startup
		const instantiationService = workbench.startup();

		// Window
		this._register(instantiationService.createInstance(NativeWindow));
	}

open方法主要做了几件事情:

  • 首先调用this.initServices()来初始化服务,这个和DOMLoad事件是并行的。
  • 创建workbench实例,这个时候拿到document.body给它。
  • 初始化事件并调用workbenchstartup方法。
  • 初始化NativeWindow的实例。

我们先来看看initServices都初始化了什么服务:

private async initServices(): Promise<{ serviceCollection: ServiceCollection; logService: ILogService; storageService: NativeWorkbenchStorageService }> {
		const serviceCollection = new ServiceCollection();


		// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
		//
		// NOTE: Please do NOT register services here. Use `registerSingleton()`
		//       from `workbench.common.main.ts` if the service is shared between
		//       desktop and web or `workbench.desktop.main.ts` if the service
		//       is desktop only.
		//
		// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


		// Main Process
		const mainProcessService = this._register(new ElectronIPCMainProcessService(this.configuration.windowId));
		serviceCollection.set(IMainProcessService, mainProcessService);

		// Policies
		const policyService = this.configuration.policiesData ? new PolicyChannelClient(this.configuration.policiesData, mainProcessService.getChannel('policy')) : new NullPolicyService();
		serviceCollection.set(IPolicyService, policyService);

		// Product
		const productService: IProductService = { _serviceBrand: undefined, ...product };
		serviceCollection.set(IProductService, productService);

		// Environment
		const environmentService = new NativeWorkbenchEnvironmentService(this.configuration, productService);
		serviceCollection.set(INativeWorkbenchEnvironmentService, environmentService);

		// Logger
		const loggers = [
			...this.configuration.loggers.global.map(loggerResource => ({ ...loggerResource, resource: URI.revive(loggerResource.resource) })),
			...this.configuration.loggers.window.map(loggerResource => ({ ...loggerResource, resource: URI.revive(loggerResource.resource), hidden: true })),
		];
		const loggerService = new LoggerChannelClient(this.configuration.windowId, this.configuration.logLevel, environmentService.logsHome, loggers, mainProcessService.getChannel('logger'));
		serviceCollection.set(ILoggerService, loggerService);

		// Log
		const logService = this._register(new NativeLogService(loggerService, environmentService));
		serviceCollection.set(ILogService, logService);
		if (isCI) {
			logService.info('workbench#open()'); // marking workbench open helps to diagnose flaky integration/smoke tests
		}
		if (logService.getLevel() === LogLevel.Trace) {
			logService.trace('workbench#open(): with configuration', safeStringify(this.configuration));
		}
		if (process.sandboxed) {
			logService.info('Electron sandbox mode is enabled!');
		}

		// Shared Process
		const sharedProcessService = new SharedProcessService(this.configuration.windowId, logService);
		serviceCollection.set(ISharedProcessService, sharedProcessService);

		// Utility Process Worker
		const utilityProcessWorkerWorkbenchService = new UtilityProcessWorkerWorkbenchService(this.configuration.windowId, this.configuration.preferUtilityProcess, logService, sharedProcessService, mainProcessService);
		serviceCollection.set(IUtilityProcessWorkerWorkbenchService, utilityProcessWorkerWorkbenchService);

		// Remote
		const remoteAuthorityResolverService = new RemoteAuthorityResolverService(productService);
		serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);


		// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
		//
		// NOTE: Please do NOT register services here. Use `registerSingleton()`
		//       from `workbench.common.main.ts` if the service is shared between
		//       desktop and web or `workbench.desktop.main.ts` if the service
		//       is desktop only.
		//
		// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


		// Sign
		const signService = ProxyChannel.toService<ISignService>(mainProcessService.getChannel('sign'));
		serviceCollection.set(ISignService, signService);

		// Files
		const fileService = this._register(new FileService(logService));
		serviceCollection.set(IWorkbenchFileService, fileService);

		// Local Files
		const diskFileSystemProvider = this._register(new DiskFileSystemProvider(mainProcessService, utilityProcessWorkerWorkbenchService, logService));
		fileService.registerProvider(Schemas.file, diskFileSystemProvider);

		// User Data Provider
		fileService.registerProvider(Schemas.vscodeUserData, this._register(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.vscodeUserData, logService)));

		// URI Identity
		const uriIdentityService = new UriIdentityService(fileService);
		serviceCollection.set(IUriIdentityService, uriIdentityService);

		// User Data Profiles
		const userDataProfilesService = new UserDataProfilesService(this.configuration.profiles.all, URI.revive(this.configuration.profiles.home).with({ scheme: environmentService.userRoamingDataHome.scheme }), mainProcessService.getChannel('userDataProfiles'));
		serviceCollection.set(IUserDataProfilesService, userDataProfilesService);
		const userDataProfileService = new UserDataProfileService(reviveProfile(this.configuration.profiles.profile, userDataProfilesService.profilesHome.scheme), userDataProfilesService);
		serviceCollection.set(IUserDataProfileService, userDataProfileService);

		// Remote Agent
		const remoteAgentService = this._register(new RemoteAgentService(userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService));
		serviceCollection.set(IRemoteAgentService, remoteAgentService);

		// Remote Files
		this._register(RemoteFileSystemProviderClient.register(remoteAgentService, fileService, logService));

		// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
		//
		// NOTE: Please do NOT register services here. Use `registerSingleton()`
		//       from `workbench.common.main.ts` if the service is shared between
		//       desktop and web or `workbench.desktop.main.ts` if the service
		//       is desktop only.
		//
		// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

		// Create services that require resolving in parallel
		const workspace = this.resolveWorkspaceIdentifier(environmentService);
		const [configurationService, storageService] = await Promise.all([
			this.createWorkspaceService(workspace, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService, policyService).then(service => {

				// Workspace
				serviceCollection.set(IWorkspaceContextService, service);

				// Configuration
				serviceCollection.set(IWorkbenchConfigurationService, service);

				return service;
			}),

			this.createStorageService(workspace, environmentService, userDataProfileService, userDataProfilesService, mainProcessService).then(service => {

				// Storage
				serviceCollection.set(IStorageService, service);

				return service;
			}),

			this.createKeyboardLayoutService(mainProcessService).then(service => {

				// KeyboardLayout
				serviceCollection.set(INativeKeyboardLayoutService, service);

				return service;
			})
		]);

		// Workspace Trust Service
		const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService);
		serviceCollection.set(IWorkspaceTrustEnablementService, workspaceTrustEnablementService);

		const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService, fileService);
		serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService);

		// Update workspace trust so that configuration is updated accordingly
		configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted());
		this._register(workspaceTrustManagementService.onDidChangeTrust(() => configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted())));


		// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
		//
		// NOTE: Please do NOT register services here. Use `registerSingleton()`
		//       from `workbench.common.main.ts` if the service is shared between
		//       desktop and web or `workbench.desktop.main.ts` if the service
		//       is desktop only.
		//
		// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


		return { serviceCollection, logService, storageService };
	}

大致列一下:

  • mainProcessService,一个ElectronIPCMainProcessService服务。
  • policyService,一个PolicyChannelClient或NullPolicyService服务,具体取决于this.configuration.policiesData是否存在。
  • productService,一个IProductService服务,它包含了一些产品相关的信息。
  • environmentService,一个NativeWorkbenchEnvironmentService服务,它提供了一些与环境相关的信息。
  • loggerService,一个LoggerChannelClient服务,它提供了一个日志服务。
  • logService,一个NativeLogService服务,它使用了上述的loggerService提供的日志服务,但提供了更多的功能。
  • sharedProcessService,一个SharedProcessService服务,它提供了一些共享进程相关的功能。
  • utilityProcessWorkerWorkbenchService,一个UtilityProcessWorkerWorkbenchService服务,它提供了一些与工作台有关的实用程序进程相关的功能。
  • remoteAuthorityResolverService,一个RemoteAuthorityResolverService服务,它提供了一些与远程连接相关的功能。
  • fileService,一个FileService服务,它提供了文件系统相关的功能。
  • diskFileSystemProvider,一个DiskFileSystemProvider服务,它提供了访问本地磁盘文件系统的功能。
  • userDataProfilesService,一个UserDataProfilesService服务,它提供了一些与用户数据相关的功能。
  • userDataProfileService,一个UserDataProfileService服务,它提供了一些用户数据配置文件相关的功能。
  • remoteAgentService,一个RemoteAgentService服务,它提供了一些与远程代理相关的功能。
  • workspaceTrustEnablementService,一个WorkspaceTrustEnablementService服务,它提供了一些与工作区信任相关的功能。
  • workspaceTrustManagementService,一个WorkspaceTrustManagementService服务,它提供了一些与工作区信任管理相关的功能。

其实这里和electron-main那个入口初始化的非常像了,这里把serviceCollection传入了Workbench中。

接下来看一下Workbenchstartup

tartup(): IInstantiationService {
		try {

			// Configure emitter leak warning threshold
			setGlobalLeakWarningThreshold(175);

			// Services
			const instantiationService = this.initServices(this.serviceCollection);

			instantiationService.invokeFunction(accessor => {
				const lifecycleService = accessor.get(ILifecycleService);
				const storageService = accessor.get(IStorageService);
				const configurationService = accessor.get(IConfigurationService);
				const hostService = accessor.get(IHostService);
				const dialogService = accessor.get(IDialogService);

				// Layout
				this.initLayout(accessor);

				// Registries
				Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).start(accessor);
				Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor);

				// Context Keys
				this._register(instantiationService.createInstance(WorkbenchContextKeysHandler));

				// Register Listeners
				this.registerListeners(lifecycleService, storageService, configurationService, hostService, dialogService);

				// Render Workbench
				this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);

				// Workbench Layout
				this.createWorkbenchLayout();

				// Layout
				this.layout();

				// Restore
				this.restore(lifecycleService);
			});

			return instantiationService;
		} catch (error) {
			onUnexpectedError(error);

			throw error; // rethrow because this is a critical issue we cannot handle properly here
		}
	}

这里做了几件事情:

  • 初始化Layout,实际上Workbench就是继承自Layout
  • 注册插件
  • 注册上下文
  • 渲染workbench
  • 创建Layout
  • 进行布局Layout
  • restore启动

我们重点来看一下渲染相关的代码:

private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void {

		// ARIA
		setARIAContainer(this.container);

		// State specific classes
		const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac';
		const workbenchClasses = coalesce([
			'monaco-workbench',
			platformClass,
			isWeb ? 'web' : undefined,
			isChrome ? 'chromium' : isFirefox ? 'firefox' : isSafari ? 'safari' : undefined,
			...this.getLayoutClasses(),
			...(this.options?.extraClasses ? this.options.extraClasses : [])
		]);

		this.container.classList.add(...workbenchClasses);
		document.body.classList.add(platformClass); // used by our fonts

		if (isWeb) {
			document.body.classList.add('web');
		}

		// Apply font aliasing
		this.updateFontAliasing(undefined, configurationService);

		// Warm up font cache information before building up too many dom elements
		this.restoreFontInfo(storageService, configurationService);

		// Create Parts
		for (const { id, role, classes, options } of [
			{ id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] },
			{ id: Parts.BANNER_PART, role: 'banner', classes: ['banner'] },
			{ id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] }, // Use role 'none' for some parts to make screen readers less chatty #114892
			{ id: Parts.SIDEBAR_PART, role: 'none', classes: ['sidebar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] },
			{ id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.willRestoreEditors() } },
			{ id: Parts.PANEL_PART, role: 'none', classes: ['panel', 'basepanel', positionToString(this.getPanelPosition())] },
			{ id: Parts.AUXILIARYBAR_PART, role: 'none', classes: ['auxiliarybar', 'basepanel', this.getSideBarPosition() === Position.LEFT ? 'right' : 'left'] },
			{ id: Parts.STATUSBAR_PART, role: 'status', classes: ['statusbar'] }
		]) {
			const partContainer = this.createPart(id, role, classes);

			mark(`code/willCreatePart/${id}`);
			this.getPart(id).create(partContainer, options);
			mark(`code/didCreatePart/${id}`);
		}

		// Notification Handlers
		this.createNotificationsHandlers(instantiationService, notificationService);

		// Add Workbench to DOM
		this.parent.appendChild(this.container);
	}

可以看到renderWorkbenchcontainer加到了parent中,但这个时候part还没有被添加,实际上part就是指workbench的几个重要的部件:

  • TITLEBAR_PART:标题栏,角色为 contentinfo,CSS 类为 titlebar。
  • BANNER_PART:横幅,角色为 banner,CSS 类为 banner。
  • ACTIVITYBAR_PART:活动栏,角色为 none,CSS 类为 activitybar,并根据侧边栏位置添加 left 或 right 类。
  • SIDEBAR_PART:侧边栏,角色为 none,CSS 类为 sidebar,并根据侧边栏位置添加 left 或 right 类。
  • EDITOR_PART:编辑器区域,角色为 main,CSS 类为 editor,根据用户设置决定是否还原编辑器状态。
  • PANEL_PART:面板,角色为 none,CSS 类为 panel、basepanel,并根据面板位置添加 bottom 或 right 类。
  • AUXILIARYBAR_PART:辅助栏,角色为 none,CSS 类为 auxiliarybar、basepanel,并根据侧边栏位置添加 left 或 right 类。

这几个部件组成了整个workbenchLayout。接下来在创建Layout的时候会插入到container当中:

protected createWorkbenchLayout(): void {
		const titleBar = this.getPart(Parts.TITLEBAR_PART);
		const bannerPart = this.getPart(Parts.BANNER_PART);
		const editorPart = this.getPart(Parts.EDITOR_PART);
		const activityBar = this.getPart(Parts.ACTIVITYBAR_PART);
		const panelPart = this.getPart(Parts.PANEL_PART);
		const auxiliaryBarPart = this.getPart(Parts.AUXILIARYBAR_PART);
		const sideBar = this.getPart(Parts.SIDEBAR_PART);
		const statusBar = this.getPart(Parts.STATUSBAR_PART);

		// View references for all parts
		this.titleBarPartView = titleBar;
		this.bannerPartView = bannerPart;
		this.sideBarPartView = sideBar;
		this.activityBarPartView = activityBar;
		this.editorPartView = editorPart;
		this.panelPartView = panelPart;
		this.auxiliaryBarPartView = auxiliaryBarPart;
		this.statusBarPartView = statusBar;

		const viewMap = {
			[Parts.ACTIVITYBAR_PART]: this.activityBarPartView,
			[Parts.BANNER_PART]: this.bannerPartView,
			[Parts.TITLEBAR_PART]: this.titleBarPartView,
			[Parts.EDITOR_PART]: this.editorPartView,
			[Parts.PANEL_PART]: this.panelPartView,
			[Parts.SIDEBAR_PART]: this.sideBarPartView,
			[Parts.STATUSBAR_PART]: this.statusBarPartView,
			[Parts.AUXILIARYBAR_PART]: this.auxiliaryBarPartView
		};

		const fromJSON = ({ type }: { type: Parts }) => viewMap[type];
		const workbenchGrid = SerializableGrid.deserialize(
			this.createGridDescriptor(),
			{ fromJSON },
			{ proportionalLayout: false }
		);

		this.container.prepend(workbenchGrid.element);
		this.container.setAttribute('role', 'application');
		this.workbenchGrid = workbenchGrid;
		this.workbenchGrid.edgeSnapping = this.state.runtime.fullscreen;

		for (const part of [titleBar, editorPart, activityBar, panelPart, sideBar, statusBar, auxiliaryBarPart, bannerPart]) {
			this._register(part.onDidVisibilityChange((visible) => {
				if (part === sideBar) {
					this.setSideBarHidden(!visible, true);
				} else if (part === panelPart) {
					this.setPanelHidden(!visible, true);
				} else if (part === auxiliaryBarPart) {
					this.setAuxiliaryBarHidden(!visible, true);
				} else if (part === editorPart) {
					this.setEditorHidden(!visible, true);
				}
				this._onDidChangePartVisibility.fire();
				this._onDidLayout.fire(this._dimension);
			}));
		}

		this._register(this.storageService.onWillSaveState(willSaveState => {
			if (willSaveState.reason === WillSaveStateReason.SHUTDOWN) {
				// Side Bar Size
				const sideBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN)
					? this.workbenchGrid.getViewCachedVisibleSize(this.sideBarPartView)
					: this.workbenchGrid.getViewSize(this.sideBarPartView).width;
				this.stateModel.setInitializationValue(LayoutStateKeys.SIDEBAR_SIZE, sideBarSize as number);

				// Panel Size
				const panelSize = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN)
					? this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView)
					: (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) === Position.BOTTOM ? this.workbenchGrid.getViewSize(this.panelPartView).height : this.workbenchGrid.getViewSize(this.panelPartView).width);
				this.stateModel.setInitializationValue(LayoutStateKeys.PANEL_SIZE, panelSize as number);

				// Auxiliary Bar Size
				const auxiliaryBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN)
					? this.workbenchGrid.getViewCachedVisibleSize(this.auxiliaryBarPartView)
					: this.workbenchGrid.getViewSize(this.auxiliaryBarPartView).width;
				this.stateModel.setInitializationValue(LayoutStateKeys.AUXILIARYBAR_SIZE, auxiliaryBarSize as number);

				this.stateModel.save(true, true);
			}
		}));
	}

最后再通过layout来重新计算区域宽高等信息。

至此整个workbench的大概启动流程就结束了。

关于VSCode启动过程中的性能点

还记得我们上篇分析了VSCode的性能打点,现在我们已经整个介绍完了从主进程到渲染进程的启动流程,可以对VSCode的重要打点做一个梳理,更好地理解整个启动流程:

统计指标 计算方式 说明
start => app.isReady code/mainAppReady-code/didStartMain 这个计算的是app.on('ready')的时间
app.isReady => window.loadUrl() willOpenNewWindow-mainAppReady 表示的是从监听ready到开始打开窗口这段时间,这里包含了整个主进程bundle的加载时延
window.loadUrl() => begin to require(workbench.desktop.main.js) willLoadWorkbenchMain-willOpenNewWindow willLoadWorkbenchMain实际上是在html的script标签前,意味着这里包含了渲染进程的延时
require(workbench.desktop.main.js) didLoadWorkbenchMain-willLoadWorkbenchMain didLoadWorkbenchMain是加载器加载完整个main的延时
register extensions & spawn extension host didLoadExtensions-willLoadExtensions 加载插件的延时
overall workbench load didStartWorkbench-willStartWorkbench willStartWorkbench是在workbench的构造函数里打点,这里实际上记录的是整个setup的时间
workbench ready didStartWorkbench-didStartMain 这里实际上是从主进程开始到渲染完成的时延,也是最重要的一个指标了
renderer ready didStartWorkbench-didStartRenderer 这里记录的是renderer进程开始到workbench开始渲染的时延
extensions registered didLoadExtensions-didStartMain 从开始到插件加载完成的时延,这个应该是vscode里面耗时最长的一个指标了

小结

本文分析了VSCode的窗口加载机制,以及渲染进程的启动流程,最后也剖析了一下VSCode在启动流程中的重要打点性能指标,要点如下:

  • VSCode跟窗口相关的两个服务,CodeWindow负责控制窗口的创建销毁、生命周期等等,windowsMainService负责管理CodeWindow实例,控制各种不同的打开方式和新窗口打开。
  • 渲染进程通过AMD模块加载器启动,也有和主进程类似的服务初始化逻辑,用于启动依赖注入机制。
  • 在渲染进程中workbench是继承自Layout的,Layout本身包含了标题栏、侧边栏、编辑器等多个part,组成了主面板的整个布局,通过各个part的初始化完成渲染,最后add到Container的DOM上,完成主面板的渲染流程。
  • VSCode本身性能打点是从主进程第一行代码开始计算,重点的几个指标包含ready、打开窗口、渲染进程ready、渲染进程完成以及插件加载完成几个点。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanffaee
系列文章
更多 icon
同类精品
更多 icon
继续加载