进程模型
Electron 的进程模型是其核心架构,理解进程模型对于开发高质量的 Electron 应用至关重要。本节将深入讲解主进程、渲染进程和预加载脚本的工作原理。
多进程架构
Electron 继承了 Chromium 的多进程架构。这种设计带来了更好的稳定性和安全性。
为什么需要多进程
早期的浏览器采用单进程架构,所有功能都在一个进程中运行。这种设计存在严重问题:
- 稳定性差:一个网页崩溃会导致整个浏览器崩溃
- 安全性低:恶意网页可以访问其他网页的数据
- 性能问题:一个卡死的网页会影响整个浏览器
现代浏览器采用多进程架构,每个标签页运行在独立的渲染进程中。Electron 继承了这种设计。
进程架构图
┌─────────────────────────────────────────────────────────┐
│ Electron 应用 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 主进程 │ │ 渲染进程1 │ │ 渲染进程2 │ │
│ │ (Main) │ │ (Renderer) │ │ (Renderer) │ │
│ │ │ │ │ │ │ │
│ │ - Node.js │ │ - Web 环境 │ │ - Web 环境 │ │
│ │ - 原生 API │ │ - HTML/CSS │ │ - HTML/CSS │ │
│ │ - 窗口管理 │ │ - JavaScript│ │ - JavaScript│ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ IPC 通信通道 │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────┘
主进程
主进程是 Electron 应用的核心,每个应用有且只有一个主进程。
主进程的特点
- 唯一性:一个应用只有一个主进程
- Node.js 环境:可以访问完整的 Node.js API
- 原生能力:可以调用 Electron 的原生 API
- 生命周期管理:控制应用的启动和退出
主进程的职责
创建和管理窗口
const { app, BrowserWindow } = require('electron')
const path = require('node:path')
let mainWindow
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
mainWindow.loadFile('index.html')
mainWindow.on('closed', () => {
mainWindow = null
})
}
管理应用生命周期
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
处理 IPC 通信
const { ipcMain } = require('electron')
ipcMain.on('message-from-renderer', (event, arg) => {
console.log('收到渲染进程消息:', arg)
event.reply('message-to-renderer', '主进程已收到')
})
ipcMain.handle('async-operation', async (event, data) => {
const result = await doSomethingAsync(data)
return result
})
app 模块常用事件
| 事件 | 说明 |
|---|---|
ready | 应用初始化完成 |
window-all-closed | 所有窗口关闭 |
before-quit | 应用即将退出 |
will-quit | 应用将要退出 |
quit | 应用已退出 |
activate | 应用被激活 |
app 模块常用方法
app.quit()
app.exit(0)
app.relaunch()
app.getPath('userData')
app.getVersion()
app.getName()
app.whenReady()
app.isReady()
渲染进程
渲染进程负责渲染网页内容,每个 BrowserWindow 实例对应一个独立的渲染进程。
渲染进程的特点
- Web 标准环境:可以使用 HTML、CSS、JavaScript
- 默认无 Node.js:出于安全考虑,默认不能直接使用 Node.js
- 隔离性:不同窗口的渲染进程相互隔离
- 沙箱化:默认运行在沙箱环境中
渲染进程的限制
默认情况下,渲染进程不能:
- 使用
require加载 Node.js 模块 - 访问文件系统
- 执行系统命令
- 访问原生模块
这些限制是为了安全考虑。如果需要这些能力,应该通过预加载脚本安全地暴露。
启用 Node.js 集成(不推荐)
虽然可以启用 Node.js 集成,但这会带来安全风险:
const mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
这种方式允许渲染进程直接使用 Node.js,但不推荐在生产环境使用,特别是加载远程内容时。
预加载脚本
预加载脚本是在渲染进程加载网页内容之前执行的脚本,它是连接主进程和渲染进程的安全桥梁。
预加载脚本的作用
- 安全暴露 API:有选择地暴露 Node.js 能力给渲染进程
- 上下文隔离:在隔离的环境中运行,不影响页面全局对象
- 桥接通信:提供 IPC 通信的封装接口
基本用法
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => {
const validChannels = ['toMain']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
const validChannels = ['fromMain']
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
contextBridge API
contextBridge.exposeInMainWorld(apiKey, api) 方法将 API 暴露给渲染进程:
apiKey:暴露到window对象上的键名api:暴露的对象或方法
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
doSomething: () => console.log('doing something'),
data: {
name: 'Electron',
version: '28.0.0'
}
})
在渲染进程中使用:
console.log(window.myAPI.data.name)
window.myAPI.doSomething()
安全最佳实践
白名单验证
只允许特定的 IPC 通道:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => {
const validChannels = ['update-title', 'save-file', 'open-file']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
}
})
不要暴露整个 ipcRenderer
错误做法:
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: ipcRenderer
})
正确做法:
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title),
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
进程间通信
主进程和渲染进程通过 IPC(Inter-Process Communication)进行通信。
ipcMain 和 ipcRenderer
ipcMain:主进程使用,监听和发送消息ipcRenderer:渲染进程使用,通过预加载脚本暴露
通信模式
渲染进程 → 主进程(单向)
// 预加载脚本
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
// 主进程
ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})
// 渲染进程
window.electronAPI.setTitle('新标题')
渲染进程 → 主进程(双向)
// 预加载脚本
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
// 主进程
ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (!canceled) {
return filePaths[0]
}
})
// 渲染进程
const filePath = await window.electronAPI.openFile()
主进程 → 渲染进程
// 主进程
mainWindow.webContents.send('update-counter', 1)
// 预加载脚本
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (event, value) => callback(value))
})
// 渲染进程
window.electronAPI.onUpdateCounter((value) => {
console.log('计数器更新:', value)
})
Utility Process
Electron 还提供了 Utility Process,用于运行 CPU 密集型任务。
创建 Utility Process
const { UtilityProcess } = require('electron')
const path = require('node:path')
const utilityProcess = new UtilityProcess(path.join(__dirname, 'utility.js'))
utilityProcess.on('exit', (code) => {
console.log(`Utility process exited with code ${code}`)
})
utilityProcess.postMessage({ type: 'start', data: 'some data' })
utilityProcess.on('message', (message) => {
console.log('收到消息:', message)
})
Utility Process 的优势
- 独立进程:不影响主进程和渲染进程
- Node.js 环境:可以访问完整的 Node.js API
- 通信能力:可以与渲染进程直接通信
进程调试
调试主进程
在 VS Code 中配置调试:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"args": ["."],
"outputCapture": "std"
}
]
}
调试渲染进程
渲染进程可以使用 Chrome DevTools 调试:
mainWindow.webContents.openDevTools()
查看进程信息
const { app } = require('electron')
console.log('应用名称:', app.getName())
console.log('应用版本:', app.getVersion())
console.log('Electron 版本:', process.versions.electron)
console.log('Node 版本:', process.versions.node)
console.log('Chrome 版本:', process.versions.chrome)
总结
理解 Electron 的进程模型是开发高质量应用的基础:
- 主进程:负责窗口管理和原生 API 调用,运行在 Node.js 环境
- 渲染进程:负责渲染网页内容,运行在 Web 环境
- 预加载脚本:安全地连接主进程和渲染进程
- IPC 通信:进程间通过消息传递进行通信
正确使用进程模型,可以让你的应用更加安全、稳定和高效。