第 2 章:Electron 核心概念与进程模型

理解 Electron 的进程模型是掌握桌面开发的关键。本章将深入讲解主进程与渲染进程的分工、进程间通信(IPC)机制,以及 Electron 的安全架构。

2.1 主进程 vs 渲染进程

Electron 采用多进程架构,这是它与传统 Web 开发最大的不同。让我们用一个类比来理解:

类比理解

把 Electron 应用想象成一个餐厅:

  • 主进程(Main Process) = 餐厅经理(Node.js 服务器)
    负责管理整个餐厅、协调各部门、处理外部事务
  • 渲染进程(Renderer Process) = 各个餐桌的服务员(浏览器标签页)
    每个窗口一个进程,负责直接为顾客(用户)服务

主进程(Main Process)

渲染进程(Renderer Process)

特性 主进程 (Main) 渲染进程 (Renderer)
数量 每个应用 1 个 每个窗口 1 个
入口 package.json 中 main 字段 BrowserWindow.loadFile() 加载的 HTML
Node.js API ✅ 完整访问 ❌ 默认关闭(可通过 nodeIntegration 开启,但不推荐)
DOM/BOM ❌ 无 ✅ 完整访问
系统 API ✅ 完整访问 ❌ 需通过 IPC 间接调用
用途 应用生命周期、窗口管理、系统交互 UI 渲染、用户交互、前端逻辑

2.2 进程间通信(IPC)

既然主进程和渲染进程是隔离的,它们如何协作?答案是 IPC(Inter-Process Communication,进程间通信)

类比理解

IPC 就像餐厅里服务员用对讲机呼叫经理。渲染进程(服务员)需要调用系统功能(如保存文件)时,通过 IPC 发送消息给主进程(经理),主进程处理完后把结果传回。

IPC 通信模式

模式 方向 特点 适用场景
ipcMain.on / ipcRenderer.send 渲染 → 主 单向发送,无返回值 通知主进程做某事
ipcMain.handle / ipcRenderer.invoke 渲染 ↔ 主 双向,Promise 返回值 需要返回数据的请求
webContents.send 主 → 渲染 主进程主动推送 广播消息、状态更新

完整 IPC 示例

主进程(main.js)

main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('path')
const fs = require('fs')

let mainWindow

function createWindow () {
  mainWindow = new BrowserWindow({
    width: 900,
    height: 700,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      // 安全设置:关闭 nodeIntegration,启用 contextIsolation
      nodeIntegration: false,
      contextIsolation: true
    }
  })

  mainWindow.loadFile('index.html')
}

// ========== IPC 处理 ==========

// 模式1:单向接收(渲染 → 主)
ipcMain.on('app:quit', () => {
  console.log('收到退出指令')
  app.quit()
})

// 模式2:双向请求响应(渲染 ↔ 主)
ipcMain.handle('file:read', async (event, filePath) => {
  try {
    const content = fs.readFileSync(filePath, 'utf-8')
    return { success: true, content }
  } catch (err) {
    return { success: false, error: err.message }
  }
})

// 模式2:打开文件对话框
ipcMain.handle('dialog:openFile', async () => {
  const result = await dialog.showOpenDialog(mainWindow, {
    properties: ['openFile'],
    filters: [
      { name: '文本文件', extensions: ['txt', 'md'] },
      { name: '所有文件', extensions: ['*'] }
    ]
  })
  return result
})

app.whenReady().then(createWindow)

预加载脚本(preload.js)- 安全桥梁

preload.js
const { contextBridge, ipcRenderer } = require('electron')

// 通过 contextBridge 安全地暴露 API 给渲染进程
// 渲染进程只能通过 window.electronAPI 访问这些暴露的方法
contextBridge.exposeInMainWorld('electronAPI', {
  // 模式1:单向发送
  quitApp: () => ipcRenderer.send('app:quit'),
  
  // 模式2:双向调用(返回 Promise)
  readFile: (filePath) => ipcRenderer.invoke('file:read', filePath),
  openFileDialog: () => ipcRenderer.invoke('dialog:openFile'),
  
  // 模式3:接收主进程推送(主 → 渲染)
  onMessageFromMain: (callback) => {
    ipcRenderer.on('main:message', (event, data) => callback(data))
  }
})

渲染进程(renderer.js)

renderer.js
// 渲染进程中通过 window.electronAPI 调用暴露的方法

// 模式1:单向发送 - 退出应用
document.getElementById('quit-btn').addEventListener('click', () => {
  window.electronAPI.quitApp()
})

// 模式2:双向调用 - 读取文件
async function loadFile() {
  try {
    // 先打开对话框选择文件
    const dialogResult = await window.electronAPI.openFileDialog()
    
    if (!dialogResult.canceled && dialogResult.filePaths.length > 0) {
      const filePath = dialogResult.filePaths[0]
      
      // 读取文件内容
      const result = await window.electronAPI.readFile(filePath)
      
      if (result.success) {
        document.getElementById('content').textContent = result.content
      } else {
        alert('读取失败:' + result.error)
      }
    }
  } catch (err) {
    console.error('调用失败:', err)
  }
}

// 模式3:接收主进程消息
window.electronAPI.onMessageFromMain((data) => {
  console.log('收到主进程消息:', data)
  document.getElementById('status').textContent = data
})

// 绑定按钮事件
document.getElementById('open-btn').addEventListener('click', loadFile)

HTML 界面

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>IPC 通信示例</title>
  <style>
    body { font-family: sans-serif; padding: 20px; background: #1e1e1e; color: #d4d4d4; }
    button { padding: 10px 20px; margin: 5px; cursor: pointer; }
    #content { 
      background: #252526; 
      padding: 15px; 
      border-radius: 4px; 
      min-height: 200px;
      white-space: pre-wrap;
      font-family: monospace;
    }
  </style>
</head>
<body>
  <h1>IPC 通信演示</h1>
  <button id="open-btn">📂 打开文件</button>
  <button id="quit-btn">❌ 退出应用</button>
  <p>状态: <span id="status">就绪</span></p>
  <div id="content">点击"打开文件"按钮选择文件...</div>
  
  <!-- 注意:不需要引入 preload.js,它由主进程自动加载 -->
  <script src="renderer.js"></script>
</body>
</html>
安全警告

永远不要直接暴露 ipcRenderer 给渲染进程!上面的 preload.js 使用了 contextBridge选择性暴露特定方法,这是 Electron 推荐的安全做法。直接暴露整个 ipcRenderer 会让恶意脚本有能力发送任意 IPC 消息。

2.3 预加载脚本与安全模型

Electron 的安全模型经历了多次演进。理解这些安全设置对构建生产级应用至关重要。

关键安全配置

配置项 默认值 推荐值 说明
nodeIntegration false false 是否在渲染进程中启用 Node.js。关闭可防止 XSS 攻击执行系统命令。
contextIsolation true true 上下文隔离。开启后 preload 和页面运行在不同上下文,防止页面脚本篡改暴露的 API。
enableRemoteModule false false remote 模块已被废弃,使用 IPC 替代。
allowRunningInsecureContent false false 允许在 HTTPS 页面中运行 HTTP 内容。保持关闭。
安全红线
  • 不要在渲染进程中直接执行 require('child_process').exec('rm -rf /')
  • 不要加载远程不可信的 URL 到主窗口
  • 不要禁用 contextIsolation 来"图方便"
  • 所有用户输入都要验证和转义

安全架构图解

Electron 安全架构
┌─────────────────────────────────────────────────────────┐
│                    渲染进程 (Chromium)                     │
│  ┌─────────────────┐      ┌──────────────────────────┐   │
│  │   页面脚本       │      │     预加载脚本            │   │
│  │  (不可信代码)    │      │  (可信,可访问 Node API)  │   │
│  │                 │      │                          │   │
│  │  window.electron│◄─────│ contextBridge.exposeIn   │   │
│  │    API.readFile │      │   MainWorld('electronAPI')│   │
│  └─────────────────┘      └──────────────────────────┘   │
│           ▲                           │                  │
│           │      上下文隔离              │ 可访问 Node.js  │
│           └───────────────────────────┘                  │
├─────────────────────────────────────────────────────────┤
│                      IPC 通信层                          │
│              (ipcRenderer ↔ ipcMain)                     │
├─────────────────────────────────────────────────────────┤
│                     主进程 (Node.js)                     │
│  ┌─────────────────────────────────────────────────┐   │
│  │  完整的系统访问权限:fs、net、child_process、dialog │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

2.4 应用生命周期事件

Electron 应用有一系列生命周期事件,掌握它们能帮你更好地控制应用行为。

生命周期事件示例
const { app } = require('electron')

// 应用正在准备启动
app.on('will-finish-launching', () => {
  console.log('应用即将完成启动')
})

// Electron 初始化完成,可以创建窗口了
app.whenReady().then(() => {
  console.log('应用已就绪')
  createWindow()
})

// 所有窗口已关闭
app.on('window-all-closed', () => {
  console.log('所有窗口已关闭')
  // macOS 上通常不退出应用
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

// 应用即将退出
app.on('will-quit', (event) => {
  console.log('应用即将退出')
  // event.preventDefault() // 可以阻止退出
})

// 应用已退出
app.on('quit', () => {
  console.log('应用已退出')
})

// macOS: 点击 Dock 图标时触发
app.on('activate', () => {
  console.log('应用被激活')
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

// 应用获得焦点
app.on('browser-window-focus', () => {
  console.log('窗口获得焦点')
})

// 应用失去焦点
app.on('browser-window-blur', () => {
  console.log('窗口失去焦点')
})

生命周期流程图

生命周期流程
[启动应用]
    ↓
[will-finish-launching]
    ↓
[ready] ←── 创建窗口、设置菜单、初始化数据
    ↓
[运行中] ←── 用户交互、窗口管理
    ↓
[window-all-closed]
    ↓
    ├─→ macOS: 保持运行,等待 [activate] 重新创建窗口
    └─→ Win/Linux: 继续 → [will-quit]
                            ↓
                      [before-quit]
                            ↓
                        [quit] ←── 清理资源、保存状态
最佳实践
  • ready 事件中初始化应用,不要在此之前调用 Electron API
  • before-quitwill-quit 中保存用户数据和配置
  • macOS 上处理 activate 事件,实现点击 Dock 重新打开窗口
  • 使用 app.isReady() 检查应用状态,避免在 ready 之前操作
本章小结

本章核心要点:

  • Electron 采用多进程架构:1 个主进程 + N 个渲染进程
  • 主进程拥有系统级权限,渲染进程负责 UI
  • IPC 是进程间通信的唯一安全通道,有三种通信模式
  • 预加载脚本(preload)是安全桥梁,通过 contextBridge 暴露 API
  • 始终开启 contextIsolation,关闭 nodeIntegration
  • 掌握生命周期事件,正确管理应用启动和退出

下一章我们将学习如何用 Vue3 + Vite 进行工程化开发,告别手搭项目的原始时代。