Electron 主进程与渲染进程深度解析
前言
理解 Electron 的进程模型是掌握 Electron 开发的关键。很多初学者在这里容易混淆,导致代码架构混乱、安全问题频发。
我记得刚开始学 Electron 时,经常搞不清楚哪些代码应该放在主进程,哪些应该放在渲染进程。后来通过不断实践和踩坑,才真正理解了双进程模型的设计理念。
今天,我们就来深入探讨 Electron 的进程模型。
为什么需要双进程?
传统浏览器的多进程架构
Chrome 浏览器采用多进程架构,主要原因:
1. 稳定性 - 一个标签页崩溃不影响其他标签
2. 安全性 - 渲染进程在沙箱中运行,限制权限
3. 性能 - 利用多核CPU,并行处理Electron 继承并扩展
Chrome 的架构:
浏览器进程 → 多个渲染进程
Electron 的架构:
主进程 (Main) → 多个渲染进程 (Renderer)
↓
Node.js API
系统级 API
生命周期管理主进程(Main Process)
职责和特点
主进程负责:
- 应用生命周期管理
- 创建和管理所有窗口
- 处理系统级别的操作
- 菜单、托盘、快捷键等原生功能
- 与渲染进程通信
特点:
- 每个 Electron 应用只有一个主进程
- 运行
package.json中main字段指定的文件 - 可以使用所有 Node.js API
- 可以使用所有 Electron 主进程 API
主进程的生命周期
javascript
const { app, BrowserWindow } = require('electron')
// 1. 应用启动前(可选)
app.on('will-finish-launching', () => {
console.log('应用即将启动')
// 注册自定义协议等
})
// 2. 应用准备就绪
app.whenReady().then(() => {
console.log('应用已准备就绪')
createWindow()
// macOS 特性
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
// 3. 所有窗口关闭
app.on('window-all-closed', () => {
console.log('所有窗口已关闭')
// macOS 上通常不退出应用
if (process.platform !== 'darwin') {
app.quit()
}
})
// 4. 应用即将退出
app.on('will-quit', (event) => {
console.log('应用即将退出')
// 可以阻止退出
// event.preventDefault()
})
// 5. 应用退出
app.on('quit', (event, exitCode) => {
console.log('应用已退出,退出码:', exitCode)
})主进程常用 API
javascript
const {
app, // 应用生命周期
BrowserWindow, // 窗口管理
ipcMain, // 进程间通信
Menu, // 菜单
Tray, // 系统托盘
dialog, // 对话框
globalShortcut,// 全局快捷键
protocol, // 自定义协议
powerMonitor, // 电源监控
screen // 屏幕信息
} = require('electron')
// 应用信息
console.log('应用名称:', app.getName())
console.log('应用版本:', app.getVersion())
console.log('应用路径:', app.getAppPath())
console.log('用户数据目录:', app.getPath('userData'))
// 屏幕信息
const primaryDisplay = screen.getPrimaryDisplay()
console.log('屏幕尺寸:', primaryDisplay.size)
console.log('工作区尺寸:', primaryDisplay.workAreaSize)
// 系统信息
console.log('平台:', process.platform)
console.log('架构:', process.arch)
console.log('版本:', process.versions)主进程最佳实践
javascript
// ✅ 好的做法:模块化组织
// main/index.js
const { app } = require('electron')
const WindowManager = require('./window-manager')
const MenuManager = require('./menu-manager')
const IpcManager = require('./ipc-manager')
class Application {
constructor() {
this.windowManager = new WindowManager()
this.menuManager = new MenuManager()
this.ipcManager = new IpcManager()
}
init() {
this.setupEventListeners()
}
setupEventListeners() {
app.whenReady().then(() => {
this.windowManager.createMainWindow()
this.menuManager.createMenu()
this.ipcManager.registerHandlers()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
}
}
const application = new Application()
application.init()渲染进程(Renderer Process)
职责和特点
渲染进程负责:
- 渲染网页内容
- 处理用户界面交互
- 执行网页中的 JavaScript 代码
- 通过 IPC 与主进程通信
特点:
- 每个
BrowserWindow实例对应一个渲染进程 - 运行在 Chromium 的渲染引擎中
- 默认情况下无法直接访问 Node.js API(安全考虑)
- 通过 preload 脚本安全地暴露 API
渲染进程的限制
javascript
// ❌ 默认情况下这些代码在渲染进程中无法运行
const fs = require('fs') // Error: require is not defined
const path = require('path') // Error
// ❌ 也无法直接访问 Electron API
const { ipcRenderer } = require('electron') // Error通过 Preload 脚本桥接
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
const fs = require('fs')
const path = require('path')
// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 文件操作
readFile: (filePath) => fs.readFileSync(filePath, 'utf8'),
writeFile: (filePath, content) => fs.writeFileSync(filePath, content),
// IPC 通信
invoke: (channel, data) => ipcRenderer.invoke(channel, data),
send: (channel, data) => ipcRenderer.send(channel, data),
on: (channel, callback) => {
ipcRenderer.on(channel, (event, ...args) => callback(...args))
},
// 路径操作
joinPath: (...paths) => path.join(...paths),
// 应用信息
platform: process.platform,
versions: process.versions
})javascript
// renderer.js
// ✅ 现在可以安全地使用暴露的 API
const content = await window.electronAPI.readFile('config.json')
await window.electronAPI.invoke('save-data', { data: 'test' })渲染进程安全配置
javascript
// main.js
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// ✅ 推荐的安全配置
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 启用上下文隔离
nodeIntegration: false, // 禁用 Node 集成
nodeIntegrationInWorker: false,
nodeIntegrationInSubFrames: false,
sandbox: true, // 启用沙箱
enableRemoteModule: false, // 禁用 remote 模块
// 内容安全策略
webSecurity: true,
allowRunningInsecureContent: false
}
})进程对比
功能对比表
| 特性 | 主进程 | 渲染进程 |
|---|---|---|
| 数量 | 1 个 | 多个(每个窗口一个) |
| 运行环境 | Node.js | Chromium + 受限 Node.js |
| Node.js API | ✅ 完全访问 | ❌ 默认禁用 |
| Electron API | ✅ 主进程 API | ⚠️ 渲染进程 API + preload |
| DOM 操作 | ❌ 不可用 | ✅ 完全支持 |
| 窗口创建 | ✅ 可以 | ❌ 不能 |
| 系统级操作 | ✅ 可以 | ❌ 不能 |
| UI 渲染 | ❌ 不能 | ✅ 负责 |
职责划分
主进程应该做的事:
✅ 创建和管理窗口
✅ 处理应用生命周期
✅ 系统级操作(文件、网络等)
✅ 原生功能(菜单、托盘、通知)
✅ 数据持久化
✅ 与渲染进程通信
渲染进程应该做的事:
✅ 渲染 UI
✅ 处理用户交互
✅ 前端逻辑
✅ 调用主进程 API(通过 IPC)
✅ 页面路由和状态管理
❌ 避免的反模式:
- 在主进程中处理复杂的 UI 逻辑
- 在渲染进程中直接操作文件系统
- 在渲染进程中创建新窗口
- 使用 remote 模块(已废弃)进程通信模式
1. 渲染进程 → 主进程
javascript
// 渲染进程(通过 preload 暴露的 API)
const result = await window.electronAPI.invoke('do-something', { data: 'test' })
// 主进程
ipcMain.handle('do-something', async (event, args) => {
console.log('收到来自渲染进程的请求:', args)
return { success: true }
})2. 主进程 → 渲染进程
javascript
// 主进程
mainWindow.webContents.send('update-data', { newData: 'value' })
// 渲染进程(通过 preload)
window.electronAPI.on('update-data', (data) => {
console.log('收到来自主进程的数据:', data)
})3. 渲染进程 ↔ 渲染进程
javascript
// 通过主进程中转
// 渲染进程 A
window.electronAPI.send('message-to-b', { message: 'Hello B' })
// 主进程
ipcMain.on('message-to-b', (event, data) => {
const windowB = BrowserWindow.fromId(windowBId)
windowB.webContents.send('message-from-a', data)
})
// 渲染进程 B
window.electronAPI.on('message-from-a', (data) => {
console.log('收到来自 A 的消息:', data)
})实战示例:数据管理架构
主进程 - 数据层
javascript
// main/data-manager.js
const fs = require('fs')
const path = require('path')
const { app, ipcMain } = require('electron')
class DataManager {
constructor() {
this.dataPath = path.join(app.getPath('userData'), 'data.json')
this.data = this.loadData()
this.setupIpcHandlers()
}
loadData() {
try {
if (fs.existsSync(this.dataPath)) {
const content = fs.readFileSync(this.dataPath, 'utf8')
return JSON.parse(content)
}
} catch (error) {
console.error('加载数据失败:', error)
}
return {}
}
saveData() {
try {
fs.writeFileSync(
this.dataPath,
JSON.stringify(this.data, null, 2),
'utf8'
)
return true
} catch (error) {
console.error('保存数据失败:', error)
return false
}
}
setupIpcHandlers() {
ipcMain.handle('data:get', (event, key) => {
return this.data[key]
})
ipcMain.handle('data:set', (event, key, value) => {
this.data[key] = value
return this.saveData()
})
ipcMain.handle('data:getAll', () => {
return this.data
})
ipcMain.handle('data:delete', (event, key) => {
delete this.data[key]
return this.saveData()
})
}
}
module.exports = DataManagerPreload - 安全桥接
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('dataAPI', {
get: (key) => ipcRenderer.invoke('data:get', key),
set: (key, value) => ipcRenderer.invoke('data:set', key, value),
getAll: () => ipcRenderer.invoke('data:getAll'),
delete: (key) => ipcRenderer.invoke('data:delete', key)
})渲染进程 - 使用数据
javascript
// renderer.js
class DataService {
async get(key) {
return await window.dataAPI.get(key)
}
async set(key, value) {
return await window.dataAPI.set(key, value)
}
async getAll() {
return await window.dataAPI.getAll()
}
async delete(key) {
return await window.dataAPI.delete(key)
}
}
// 使用
const dataService = new DataService()
async function loadUserData() {
const user = await dataService.get('user')
console.log('用户数据:', user)
}
async function saveUserData(userData) {
const success = await dataService.set('user', userData)
if (success) {
console.log('保存成功')
}
}调试技巧
主进程调试
bash
# 启动调试模式
electron --inspect=5858 .
# 或在 VS Code 中配置 launch.json
{
"type": "node",
"request": "launch",
"name": "Electron: Main",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"runtimeArgs": ["--inspect=5858", "."],
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
}
}渲染进程调试
javascript
// main.js
const mainWindow = new BrowserWindow({...})
// 开发模式打开 DevTools
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools()
}
// 或按 F12日志分离
javascript
// 主进程日志
console.log('[Main]', '这是主进程日志')
// 渲染进程日志
console.log('[Renderer]', '这是渲染进程日志')
// 使用不同的日志库
const mainLog = require('electron-log')
mainLog.info('主进程信息')性能优化
1. 避免阻塞主进程
javascript
// ❌ 不好的做法
ipcMain.handle('heavy-task', (event) => {
// 耗时操作会阻塞主进程
for (let i = 0; i < 1000000000; i++) {
// ...
}
return result
})
// ✅ 好的做法:使用 Worker 或子进程
const { Worker } = require('worker_threads')
ipcMain.handle('heavy-task', async (event) => {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js')
worker.on('message', resolve)
worker.on('error', reject)
})
})2. 渲染进程优化
javascript
// 避免频繁的 IPC 调用
// ❌ 不好
for (let i = 0; i < 1000; i++) {
await window.electronAPI.saveItem(items[i])
}
// ✅ 好的做法:批量处理
await window.electronAPI.saveItems(items)安全最佳实践
1. 始终使用 contextIsolation
javascript
webPreferences: {
contextIsolation: true, // ✅ 必须启用
nodeIntegration: false
}2. 验证 IPC 消息来源
javascript
ipcMain.handle('sensitive-operation', (event, data) => {
// 验证发送者
const senderWindow = BrowserWindow.fromWebContents(event.sender)
if (!senderWindow || senderWindow.id !== mainWindow.id) {
throw new Error('未授权的请求')
}
// 验证数据
if (typeof data !== 'string' || data.length > 1000) {
throw new Error('无效的数据')
}
// 处理请求
})3. 限制 preload 脚本暴露的 API
javascript
// ❌ 危险:暴露整个 fs 模块
contextBridge.exposeInMainWorld('fs', require('fs'))
// ✅ 安全:只暴露必要的功能
contextBridge.exposeInMainWorld('fileAPI', {
readConfig: () => fs.readFileSync(configPath, 'utf8'),
writeConfig: (data) => {
// 验证数据
if (validateConfig(data)) {
fs.writeFileSync(configPath, data)
}
}
})总结
✅ 核心要点
双进程模型
- 主进程:应用管理、系统操作
- 渲染进程:UI 渲染、用户交互
通信方式
- IPC 是进程间通信的桥梁
- Preload 脚本是安全的接口层
安全第一
- 启用 contextIsolation
- 禁用 nodeIntegration
- 限制 API 暴露
职责分离
- 主进程处理业务逻辑
- 渲染进程专注 UI
🎯 最佳实践
- 模块化组织代码
- 合理划分职责
- 安全配置 webPreferences
- 使用 TypeScript 增强类型安全
- 做好错误处理
下一篇文章,我们将深入学习 IPC 通信的各种模式和高级用法。
相关文章推荐:
有问题欢迎留言讨论!