3、Electron核心概念:主进程与渲染进程、进程间通信(IPC)、Electron生命周期
好,咱们直接进入正题。这一章是Electron的骨架,搞懂了它,后面写代码就像搭积木一样简单。
很多新手一上来就被「主进程」「渲染进程」搞晕了。我刚开始学的时候也这样,心想:不就是写个桌面应用吗,怎么还分两个进程?
嗯,这里有个关键原因:安全。浏览器不允许JS直接操作文件系统,但桌面应用需要。Electron就把「能碰系统」的部分和「展示界面」的部分拆开了。
3.1 主进程与渲染进程
主进程(Main Process),说白了就是应用的「大脑」。它负责:
- 创建窗口(new BrowserWindow)
- 调用系统API(文件读写、通知、菜单)
- 管理应用生命周期
每个Electron应用有且只有一个主进程。它运行在Node.js环境里,所以你能用require、fs、path这些模块。
渲染进程(Renderer Process),就是「脸面」。每个窗口都是一个独立的渲染进程。它运行在Chromium环境里,负责:
- 展示HTML/CSS/JS页面
- 处理用户交互(点击、输入)
- 调用浏览器API(DOM操作、localStorage)
核心区别一句话:主进程管系统,渲染进程管界面。两者不能直接互相调用。
我在项目中遇到过一个问题:新手在渲染进程里直接写 require('fs'),结果报错。为什么?因为渲染进程默认没有Node.js权限。你需要手动开启 nodeIntegration: true,但我不建议这么做——有安全风险。
3.2 进程间通信(IPC)
既然两个进程不能直接对话,那怎么让它们协作?答案就是IPC(Inter-Process Communication)。
Electron提供了两种IPC方式:
| 方式 | 方向 | 使用场景 |
|---|---|---|
| ipcMain + ipcRenderer | 双向通信 | 渲染进程请求数据,主进程返回结果 |
| contextBridge + preload | 安全桥接 | 暴露有限的API给渲染进程 |
先看最基础的用法:
// 主进程 (main.js)
const { ipcMain } = require('electron')
ipcMain.handle('read-file', async (event, filePath) => {
const fs = require('fs')
return fs.readFileSync(filePath, 'utf-8')
})
// 渲染进程 (renderer.js)
const { ipcRenderer } = require('electron')
async function loadFile() {
const content = await ipcRenderer.invoke('read-file', './data.txt')
console.log(content)
}
你看,渲染进程发一个「请求」,主进程处理完再「回复」。这就是IPC的核心逻辑。
我的习惯:把所有IPC通道名定义在一个常量文件里,比如 IPC_CHANNELS.js。这样两边引用同一个名字,不会写错。
但注意!上面那种写法在最新版Electron里会报安全警告。为什么?因为渲染进程直接用了 require('electron'),这等于把整个Electron API暴露给了网页代码——万一有XSS攻击,后果很严重。
所以,推荐做法是用 preload 脚本 + contextBridge:
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath)
})
// 渲染进程 (renderer.js)
// 现在只能用 window.electronAPI.readFile()
async function loadFile() {
const content = await window.electronAPI.readFile('./data.txt')
console.log(content)
}
我曾经在一个项目里偷懒,直接开了nodeIntegration。结果上线后用户反馈页面被注入了恶意脚本,差点酿成大祸。从那以后,我再也不敢跳过preload了。
3.3 Electron生命周期
一个Electron应用从启动到关闭,会经历几个关键事件。理解这些事件,你才能控制好「什么时候该做什么事」。
核心生命周期事件:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
| ready | Electron初始化完成 | 创建窗口、注册IPC |
| window-all-closed | 所有窗口都关闭了 | macOS上不退出应用,其他平台退出 |
| activate | macOS点击Dock图标 | 重新创建窗口(如果没窗口的话) |
| before-quit | 应用准备退出前 | 保存用户数据、清理资源 |
标准启动流程:
const { app, BrowserWindow } = require('electron')
// 1. 等待ready
app.whenReady().then(() => {
// 2. 创建窗口
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// 3. 加载页面
win.loadFile('index.html')
})
// 4. 处理窗口关闭
app.on('window-all-closed', () => {
// macOS习惯:不退出应用
if (process.platform !== 'darwin') {
app.quit()
}
})
// 5. macOS点击Dock重新激活
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
你想想看,为什么macOS上关闭窗口不退出应用?因为苹果的设计理念是「应用一直运行,窗口只是视图」。Windows用户可能不习惯,但跨平台开发就得尊重各平台的习惯。
避坑指南:我曾经在 ready 事件之前就调用了 BrowserWindow,结果应用直接崩溃。记住:所有窗口操作必须在 ready 之后进行。
还有一个容易被忽略的点:子进程的退出。如果你的应用启动了子进程(比如Python脚本),记得在 before-quit 里杀掉它们,否则会出现「应用关了但进程还在跑」的诡异情况。
好了,这一章的核心就这些。总结一下:
- 主进程管系统,渲染进程管界面
- IPC用 preload + contextBridge 最安全
- 生命周期事件控制着应用的「生老病死」
下一章我们就要动手写第一个Electron应用了。到时候你会发现,这些概念会反复用到。