Skip to content

Electron 主进程与渲染进程深度解析

前言

理解 Electron 的进程模型是掌握 Electron 开发的关键。很多初学者在这里容易混淆,导致代码架构混乱、安全问题频发。

我记得刚开始学 Electron 时,经常搞不清楚哪些代码应该放在主进程,哪些应该放在渲染进程。后来通过不断实践和踩坑,才真正理解了双进程模型的设计理念。

今天,我们就来深入探讨 Electron 的进程模型。

为什么需要双进程?

传统浏览器的多进程架构

Chrome 浏览器采用多进程架构,主要原因:

1. 稳定性 - 一个标签页崩溃不影响其他标签
2. 安全性 - 渲染进程在沙箱中运行,限制权限
3. 性能 - 利用多核CPU,并行处理

Electron 继承并扩展

Chrome 的架构:
浏览器进程 → 多个渲染进程

Electron 的架构:
主进程 (Main) → 多个渲染进程 (Renderer)

 Node.js API
 系统级 API
 生命周期管理

主进程(Main Process)

职责和特点

主进程负责

  • 应用生命周期管理
  • 创建和管理所有窗口
  • 处理系统级别的操作
  • 菜单、托盘、快捷键等原生功能
  • 与渲染进程通信

特点

  • 每个 Electron 应用只有一个主进程
  • 运行 package.jsonmain 字段指定的文件
  • 可以使用所有 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.jsChromium + 受限 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 = DataManager

Preload - 安全桥接

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)
    }
  }
})

总结

✅ 核心要点

  1. 双进程模型

    • 主进程:应用管理、系统操作
    • 渲染进程:UI 渲染、用户交互
  2. 通信方式

    • IPC 是进程间通信的桥梁
    • Preload 脚本是安全的接口层
  3. 安全第一

    • 启用 contextIsolation
    • 禁用 nodeIntegration
    • 限制 API 暴露
  4. 职责分离

    • 主进程处理业务逻辑
    • 渲染进程专注 UI

🎯 最佳实践

  • 模块化组织代码
  • 合理划分职责
  • 安全配置 webPreferences
  • 使用 TypeScript 增强类型安全
  • 做好错误处理

下一篇文章,我们将深入学习 IPC 通信的各种模式和高级用法。


相关文章推荐:

有问题欢迎留言讨论!