Skip to content
每个 Electron 窗口都由 BrowserWindow 类创建和管理。屏幕上看到的那个窗口,本质上是主进程构造出的一个 BrowserWindow 实例,该实例内部持有一个 webContents 对象,负责加载和渲染 HTML 页面。窗口的创建、配置、页面加载和生命周期管理都围绕这个类展开。
创建基本窗口
实例化 BrowserWindow
BrowserWindow 的构造函数签名如下:
ts
new BrowserWindow(options?: BrowserWindowConstructorOptions)options 参数是一个可选对象,几乎所有原生窗口属性都可以通过它来设置——尺寸、位置、标题、是否带边框、是否透明等等。如果不传任何参数,Electron 会使用一套默认值:窗口尺寸为 800×600,带有系统标题栏,背景为白色。
在主进程里创建一个最基础的窗口,代码通常这样写:
ts
import { BrowserWindow } from 'electron';
const win = new BrowserWindow({ width: 800, height: 600 });此时窗口对象已经创建,但还没有加载任何页面。如果不继续调用 loadFile 或 loadURL,窗口会显示 webContents 的初始空白页。
最小示例:加载本地 HTML
下面是一个可运行的示例,它完成两步:创建窗口、加载本地 HTML 文件。
ts
import { app, BrowserWindow } from 'electron';
import path from 'path';
function createWindow() {
const win = new BrowserWindow({ width: 800, height: 600 });
win.loadFile(path.join(__dirname, 'index.html'));
}
app.whenReady().then(createWindow);loadFile 接收一个文件系统路径,将本地 HTML 载入窗口。路径建议使用 path.join 拼出绝对路径——相对路径在某些打包场景下会发生解析偏差。示例中 __dirname 指向主进程脚本所在目录,假定 index.html 也位于同一目录。
运行这段代码后,窗口出现并渲染 index.html。不过窗口创建后会立即显示,而页面可能尚未完成首屏渲染,因此用户会在极短时间内看到白屏。这个问题后面有专门的解决方案。
窗口配置参数详解
BrowserWindow 的配置项非常多,这里只聚焦于直接决定窗口外观和行为的核心参数。
尺寸与位置
width/height:窗口内容区的宽度和高度(单位 px),不含系统标题栏和边框。x/y:窗口距屏幕左上角的偏移量。如果不指定,Electron 会让操作系统自动居中放置。minWidth/minHeight/maxWidth/maxHeight:为窗口尺寸设置上限和下限。达到边界时,拖拽缩放会被操作系统阻止。resizable(默认为true):是否允许用户手动调整窗口大小。设为false时,最大化按钮也会被禁用(在 Windows 上表现如此,macOS 行为会稍有差异)。
ts
new BrowserWindow({
width: 1024,
height: 768,
minWidth: 800,
minHeight: 600,
x: 100,
y: 50,
resizable: true,
});外观与行为
title:窗口标题栏上的文字。如果未设置,Electron 会使用加载页面中的<title>标签内容自动填充。frame(默认为true):窗口是否带系统原生边框和标题栏。设为false后会得到一个“无边框窗口”,需要开发者自己用 HTML/CSS 绘制标题栏和控制按钮。transparent(默认为false):窗口背景是否透明。只有在frame: false时才能真正生效,否则系统标题栏仍然不透明。透明窗口常用于桌面小组件、圆形悬浮窗等场景。backgroundColor:窗口的背景色(十六进制字符串,如'#000000'或'#00000000')。这个颜色会在页面尚未渲染前短暂出现,可以用来缓解白屏闪烁。
一个无边框透明窗口的配置示例:
ts
const win = new BrowserWindow({
width: 300,
height: 300,
frame: false,
transparent: true,
backgroundColor: '#00000000',
});透明窗口对性能有一定影响,尤其在低端硬件上,操作系统合成渲染的代价会升高。
loadFile 与 loadURL
窗口创建出来后,需要加载页面才能显示内容。BrowserWindow 实例提供了两个主要方法。
loadFile 加载本地页面
win.loadFile(filePath, options?) 从文件系统加载本地 HTML 文件。
filePath 必须是一个指向本地 HTML 文件的路径字符串。如果是相对路径,会根据当前工作目录解析,因此推荐总是转换为绝对路径。options 中常用的是 query 和 hash,可以模拟 URL 查询参数和片段标识符:
ts
win.loadFile(path.join(__dirname, 'index.html'), {
query: { debug: '1' },
hash: 'section2',
});Electron 内部会把文件路径转换成 file:// 协议的 URL,然后通过 webContents 进行导航加载。query 对象会被序列化为 URL 查询串,hash 会拼接到 URL 末尾。最终加载的地址类似:file:///path/to/index.html?debug=1#section2。
loadFile 调用会触发一次页面导航,之前页面的 unload 等事件会正常执行。该方法返回一个 Promise<void>,在页面导航完成后 resolve。
loadURL 加载远程页面
win.loadURL(url, options?) 从远程地址加载页面,url 可以是 http:// 或 https:// 开头的远程地址,也可以是 file:// 协议地址。
ts
win.loadURL('https://example.com', {
userAgent: 'MyElectronApp/1.0',
});options 中常用的属性包括 userAgent(覆盖 User-Agent)、httpReferrer、postBody 等,这些和浏览器中的 fetch/导航行为一致。
加载远程页面时,安全边界需要格外注意:默认情况下,渲染进程不具备直接访问 Node.js 的能力(nodeIntegration 默认关闭,contextIsolation 默认开启),但加载不受信任的外部内容仍然可能引入 XSS 或钓鱼风险。加载远程 URL 的应用应当谨慎规划好 Content Security Policy(CSP)和 URL 白名单机制。
窗口显示优化:ready-to-show
前面提到,窗口创建后立即显示会导致短暂白屏,原因是窗口先于内容渲染完成就显示了。
解决方案是:在创建窗口时设置 show: false,让窗口创建后保持隐藏;然后监听 ready-to-show 事件,待页面首次渲染完毕后再显示窗口。
ts
const win = new BrowserWindow({
width: 800,
height: 600,
show: false,
});
win.loadFile('index.html');
win.once('ready-to-show', () => {
win.show();
});ready-to-show 事件在以下两个条件同时满足时触发:
- 页面已完成首次绘制(像素已经被渲染到内存缓冲区);
- 窗口尚未显示(即
show: false加上尚未调用show())。
如果窗口已经在屏幕上可见,这个事件不会触发。因此它很适合用于避免白屏闪烁。如果需要加载多个窗口,给每个窗口都加上这个处理,启动体验会干净得多。
另外,如果在 ready-to-show 触发前就调用了 win.show(),窗口会立即显示,也就达不到优化目的。需要确保事件监听注册在调用 show() 之前——once 在这里比 on 更合适,因为只需要处理一次。
窗口关闭与内存管理
窗口被用户关闭(例如点下关闭按钮)时,默认行为是销毁窗口。但如果代码中仍然保留着指向该窗口实例的引用,就会阻止垃圾回收器回收这整棵对象树(包括 webContents)。正确的做法是监听 closed 事件,并在事件处理中将引用置空。
为了让这一操作可行,窗口变量应当使用 let 声明而不是 const:
ts
let win = new BrowserWindow({ /* ... */ });
win.on('closed', () => {
// 其他必要的清理逻辑
win = null;
});如果同时管理多个窗口,可能使用数组或 Map 来维护引用,此时同样需要在 closed 事件中移除对应条目。
一个更完整的清理套路是在置空前先移除实例上所有尚存的事件监听器:
ts
win.on('closed', () => {
win.removeAllListeners();
win = null;
});removeAllListeners() 会移除窗口实例上所有注册的监听器(包括自己注册的以及 webContents 上的某些残留),进一步切断引用,帮助 GC 回收。这并不是 Electron 官方强制要求的,但对于长时间运行的应用,尤其是频繁打开关闭窗口的场景,这种清理能够有效避免主进程内存缓慢上涨。
有一个容易被忽略的点:如果在窗口外部(比如主进程的某个全局模块中)保存了窗口所关联的 webContents 引用,那么即使释放了窗口实例,只要 webContents 引用还在,相关资源也不会被完全回收。因此管理窗口生命周期时必须把 webContents 的引用也一并考虑在内。
注意点
- Electron 的内建类不允许在用户代码中子类化(
class MyWindow extends BrowserWindow这种写法不行)。如果想封装窗口创建逻辑,应当使用组合而非继承,比如用一个工厂函数返回配置好的BrowserWindow实例。 loadFile和loadURL都会触发完整的页面导航周期,包括did-start-loading、did-stop-loading、did-finish-load等事件。如果需要在页面加载过程中插入逻辑(比如显示 loading 状态),就可以通过这些事件来实现。- 窗口的最小/最大尺寸约束仅对用户拖拽缩放有效,程序中通过
win.setSize()设置尺寸可以突破这些限制。同样,setBounds可以直接绕过约束设置位置和尺寸。 - Windows 与 macOS 在无边框窗口行为上有差异。例如,在 macOS 上无边框窗口仍然保留某些系统手势(如拖动标题栏区域),但自定义标题栏需要自行处理拖动逻辑;而 Windows 上无边框窗口默认无法通过拖动窗口区移动,需要开发者通过
-webkit-app-region: drag等 CSS 技巧或原生事件处理。 transparent: true在 Linux 上支持不完全,依赖于窗口管理器和合成器的实现,某些环境下透明可能失效或出现渲染瑕疵。
