第一个 Electron 应用
本教程共 45 篇 · 第 3 篇 · 更新于 2026-08-03
3. 第一个 Electron 应用
本节目标
- 搭建由 main.js、preload.js、index.html、package.json 组成的最小骨架。
- 理解 app.whenReady 的作用,以及为何窗口要在就绪后创建。
- 用 BrowserWindow 创建窗口,并用 loadFile 载入本地页面。
- 通过 contextBridge 与 ipcMain.handle 打通渲染进程与主进程的通信。
- 成功运行起第一个可显示的桌面窗口。
理论听够了,动手写一个能跑的窗口。本章用最小骨架带你跑通第一个应用:主进程创建窗口、preload 充当桥梁、HTML 负责界面。最终你会看到一个写着 Hello World 的桌面窗口。
3-1 最小骨架由四个文件组成
一个最朴素的 Electron 应用需要这些文件:
package.json:项目入口与脚本配置。main.js:主进程代码,负责创建窗口。preload.js:预加载脚本,连接主进程与渲染进程。index.html:渲染进程加载的页面。
我们先把 package.json 写出来,注意 main 指向 main.js,start 脚本用 electron . 启动。
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "第一个 Electron 应用",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"author": "码上学",
"license": "MIT",
"devDependencies": {
"electron": "^43.2.0"
}
}
3-2 主进程:创建并显示窗口
app 模块管应用生命周期,BrowserWindow 管窗口。窗口只能在 app 就绪之后创建,所以要用 app.whenReady() 等待。
// main.js —— 主进程
const { app, BrowserWindow } = require('electron/main')
const path = require('node:path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
// macOS 上点 dock 图标时,若没有窗口就再建一个
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// 非 macOS 平台,所有窗口关闭就退出应用
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
注意 webPreferences.preload 用的是绝对路径,path.join(__dirname, ...) 能保证无论在哪运行都找得到文件。这一步是安全通信的基础,后面章节会细讲。
3-3 为什么用 app.whenReady 而不是 app.on
新手常写成 app.on('ready', ...)。Electron 官方更推荐 app.whenReady(),因为它返回一个 Promise,避免直接监听 ready 事件的几个微妙陷阱。
// 推荐写法
app.whenReady().then(() => {
createWindow()
})
// 也能工作,但官方不推荐直接这样监听
app.on('ready', () => {
createWindow()
})
当你的初始化逻辑变多,Promise 写法配合 async/await 会更清晰,也不用层层嵌套回调。
3-4 preload:安全的桥梁
preload 在页面加载前运行,处于渲染进程上下文,但能用一部分 Node 能力。它通过 contextBridge 把受控的 API 暴露给页面。
// preload.js —— 预加载脚本
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
getVersion: () => ipcRenderer.invoke('get-app-version')
})
这里我们没有直接把 ipcRenderer 整个塞给页面,而是只暴露一个 getVersion 方法。这种”最小暴露”原则能挡住大部分安全麻烦。
3-5 渲染进程:HTML 界面
渲染进程就是普通网页。下面这个页面会从 window.electronAPI 拿版本号并显示。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello Electron</title>
</head>
<body>
<h1>Hello World!</h1>
<p>当前 Electron 版本:<span id="version">加载中…</span></p>
<script src="./renderer.js"></script>
</body>
</html>
// renderer.js —— 渲染进程
const versionEl = document.getElementById('version')
window.electronAPI.getVersion().then((v) => {
versionEl.textContent = v
})
页面不能直接 require('electron'),必须通过 preload 暴露的 electronAPI 来拿数据。这是 Electron 安全模型的硬要求。
3-6 主进程响应版本请求
preload 里的 ipcRenderer.invoke 需要主进程用 ipcMain.handle 接住。补上这段,版本号才能回来。
// main.js —— 追加在主进程
const { ipcMain } = require('electron/main')
ipcMain.handle('get-app-version', () => {
return app.getVersion()
})
把这几段拼起来,运行 npm start,就能看到一个显示版本号的窗口。主进程、preload、渲染进程三者如何配合,到这里已经完整跑通了一遍。
3-7 运行起来并排查常见问题
在项目根目录执行:
npm start
如果窗口没出现,先确认三件事:package.json 的 main 写对了、electron 已装进 node_modules、index.html 和 preload.js 路径正确。终端里若有报错,Electron 会原样打印,按行号定位即可。
提示:首次启动会下载 Electron 二进制,可能稍慢。若卡在下载,可配置镜像源或参考官方安装文档排查网络问题。
3-8 目录结构与文件摆放
一个最小项目的磁盘布局应当清晰。所有源文件平铺在根目录即可,打包时会由工具统一处理。
my-electron-app/
├── package.json
├── main.js
├── preload.js
├── index.html
└── renderer.js
preload.js 和 index.html 的路径在 BrowserWindow 构造时通过 __dirname 拼接,因此只要和 main.js 同目录,移动到别处也不会失效。renderer.js 由 index.html 里的 <script> 标签引入,属于渲染进程代码。
3-9 用 DevTools 调试渲染进程
窗口跑起来后,想看渲染进程的日志和元素,打开 Chromium 开发者工具即可。主进程里调用 webContents.openDevTools() 能主动弹出。
// 主进程:在 createWindow 内部,建好 win 之后调用
win.webContents.openDevTools()
更常见的做法是直接在窗口里按 F12 或 Ctrl+Shift+I,和浏览器里一模一样。主进程的调试则要靠 VS Code 或 --inspect 端口,留到调试章节再展开。
3-10 常见误区
最容易踩的坑是把创建窗口的代码写在 app.whenReady() 之外,结果窗口建不出来。记住窗口只能在就绪之后创建。
另一个高频错误是在 index.html 里直接 require('electron')。浏览器环境根本没有这个函数,会直接报错。所有 Node 能力都必须经 preload 暴露。
还有人图省事把 contextIsolation 关掉。这会让页面拿到 preload 的全部特权,属于高危写法,绝不要为了调试方便破坏安全默认。
3-11 从零到运行的完整回顾
把前面的步骤串一遍,完整流程是这样。先建文件夹、npm init 生成 package.json,再把 main 指到 main.js、start 设为 electron .。
接着装 Electron 到开发依赖。然后写 main.js:引入 app 与 BrowserWindow,在 whenReady 里建窗口并 loadFile。再写 preload.js 用 contextBridge 暴露接口,index.html 与 renderer.js 负责界面与交互。
最后 npm start,窗口弹出,版本号显示出来,整个链路就通了。这一步走通,后面所有章节都是在这套骨架上添砖加瓦。
3-12 加一点交互让应用更实在
光显示版本号有点干涩,给页面加个按钮,点一下让主进程弹个系统对话框,能更直观体会 IPC 的来回。注意 exposeInMainWorld 对同一个键名只能调一次,重复调用会直接抛错,所以要把新方法并进原来那个对象里:
// preload.js —— 预加载脚本:所有接口挂在同一个键下
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
getVersion: () => ipcRenderer.invoke('get-app-version'),
openDialog: () => ipcRenderer.invoke('open-dialog')
})
主进程里用 dialog 模块响应,弹出一个信息框:
// main.js —— 主进程响应
const { dialog } = require('electron/main')
ipcMain.handle('open-dialog', () => {
dialog.showMessageBox({ message: '你好,来自主进程的问候!' })
})
渲染进程按钮点下去,调用 window.electronAPI.openDialog(),对话框就出现了。这条链路展示了”页面发请求、主进程干特权活”的完整闭环,也是后续所有 IPC 章节的基础范式。
3-13 关于热重载的小提醒
开发时改一行就要手动重启,效率很低。社区有 electron-reload 之类的工具,能在文件变动时自动重启窗口或刷新页面。它只在开发期用,别打进生产依赖。
npm install electron-reload --save-dev
// main.js 顶部,仅开发时引入
if (process.env.NODE_ENV === 'development') {
require('electron-reload')(__dirname)
}
这类工具本质是监听文件变化再触发 reload,理解了原理你自己也能用 fs.watch 写个简陋版。等后面接上 Vite、webpack 的 HMR,体验会更顺滑。现阶段先把这个最小应用跑稳最重要。
3-14 本章小结
最小应用的骨架是四个文件:配置、主进程、preload、页面。主进程用 app.whenReady 后创建 BrowserWindow,通过 loadFile 载入本地 HTML。
页面拿不到 Node 能力,必须经 preload 的 contextBridge 暴露接口,再由主进程 ipcMain.handle 响应。下一章我们专门拆开这套多进程模型。