深入理解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;
}
CodeWindow
是VSCode
主窗口的主要实现类,它控制了整个窗口的创建销毁、生命周期等等,具体来说,它上面暴露出的属性有:
onWillLoad
:一个ILoadEvent
类型的事件,当窗口开始加载时触发。onDidSignalReady
:一个没有参数的事件,当窗口准备就绪并且可以开始渲染时触发。onDidTriggerSystemContextMenu
:一个{x: number, y: number}
类型的事件,当用户在窗口上右键点击时触发,返回右键点击位置的坐标。onDidClose
:一个没有参数的事件,当窗口关闭时触发。onDidDestroy
:一个没有参数的事件,当窗口销毁时触发。whenClosedOrLoaded
:一个Promise
类型,当窗口关闭或准备就绪时会被resolve
。id
:一个number
类型,表示窗口的唯一标识符。win
:一个BrowserWindow
或null
类型,表示窗口的实例或者是null
如果窗口已经被销毁。config
:一个INativeWindowConfiguration
或undefined
类型,表示窗口的配置对象。openedWorkspace
:一个IWorkspaceIdentifier
或ISingleFolderWorkspaceIdentifier
类型,表示在窗口中打开的工作区或者单一文件夹。profile
:一个IUserDataProfile
类型,表示用户数据的配置文件。backupPath
:一个string
或undefined
类型,表示与窗口关联的备份文件的路径。remoteAuthority
:一个string
或undefined
类型,表示与窗口关联的远程服务器的身份验证信息。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 窗口,并返回窗口实例。可以通过IOpenEmptyConfiguration
和IOpenEmptyWindowOptions
参数指定窗口的选项,如是否全屏、是否显示菜单等。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
方法大致做了几件事情:
- 根据
openConfig
获取要打开的文件、文件夹、工作区、空白窗口等信息。 - 处理
openConfig
中的一些特殊参数,比如--add
、--diff
、--merge
、--wait
、--no-recent-entry
等。 - 根据获取到的信息,调用
doOpen
方法打开新的窗口,并返回已经打开的窗口数组。 - 根据打开的窗口数量决定哪个窗口要被聚焦,并在 recent list 中添加相应的条目。
- 处理
--wait
参数。 - 返回已经打开的窗口数组。
首先在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 };
}
这里还是在处理打开文件、文件夹、工作区的事情,基本逻辑就是在当前找到一个合适的窗口打开,找不到就开个新窗口打开:
doOpen
方法首先检查foldersToAdd
数组,如果存在,则尝试在最后一个活动的工作区中添加这些文件夹。如果没有找到匹配的工作区,则打开一个空白窗口,并在其中打开文件夹。- 如果存在要打开的文件,则
doOpen
方法尝试将这些文件打开到一个现有窗口中,或在新的窗口中打开。首先,它查找一个合适的窗口来打开文件,如果找到了一个窗口,那么文件将在该窗口中打开。否则,它会在最后一个活动窗口中打开文件,或者如果没有活动窗口,则会在一个新的空白窗口中打开文件。 doOpen
方法接下来处理要打开的工作区和文件夹。首先,它尝试查找是否已经有打开的窗口,可以打开这些工作区和文件夹。对于每个找到的窗口,它将工作区或文件夹添加到现有窗口中,并将打开的文件打开到该窗口中。否则,它会在一个新的窗口中打开工作区或文件夹,以及要打开的文件。- 最后,
doOpen
方法处理emptyToRestore
和emptyToOpen
,并在需要时打开空白窗口。如果存在要恢复的空窗口,则将它们恢复到它们以前的状态。否则,它将在空白窗口中打开要求的数量。
最后,我们来看一下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);
}
到这里,整个窗口就已经开始加载了,CodeWindow
的load
方法会去打开一个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.js
、loader.js
和bootstrap-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
这个模块,对应的nls
和css
也一起加载,然后调用入口逻辑启动:
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
导入了用户数据存储服务。userDataInit
和extensionsProfileScannerService
注册了单例服务。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();
}
可以看到这里调用了workbench
的open
方法启动程序:
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
给它。 - 初始化事件并调用
workbench
的startup
方法。 - 初始化
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
中。
接下来看一下Workbench
的startup
:
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);
}
可以看到renderWorkbench
把container
加到了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 类。
这几个部件组成了整个workbench
的Layout
。接下来在创建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
、渲染进程完成以及插件加载完成几个点。