跳到主要内容

进程模型

Electron 的进程模型是其核心架构,理解进程模型对于开发高质量的 Electron 应用至关重要。本节将深入讲解主进程、渲染进程和预加载脚本的工作原理。

多进程架构

Electron 继承了 Chromium 的多进程架构。这种设计带来了更好的稳定性和安全性。

为什么需要多进程

早期的浏览器采用单进程架构,所有功能都在一个进程中运行。这种设计存在严重问题:

  1. 稳定性差:一个网页崩溃会导致整个浏览器崩溃
  2. 安全性低:恶意网页可以访问其他网页的数据
  3. 性能问题:一个卡死的网页会影响整个浏览器

现代浏览器采用多进程架构,每个标签页运行在独立的渲染进程中。Electron 继承了这种设计。

进程架构图

┌─────────────────────────────────────────────────────────┐
│ Electron 应用 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 主进程 │ │ 渲染进程1 │ │ 渲染进程2 │ │
│ │ (Main) │ │ (Renderer) │ │ (Renderer) │ │
│ │ │ │ │ │ │ │
│ │ - Node.js │ │ - Web 环境 │ │ - Web 环境 │ │
│ │ - 原生 API │ │ - HTML/CSS │ │ - HTML/CSS │ │
│ │ - 窗口管理 │ │ - JavaScript│ │ - JavaScript│ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ IPC 通信通道 │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────┘

主进程

主进程是 Electron 应用的核心,每个应用有且只有一个主进程。

主进程的特点

  1. 唯一性:一个应用只有一个主进程
  2. Node.js 环境:可以访问完整的 Node.js API
  3. 原生能力:可以调用 Electron 的原生 API
  4. 生命周期管理:控制应用的启动和退出

主进程的职责

创建和管理窗口

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 实例对应一个独立的渲染进程。

渲染进程的特点

  1. Web 标准环境:可以使用 HTML、CSS、JavaScript
  2. 默认无 Node.js:出于安全考虑,默认不能直接使用 Node.js
  3. 隔离性:不同窗口的渲染进程相互隔离
  4. 沙箱化:默认运行在沙箱环境中

渲染进程的限制

默认情况下,渲染进程不能:

  • 使用 require 加载 Node.js 模块
  • 访问文件系统
  • 执行系统命令
  • 访问原生模块

这些限制是为了安全考虑。如果需要这些能力,应该通过预加载脚本安全地暴露。

启用 Node.js 集成(不推荐)

虽然可以启用 Node.js 集成,但这会带来安全风险:

const mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})

这种方式允许渲染进程直接使用 Node.js,但不推荐在生产环境使用,特别是加载远程内容时。

预加载脚本

预加载脚本是在渲染进程加载网页内容之前执行的脚本,它是连接主进程和渲染进程的安全桥梁。

预加载脚本的作用

  1. 安全暴露 API:有选择地暴露 Node.js 能力给渲染进程
  2. 上下文隔离:在隔离的环境中运行,不影响页面全局对象
  3. 桥接通信:提供 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 的优势

  1. 独立进程:不影响主进程和渲染进程
  2. Node.js 环境:可以访问完整的 Node.js API
  3. 通信能力:可以与渲染进程直接通信

进程调试

调试主进程

在 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 的进程模型是开发高质量应用的基础:

  1. 主进程:负责窗口管理和原生 API 调用,运行在 Node.js 环境
  2. 渲染进程:负责渲染网页内容,运行在 Web 环境
  3. 预加载脚本:安全地连接主进程和渲染进程
  4. IPC 通信:进程间通过消息传递进行通信

正确使用进程模型,可以让你的应用更加安全、稳定和高效。