6月5日 19:58

Electron 主进程 vs 渲染进程:职责、API 和通信机制全对比

Electron 应用跑起来后,打开任务管理器你会看到好几个进程——一个主进程加上若干渲染进程。它们各司其职,也各有局限,搞不清谁负责什么,写出的代码就会出各种诡异 bug。

一句话区分

  • 主进程:管窗口、管系统、管生命周期,是整个应用的"大管家"
  • 渲染进程:管页面、管 UI、管用户交互,是每个窗口的"画师"

主进程只有一个,渲染进程可以有多个(每个 BrowserWindow 一个)。

运行环境对比

主进程渲染进程
运行环境Node.jsChromium(浏览器)
可用 API全部 Node.js API + Electron 主进程 APIWeb API + 部分通过 preload 暴露的 Node.js API
进程数量1 个每个窗口 1 个,相互隔离
职责窗口管理、系统交互、原生菜单/对话框渲染 UI、处理用户交互
能直接操作 DOM 吗不能
能直接读文件吗不能(需要通过 IPC 请求主进程)
默认能访问 Node.js 吗不能(contextIsolation 默认开启)

这个表基本说明了一切:主进程有系统权限但不会画界面,渲染进程会画界面但没系统权限。两者必须协作。

主进程做什么

主进程是 Electron 应用的入口,package.jsonmain 字段指向的那个文件就是主进程代码:

javascript
// main.js - 主进程 const { app, BrowserWindow, ipcMain, dialog } = require('electron') let mainWindow app.whenReady().then(() => { mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false // 安全:不让渲染进程直接用 Node.js } }) mainWindow.loadFile('index.html') }) // 处理系统级操作 ipcMain.handle('open-file-dialog', async () => { const result = await dialog.showOpenDialog({ properties: ['openFile'] }) return result.filePaths[0] })

主进程的核心职责:

  • 创建和管理窗口new BrowserWindow() 只能在主进程调用
  • 控制应用生命周期app.whenReady()app.quit() 等事件
  • 系统级交互:文件对话框、系统菜单、托盘图标、通知
  • IPC 中转:处理渲染进程发来的请求,或协调多个窗口间的通信

渲染进程做什么

每个 BrowserWindow 加载的网页就运行在一个独立的渲染进程里:

javascript
// renderer.js - 渲染进程(通过 preload 暴露的 API 与主进程通信) const filePath = await window.electronAPI.openFileDialog() document.getElementById('file-path').textContent = filePath

渲染进程的核心职责:

  • 渲染 UI:HTML + CSS + JavaScript,和写网页一模一样
  • 处理用户交互:按钮点击、表单提交、滚动等
  • 数据展示:从主进程获取数据后渲染到界面

渲染进程之间是相互隔离的——窗口 A 的渲染进程不能直接访问窗口 B 的 DOM 或变量,必须通过主进程中转。

preload 脚本:两个世界之间的桥梁

渲染进程默认无法访问 Node.js API(这是安全设计),但很多场景又确实需要调用系统功能。preload 脚本就是解决这个问题的:

javascript
// preload.js - 运行在渲染进程的上下文中,但有 Node.js 访问权限 const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { openFileDialog: () => ipcRenderer.invoke('open-file-dialog'), readFile: (path) => ipcRenderer.invoke('read-file', path), onSave: (callback) => { ipcRenderer.on('trigger-save', (event, data) => callback(data)) } })

preload 的执行时机很特殊:它在渲染进程的网页加载之前运行,既可以用 Node.js API,又可以访问渲染进程的 window 对象。contextBridge.exposeInMainWorld 把方法安全地挂到 window 上,让网页代码能调用。

常见的坑

在主进程里操作 DOM

不行。主进程没有 DOM 环境。如果你想修改 UI,必须在渲染进程里操作,或者通过 IPC 通知渲染进程去改。

在渲染进程里直接 require('fs')

默认不行。nodeIntegration 默认关闭,contextIsolation 默认开启——这是 Electron 的安全最佳实践。需要读文件时,通过 preload 暴露方法,让主进程来读。

多窗口间的全局状态

渲染进程之间不共享内存。如果你需要一个全局状态(比如用户登录信息),有三种方案:

  1. 主进程做状态中心:所有窗口通过 IPC 读写主进程中的变量
  2. LocalStorage/IndexDB:同一 origin 下共享,但只适合简单场景
  3. 共享内存/文件:性能要求高时使用

渲染进程崩溃不影响主进程

这是一个设计优势——某个网页崩溃了,主进程可以检测到并重新创建窗口,整个应用不会挂掉:

javascript
mainWindow.webContents.on('crashed', () => { mainWindow.destroy() mainWindow = new BrowserWindow({ ... }) mainWindow.loadFile('index.html') })

进程架构图

shell
┌─────────────────────────────────┐ │ 主进程 (1)│ app / BrowserWindow / ipcMain │ │ 文件系统 / 系统对话框 / 原生菜单 │ └──────────┬──────────────────────┘ │ IPC 通信 ┌──────┴──────┬──────────────┐ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌────────┐ │渲染进程1│ │渲染进程2│ │渲染进程3│ │窗口 A │ │窗口 B │ │窗口 C │ │DOM/JS │ │DOM/JS │ │DOM/JS │ └────────┘ └────────┘ └────────┘

理解了这个架构,就明白了 Electron 开发的核心原则:系统操作在主进程,UI 操作在渲染进程,两者通过 IPC 通信

标签:Electron