electron系列3:进程模型深度解析:主进程、渲染进程、预加载脚本

一、为什么需要理解进程模型?

很多Electron初学者会遇到这样的困惑:

二、进程模型全景图

2.1 架构总览

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                         Electron 进程模型全景图                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                         主进程 (Main Process)                        │    │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐ │    │
│  │  │ 窗口管理器   │  │ 应用生命周期 │  │ 原生菜单    │  │ 全局快捷键  │ │    │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘ │    │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐ │    │
│  │  │ 文件系统    │  │ 数据库     │  │ 子进程     │  │ 网络请求    │ │    │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘ │    │
│  │                                                                      │    │
│  │  🟢 可以访问: Node.js API + Electron 原生 API                        │    │
│  │  🔴 不能访问: DOM (没有浏览器环境)                                    │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                      │                                      │
│                              IPC 通信通道                                    │
│                    (ipcMain.on / ipcRenderer.send)                          │
│                                      │                                      │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                     预加载脚本 (Preload Script)                      │    │
│  │                                                                      │    │
│  │  "桥梁" - 在主进程和渲染进程之间建立安全的通信通道                    │    │
│  │                                                                      │    │
│  │  contextBridge.exposeInMainWorld('api', { ... })                     │    │
│  │                                                                      │    │
│  │  🟢 可以访问: Node.js API + Electron API + DOM (部分)                │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                      │                                      │
│                              contextBridge                                  │
│                                      │                                      │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                      渲染进程 (Renderer Process)                     │    │
│  │                                                                      │    │
│  │  ┌─────────────────────────────────────────────────────────────┐    │    │
│  │  │                    Web 页面 (Vue/React/Angular)              │    │    │
│  │  │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐        │    │    │
│  │  │  │ 组件库  │  │ 路由   │  │ 状态管理 │  │ 动画   │        │    │    │
│  │  │  └─────────┘  └─────────┘  └─────────┘  └─────────┘        │    │    │
│  │  └─────────────────────────────────────────────────────────────┘    │    │
│  │                                                                      │    │
│  │  🟢 可以访问: DOM API + 通过window.api调用的桌面能力                  │    │
│  │  🔴 不能访问: Node.js API (除非配置开启,但不安全)                    │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

2.2 进程职责对比表

维度 主进程 渲染进程 预加载脚本
实例数量 1个 多个(每个窗口一个) 每个渲染进程一个
生命周期 应用启动到退出 窗口关闭即销毁 随渲染进程
Node.js访问 ✅ 完全访问 ❌ 默认关闭 ✅ 完全访问
DOM访问 ❌ 无 ✅ 完全访问 ⚠️ 有限访问
主要职责 管理窗口、系统集成 渲染UI、用户交互 API桥接、安全隔离
代码复杂度
崩溃影响 整个应用退出 单个窗口关闭 对应窗口失效

三、主进程深度解析

3.1 主进程的职责

主进程是Electron应用的"大脑",负责:

javascript 复制代码
// packages/main/src/index.ts - 完整示例

import { app, BrowserWindow, Menu, Tray, ipcMain, shell, globalShortcut } from 'electron'
import path from 'path'

// ============== 1. 控制应用生命周期 ==============
app.whenReady().then(() => {
  console.log('应用已就绪')
  createMainWindow()
})

app.on('window-all-closed', () => {
  // macOS 应用通常不退出
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // macOS 点击dock图标时重新创建窗口
  if (BrowserWindow.getAllWindows().length === 0) {
    createMainWindow()
  }
})

// ============== 2. 创建和管理窗口 ==============
let mainWindow: BrowserWindow | null = null
let settingsWindow: BrowserWindow | null = null

function createMainWindow(): BrowserWindow {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, '../preload/dist/index.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  })
  
  mainWindow.loadFile('dist/index.html')
  
  mainWindow.on('closed', () => {
    mainWindow = null
  })
  
  return mainWindow
}

function createSettingsWindow(): BrowserWindow {
  settingsWindow = new BrowserWindow({
    width: 600,
    height: 400,
    parent: mainWindow!,  // 设置父窗口
    modal: true,          // 模态窗口
    webPreferences: {
      preload: path.join(__dirname, '../preload/dist/index.js'),
    },
  })
  
  settingsWindow.loadFile('dist/settings.html')
  
  settingsWindow.on('closed', () => {
    settingsWindow = null
  })
  
  return settingsWindow
}

// ============== 3. 创建原生菜单 ==============
const menuTemplate: MenuItemConstructorOptions[] = [
  {
    label: '文件',
    submenu: [
      {
        label: '新建文件',
        accelerator: 'CmdOrCtrl+N',
        click: () => {
          // 触发新建文件操作
          mainWindow?.webContents.send('menu:new-file')
        }
      },
      {
        label: '打开文件',
        accelerator: 'CmdOrCtrl+O',
        click: () => {
          // 触发打开文件对话框
          mainWindow?.webContents.send('menu:open-file')
        }
      },
      { type: 'separator' },
      {
        label: '退出',
        accelerator: 'CmdOrCtrl+Q',
        click: () => {
          app.quit()
        }
      }
    ]
  },
  {
    label: '编辑',
    submenu: [
      { label: '撤销', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
      { label: '重做', accelerator: 'CmdOrCtrl+Y', role: 'redo' },
      { type: 'separator' },
      { label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' },
      { label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' },
      { label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' }
    ]
  },
  {
    label: '视图',
    submenu: [
      { label: '重新加载', accelerator: 'CmdOrCtrl+R', role: 'reload' },
      { label: '开发者工具', accelerator: 'F12', role: 'toggleDevTools' },
      { type: 'separator' },
      { label: '实际大小', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' },
      { label: '放大', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' },
      { label: '缩小', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' }
    ]
  },
  {
    label: '窗口',
    submenu: [
      {
        label: '设置',
        click: () => {
          if (settingsWindow) {
            settingsWindow.focus()
          } else {
            createSettingsWindow()
          }
        }
      },
      { type: 'separator' },
      { label: '最小化', role: 'minimize' },
      { label: '关闭', role: 'close' }
    ]
  }
]

// macOS 特殊处理
if (process.platform === 'darwin') {
  menuTemplate.unshift({
    label: app.getName(),
    submenu: [
      { label: '关于', role: 'about' },
      { type: 'separator' },
      { label: '服务', role: 'services' },
      { type: 'separator' },
      { label: '隐藏', role: 'hide' },
      { label: '隐藏其他', role: 'hideOthers' },
      { label: '显示全部', role: 'unhide' },
      { type: 'separator' },
      { label: '退出', accelerator: 'Cmd+Q', click: () => app.quit() }
    ]
  })
}

const menu = Menu.buildFromTemplate(menuTemplate)
Menu.setApplicationMenu(menu)

// ============== 4. 创建系统托盘 ==============
let tray: Tray | null = null

function createTray() {
  const iconPath = path.join(__dirname, '../../resources/tray-icon.png')
  tray = new Tray(iconPath)
  
  const contextMenu = Menu.buildFromTemplate([
    { label: '显示主窗口', click: () => mainWindow?.show() },
    { label: '退出', click: () => app.quit() }
  ])
  
  tray.setToolTip('My Electron App')
  tray.setContextMenu(contextMenu)
  
  tray.on('click', () => {
    mainWindow?.isVisible() ? mainWindow?.hide() : mainWindow?.show()
  })
}

// ============== 5. 注册全局快捷键 ==============
app.whenReady().then(() => {
  // 注册 Ctrl+Shift+Space 快捷键
  const ret = globalShortcut.register('CommandOrControl+Shift+Space', () => {
    console.log('全局快捷键被触发')
    mainWindow?.show()
    mainWindow?.focus()
  })
  
  if (!ret) {
    console.log('快捷键注册失败')
  }
})

// 应用退出前注销快捷键
app.on('will-quit', () => {
  globalShortcut.unregisterAll()
})

// ============== 6. IPC 通信处理器 ==============
ipcMain.handle('file:read', async (_event, filePath: string) => {
  // 读取文件 - 使用 Node.js fs 模块
  const fs = require('fs/promises')
  const content = await fs.readFile(filePath, 'utf-8')
  return content
})

ipcMain.handle('system:get-info', () => {
  return {
    platform: process.platform,
    arch: process.arch,
    version: app.getVersion(),
    electronVersion: process.versions.electron,
    chromeVersion: process.versions.chrome,
    nodeVersion: process.versions.node,
  }
})

// ============== 7. 打开外部链接 ==============
ipcMain.handle('system:open-external', async (_event, url: string) => {
  await shell.openExternal(url)
})

3.2 主进程的注意事项

javascript 复制代码
// ❌ 错误示例:在主进程执行耗时操作
ipcMain.handle('process:large-data', async () => {
  // 这会阻塞所有窗口!
  const result = heavyComputation()  // 耗时5秒
  return result
})

// ✅ 正确示例:使用 Worker Thread 或拆分任务
import { Worker } from 'worker_threads'

ipcMain.handle('process:large-data', async () => {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js')
    worker.on('message', resolve)
    worker.on('error', reject)
    worker.postMessage('start')
  })
})

// ❌ 错误示例:在多个窗口之间直接共享引用
let globalData = {}  // 不同渲染进程访问会有问题

// ✅ 正确示例:使用 WebContents 发送消息
function broadcastToAllWindows(channel: string, data: any) {
  BrowserWindow.getAllWindows().forEach(window => {
    window.webContents.send(channel, data)
  })
}

四、渲染进程深度解析

4.1 渲染进程的本质

每个渲染进程就是一个完整的Chromium浏览器实例,可以:

html 复制代码
<!-- packages/renderer/src/App.vue -->
<template>
  <div class="app">
    <h1>渲染进程示例</h1>
    
    <!-- 1. 可以使用完整的 DOM API -->
    <div ref="container" class="container"></div>
    
    <!-- 2. 可以使用 CSS 动画 -->
    <div class="animated-box" :class="{ active: isActive }"></div>
    
    <!-- 3. 可以使用 Web API -->
    <button @click="fetchData">Fetch Data</button>
    
    <!-- 4. 可以播放视频/音频 -->
    <video ref="videoPlayer" controls>
      <source src="/sample.mp4" type="video/mp4">
    </video>
    
    <!-- 5. 可以使用 Canvas -->
    <canvas ref="canvas" width="400" height="300"></canvas>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const container = ref<HTMLDivElement>()
const videoPlayer = ref<HTMLVideoElement>()
const canvas = ref<HTMLCanvasElement>()
const isActive = ref(false)

onMounted(() => {
  // DOM 操作
  if (container.value) {
    const newDiv = document.createElement('div')
    newDiv.textContent = '动态创建的元素'
    container.value.appendChild(newDiv)
  }
  
  // Canvas 绘图
  if (canvas.value) {
    const ctx = canvas.value.getContext('2d')
    if (ctx) {
      ctx.fillStyle = 'red'
      ctx.fillRect(0, 0, 100, 100)
      ctx.fillStyle = 'blue'
      ctx.fillRect(100, 100, 100, 100)
    }
  }
  
  // 请求动画帧
  function animate() {
    isActive.value = !isActive.value
    requestAnimationFrame(animate)
  }
  // animate() // 开启动画
})

// Web API 调用
const fetchData = async () => {
  // 普通 HTTP 请求
  const response = await fetch('https://api.github.com/repos/electron/electron')
  const data = await response.json()
  console.log(data)
  
  // WebSocket 连接
  const ws = new WebSocket('ws://localhost:8080')
  ws.onopen = () => console.log('WebSocket connected')
  
  // IndexedDB 存储
  const request = indexedDB.open('MyDatabase', 1)
  request.onsuccess = () => console.log('IndexedDB opened')
  
  // 本地存储
  localStorage.setItem('key', 'value')
  
  // Service Worker
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
  }
}
</script>

<style scoped>
.animated-box {
  width: 100px;
  height: 100px;
  background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
  transition: transform 0.3s ease;
}

.animated-box.active {
  transform: rotate(45deg) scale(1.2);
}
</style>

4.2 渲染进程的安全限制

javascript 复制代码
// ❌ 以下代码在默认配置下无法运行

// 1. 无法直接 require Node.js 模块
const fs = require('fs')  // ReferenceError: require is not defined

// 2. 无法使用 __dirname 等 Node.js 全局变量
console.log(__dirname)  // ReferenceError: __dirname is not defined

// 3. 无法直接使用 Electron 模块
const { ipcRenderer } = require('electron')  // 错误!

// ✅ 必须通过预加载脚本暴露的 API 来调用
window.electronAPI.file.readTextFile('/path/to/file.txt')

五、预加载脚本:安全的桥梁

5.1 为什么需要预加载脚本?

javascript 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    安全模型对比                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  方案1: nodeIntegration: true (不安全)                          │
│  ┌─────────────┐                                                │
│  │ 渲染进程     │ ← 可以直接访问 Node.js                         │
│  │ (任意网页)   │ ← XSS攻击可以直接删除系统文件!                 │
│  └─────────────┘                                                │
│                                                                  │
│  方案2: nodeIntegration: false + preload (安全)                 │
│  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐       │
│  │ 渲染进程     │ ──→ │ 预加载脚本   │ ──→ │ 主进程       │       │
│  │ (不可信)    │     │ (可信桥梁)  │     │ (系统能力)   │       │
│  └─────────────┘     └─────────────┘     └─────────────┘       │
│        ↑                    │                    │              │
│        │                    ↓                    │              │
│  只能调用预加载     只暴露必要的API        实际执行敏感操作      │
│  暴露的API                                                       │
│                                                                  │
│  ✅ XSS攻击无法直接访问系统资源                                  │
│  ✅ 可以精细控制暴露哪些API                                      │
│  ✅ 可以添加权限检查、日志记录                                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

5.2 完整的预加载脚本实现

packages/preload/src/index.ts

javascript 复制代码
import { contextBridge, ipcRenderer } from 'electron'

// ============== 1. 定义暴露的API类型 ==============
export interface SafeElectronAPI {
  // 文件操作(只暴露必要的操作方法)
  file: {
    readText: (path: string) => Promise<string>
    writeText: (path: string, content: string) => Promise<void>
    selectFile: (options?: { filters?: FileFilter[] }) => Promise<string | null>
  }
  
  // 系统信息(只读,不暴露修改能力)
  system: {
    getPlatform: () => string
    getVersion: () => string
    getScreenSize: () => { width: number; height: number }
    openExternal: (url: string) => Promise<void>
  }
  
  // 窗口控制(只暴露用户主动触发的操作)
  window: {
    minimize: () => void
    maximize: () => void
    close: () => void
    isMaximized: () => Promise<boolean>
  }
  
  // 事件监听(支持双向通信,但不暴露发送能力)
  onThemeChange: (callback: (theme: 'light' | 'dark') => void) => () => void
  
  // 数据存储(安全的本地存储)
  storage: {
    get: (key: string) => Promise<any>
    set: (key: string, value: any) => Promise<void>
    delete: (key: string) => Promise<void>
  }
}

interface FileFilter {
  name: string
  extensions: string[]
}

// ============== 2. 实现API(带安全校验) ==============
const createSafeAPI = (): SafeElectronAPI => {
  return {
    // 文件操作 - 限制只能操作特定目录
    file: {
      readText: async (path: string) => {
        // 安全检查:限制只能读取用户选择的文件
        if (!isValidPath(path)) {
          throw new Error('Invalid file path')
        }
        return ipcRenderer.invoke('file:read-text', path)
      },
      
      writeText: async (path: string, content: string) => {
        // 安全检查:限制写入内容大小
        if (content.length > 100 * 1024 * 1024) {  // 100MB限制
          throw new Error('File too large')
        }
        return ipcRenderer.invoke('file:write-text', path, content)
      },
      
      selectFile: async (options?: { filters?: FileFilter[] }) => {
        return ipcRenderer.invoke('dialog:select-file', options)
      },
    },
    
    // 系统信息 - 只读
    system: {
      getPlatform: () => process.platform,
      getVersion: () => process.versions.electron,
      getScreenSize: () => {
        const { screen } = require('electron')
        const { width, height } = screen.getPrimaryDisplay().workAreaSize
        return { width, height }
      },
      openExternal: (url: string) => {
        // 安全检查:只允许特定协议
        if (!url.startsWith('https://') && !url.startsWith('http://')) {
          throw new Error('Only HTTP/HTTPS URLs are allowed')
        }
        return ipcRenderer.invoke('system:open-external', url)
      },
    },
    
    // 窗口控制
    window: {
      minimize: () => ipcRenderer.send('window:minimize'),
      maximize: () => ipcRenderer.send('window:maximize'),
      close: () => ipcRenderer.send('window:close'),
      isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
    },
    
    // 主题监听
    onThemeChange: (callback) => {
      const handler = (_event: any, theme: 'light' | 'dark') => {
        // 确保回调只接收预期的数据类型
        if (theme === 'light' || theme === 'dark') {
          callback(theme)
        }
      }
      ipcRenderer.on('system:theme-changed', handler)
      return () => ipcRenderer.removeListener('system:theme-changed', handler)
    },
    
    // 安全存储 - 加密存储
    storage: {
      get: async (key: string) => {
        const encrypted = await ipcRenderer.invoke('storage:get', key)
        if (!encrypted) return null
        // 解密后返回
        return decrypt(encrypted)
      },
      set: async (key: string, value: any) => {
        const encrypted = encrypt(value)
        await ipcRenderer.invoke('storage:set', key, encrypted)
      },
      delete: async (key: string) => {
        await ipcRenderer.invoke('storage:delete', key)
      },
    },
  }
}

// 路径安全检查
function isValidPath(path: string): boolean {
  // 禁止访问系统敏感目录
  const forbiddenPaths = [
    '/etc/passwd',
    '/etc/shadow',
    'C:\\Windows\\System32',
    '/System',
  ]
  
  for (const forbidden of forbiddenPaths) {
    if (path.startsWith(forbidden)) {
      console.warn(`Blocked access to forbidden path: ${path}`)
      return false
    }
  }
  
  return true
}

// 简单的加密(生产环境应使用更安全的方案)
function encrypt(data: any): string {
  // TODO: 实现真正的加密
  return Buffer.from(JSON.stringify(data)).toString('base64')
}

function decrypt(encrypted: string): any {
  // TODO: 实现真正的解密
  return JSON.parse(Buffer.from(encrypted, 'base64').toString())
}

// ============== 3. 暴露API到渲染进程 ==============
const safeAPI = createSafeAPI()
contextBridge.exposeInMainWorld('electronAPI', safeAPI)

// ============== 4. 类型声明 ==============
declare global {
  interface Window {
    electronAPI: SafeElectronAPI
  }
}

// ============== 5. 加载完成通知 ==============
window.addEventListener('DOMContentLoaded', () => {
  console.log('Preload script loaded, API exposed:', Object.keys(safeAPI))
})

5.3 预加载脚本的最佳实践

javascript 复制代码
// ✅ 最佳实践 1:只暴露必要的方法
contextBridge.exposeInMainWorld('api', {
  readFile: (path) => ipcRenderer.invoke('read-file', path)
  // 不要暴露整个 fs 模块!
})

// ❌ 错误:暴露了整个模块
contextBridge.exposeInMainWorld('fs', require('fs'))

// ✅ 最佳实践 2:参数校验和净化
contextBridge.exposeInMainWorld('api', {
  saveFile: async (path, content) => {
    // 校验参数类型
    if (typeof path !== 'string' || typeof content !== 'string') {
      throw new Error('Invalid parameters')
    }
    // 校验路径安全性
    if (path.includes('..') || path.includes('~')) {
      throw new Error('Invalid path')
    }
    return ipcRenderer.invoke('save-file', path, content)
  }
})

// ✅ 最佳实践 3:添加日志记录
contextBridge.exposeInMainWorld('api', {
  sensitiveOperation: async (data) => {
    console.log('[Security] Sensitive operation called', {
      timestamp: Date.now(),
      dataSize: data.length
    })
    return ipcRenderer.invoke('sensitive-op', data)
  }
})

六、IPC通信详解

6.1 通信模式全景图

javascript 复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                         IPC 通信模式全景图                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  模式1: 单向通信 (渲染进程 → 主进程)                                         │
│  ┌─────────────┐    ipcRenderer.send()    ┌─────────────┐                   │
│  │ 渲染进程     │ ───────────────────────→ │ 主进程       │                   │
│  │             │                          │             │                   │
│  │             │                          │ ipcMain.on()│                   │
│  └─────────────┘                          └─────────────┘                   │
│                                                                              │
│  模式2: 双向通信 (请求-响应)                                                 │
│  ┌─────────────┐    ipcRenderer.invoke()   ┌─────────────┐                   │
│  │ 渲染进程     │ ───────────────────────→ │ 主进程       │                   │
│  │             │                          │             │                   │
│  │             │    Promise.resolve()     │ ipcMain.    │                   │
│  │             │ ←─────────────────────── │ handle()    │                   │
│  └─────────────┘                          └─────────────┘                   │
│                                                                              │
│  模式3: 主进程主动推送                                                        │
│  ┌─────────────┐                          ┌─────────────┐                   │
│  │ 渲染进程     │                          │ 主进程       │                   │
│  │             │                          │             │                   │
│  │ ipcRenderer.│    win.webContents.      │             │                   │
│  │ on()        │ ←─────────────────────── │ send()      │                   │
│  └─────────────┘                          └─────────────┘                   │
│                                                                              │
│  模式4: 渲染进程之间通信 (通过主进程中转)                                     │
│  ┌─────────────┐    ipcRenderer.send()   ┌─────────────┐                    │
│  │ 渲染进程 A   │ ──────────────────────→ │ 主进程       │                    │
│  └─────────────┘                          │             │                    │
│                                          │ 转发消息     │                    │
│  ┌─────────────┐    win.webContents.     │             │                    │
│  │ 渲染进程 B   │ ←────────────────────── │ send()      │                    │
│  └─────────────┘                          └─────────────┘                    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

6.2 IPC 通信完整示例

主进程处理器:

TypeScript 复制代码
// packages/main/src/ipc/handlers.ts
import { ipcMain, BrowserWindow } from 'electron'

// 模式1: 单向通信(不需要响应)
ipcMain.on('log:info', (event, message: string) => {
  console.log(`[Renderer ${event.sender.id}]: ${message}`)
})

// 模式2: 双向通信(需要响应)
ipcMain.handle('compute:sum', async (event, a: number, b: number) => {
  console.log(`Computing sum of ${a} + ${b} from window ${event.sender.id}`)
  return a + b
})

// 模式2: 异步操作示例
ipcMain.handle('data:fetch-large', async (event, query: string) => {
  // 模拟耗时操作
  const result = await fetchFromDatabase(query)
  return result
})

// 模式3: 主进程主动推送(广播)
export function broadcastToAllRenderers(channel: string, data: any) {
  BrowserWindow.getAllWindows().forEach(window => {
    if (!window.isDestroyed()) {
      window.webContents.send(channel, data)
    }
  })
}

// 示例:定时广播系统时间
setInterval(() => {
  broadcastToAllRenderers('system:tick', {
    timestamp: Date.now(),
    time: new Date().toISOString()
  })
}, 1000)

// 模式4: 渲染进程间通信(通过主进程转发)
ipcMain.on('message:to-window', (event, targetWindowId: number, message: any) => {
  const targetWindow = BrowserWindow.fromId(targetWindowId)
  if (targetWindow && !targetWindow.isDestroyed()) {
    targetWindow.webContents.send('message:from-window', {
      from: event.sender.id,
      message
    })
  } else {
    event.sender.send('message:error', `Window ${targetWindowId} not found`)
  }
})

渲染进程调用示例:

javascript 复制代码
<!-- packages/renderer/src/components/IPCDemo.vue -->
<template>
  <div class="ipc-demo">
    <h2>IPC 通信示例</h2>
    
    <!-- 模式1: 单向通信 -->
    <section>
      <h3>单向通信</h3>
      <button @click="sendLog">发送日志</button>
    </section>
    
    <!-- 模式2: 双向通信 -->
    <section>
      <h3>双向通信</h3>
      <input v-model.number="numA" type="number" placeholder="数字A">
      <input v-model.number="numB" type="number" placeholder="数字B">
      <button @click="calculateSum">计算和</button>
      <p>结果: {{ sumResult }}</p>
    </section>
    
    <!-- 模式3: 接收主进程推送 -->
    <section>
      <h3>主进程推送</h3>
      <p>系统时间: {{ systemTime }}</p>
      <p>接收次数: {{ tickCount }}</p>
    </section>
    
    <!-- 模式4: 多窗口通信演示 -->
    <section>
      <h3>多窗口通信</h3>
      <button @click="openSecondWindow">打开第二个窗口</button>
      <button @click="sendMessageToOtherWindow" :disabled="!otherWindowId">
        向另一窗口发送消息
      </button>
      <p>接收到的消息: {{ receivedMessage }}</p>
    </section>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

// 模式1: 单向通信
const sendLog = () => {
  // 方式1: 通过预加载API
  window.electronAPI?.sendLog?.('用户点击了按钮')
  
  // 方式2: 直接使用ipcRenderer(如果有暴露)
  // window.ipcRenderer?.send('log:info', '用户点击了按钮')
}

// 模式2: 双向通信
const numA = ref(0)
const numB = ref(0)
const sumResult = ref<number | null>(null)

const calculateSum = async () => {
  // 通过预加载API调用
  const result = await window.electronAPI?.computeSum?.(numA.value, numB.value)
  sumResult.value = result ?? null
}

// 模式3: 接收主进程推送
const systemTime = ref('')
const tickCount = ref(0)

// 模式4: 多窗口通信
const otherWindowId = ref<number | null>(null)
const receivedMessage = ref('')

const openSecondWindow = async () => {
  // 打开第二个窗口的逻辑
  const windowId = await window.electronAPI?.openSecondaryWindow?.()
  otherWindowId.value = windowId ?? null
}

const sendMessageToOtherWindow = () => {
  if (otherWindowId.value) {
    window.electronAPI?.sendToWindow?.(otherWindowId.value, {
      text: `Hello from window ${Date.now()}`
    })
  }
}

// 监听来自主进程的消息
onMounted(() => {
  // 监听系统时间推送
  window.electronAPI?.onSystemTick?.((data: any) => {
    systemTime.value = data.time
    tickCount.value++
  })
  
  // 监听来自其他窗口的消息
  window.electronAPI?.onMessageFromWindow?.((data: any) => {
    receivedMessage.value = `来自窗口 ${data.from}: ${JSON.stringify(data.message)}`
  })
})

onUnmounted(() => {
  // 清理监听器
  window.electronAPI?.removeAllListeners?.()
})
</script>

6.3 高级IPC模式:流式传输

javascript 复制代码
// 处理大文件传输时,使用流式传输避免内存溢出
// 主进程
ipcMain.on('file:read-stream', (event, filePath: string) => {
  const stream = fs.createReadStream(filePath)
  
  stream.on('data', (chunk) => {
    // 分块发送
    event.sender.send('file:stream-chunk', chunk.toString('base64'))
  })
  
  stream.on('end', () => {
    event.sender.send('file:stream-end')
  })
  
  stream.on('error', (error) => {
    event.sender.send('file:stream-error', error.message)
  })
})

// 渲染进程
async function readLargeFile(filePath: string) {
  return new Promise((resolve, reject) => {
    const chunks: string[] = []
    
    const onChunk = (_event: any, chunk: string) => {
      chunks.push(chunk)
    }
    
    const onEnd = () => {
      cleanup()
      resolve(chunks.join(''))
    }
    
    const onError = (_event: any, error: string) => {
      cleanup()
      reject(new Error(error))
    }
    
    const cleanup = () => {
      ipcRenderer.removeListener('file:stream-chunk', onChunk)
      ipcRenderer.removeListener('file:stream-end', onEnd)
      ipcRenderer.removeListener('file:stream-error', onError)
    }
    
    ipcRenderer.on('file:stream-chunk', onChunk)
    ipcRenderer.on('file:stream-end', onEnd)
    ipcRenderer.on('file:stream-error', onError)
    
    ipcRenderer.send('file:read-stream', filePath)
  })
}

七、内存管理与性能优化

7.1 进程生命周期监控

javascript 复制代码
// packages/main/src/utils/processMonitor.ts
import { app, BrowserWindow, webContents } from 'electron'

export class ProcessMonitor {
  private interval: NodeJS.Timeout | null = null
  
  startMonitoring() {
    this.interval = setInterval(() => {
      this.logMemoryUsage()
      this.checkForLeaks()
    }, 30000) // 每30秒检查一次
  }
  
  private logMemoryUsage() {
    // 主进程内存
    const mainMemory = process.memoryUsage()
    console.log('[Memory] Main Process:', {
      rss: `${Math.round(mainMemory.rss / 1024 / 1024)}MB`,
      heapUsed: `${Math.round(mainMemory.heapUsed / 1024 / 1024)}MB`,
      heapTotal: `${Math.round(mainMemory.heapTotal / 1024 / 1024)}MB`,
    })
    
    // 渲染进程内存
    BrowserWindow.getAllWindows().forEach((window, index) => {
      const webContents = window.webContents
      const memory = webContents.getProcessMemoryInfo()
      memory.then(info => {
        console.log(`[Memory] Renderer ${index + 1}:`, {
          private: `${Math.round(info.private / 1024 / 1024)}MB`,
          shared: `${Math.round(info.shared / 1024 / 1024)}MB`,
        })
      })
    })
  }
  
  private checkForLeaks() {
    // 检查是否有未关闭的窗口
    const windows = BrowserWindow.getAllWindows()
    const hiddenWindows = windows.filter(w => !w.isVisible() && !w.isDestroyed())
    
    if (hiddenWindows.length > 0) {
      console.warn(`[Leak] Found ${hiddenWindows.length} hidden but alive windows`)
    }
  }
  
  stopMonitoring() {
    if (this.interval) {
      clearInterval(this.interval)
      this.interval = null
    }
  }
}

7.2 防止内存泄漏的最佳实践

javascript 复制代码
// ✅ 正确:清理事件监听器
class MyService {
  private handlers: Array<() => void> = []
  
  registerCleanup(cleanup: () => void) {
    this.handlers.push(cleanup)
  }
  
  destroy() {
    this.handlers.forEach(cleanup => cleanup())
    this.handlers = []
  }
}

// 在窗口关闭时清理
mainWindow.on('closed', () => {
  // 清理所有监听器
  ipcMain.removeAllListeners('some-channel')
  // 取消引用
  mainWindow = null
})

// ❌ 错误:全局变量持有引用
let globalWindowRef: BrowserWindow | null = null
function createWindow() {
  const win = new BrowserWindow()
  globalWindowRef = win  // 阻止GC回收
  return win
}

八、调试技巧

8.1 进程调试对比表

进程类型 调试方法 常用工具 注意事项
主进程 VSCode Attach VSCode Debugger 需要配置 launch.json
主进程 命令行日志 console.log 使用 debug 模块更好
渲染进程 Chrome DevTools Ctrl+Shift+I 需要手动打开
渲染进程 Vue/React DevTools 扩展插件 开发环境自动安装
预加载脚本 混合调试 主进程+渲染进程 日志会输出到主进程控制台

8.2 完整的调试配置

javascript 复制代码
// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Main Process",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "args": ["."],
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "console": "integratedTerminal",
      "sourceMaps": true
    },
    {
      "name": "Debug Renderer Process",
      "type": "chrome",
      "request": "attach",
      "port": 9222,
      "webRoot": "${workspaceFolder}/src",
      "timeout": 30000
    },
    {
      "name": "Debug All",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "args": ["."],
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "console": "integratedTerminal",
      "sourceMaps": true,
      "serverReadyAction": {
        "pattern": "DevTools listening on ws://127.0.0.1:(\\d+)/",
        "uriFormat": "http://localhost:%s",
        "webRoot": "${workspaceFolder}"
      }
    }
  ],
  "compounds": [
    {
      "name": "Full Debug",
      "configurations": ["Debug Main Process", "Debug Renderer Process"]
    }
  ]
}

九、常见问题与解决方案

Q1: 渲染进程之间如何共享数据?

javascript 复制代码
// 方案1:通过主进程中转(推荐)
// 主进程
ipcMain.handle('store:get', (event, key) => {
  return sharedStore.get(key)
})

// 渲染进程
const data = await window.electronAPI.storeGet('some-key')

// 方案2:使用 WebContents 的 session
// 主进程
const ses = win.webContents.session
ses.storage.setItem('key', 'value')

Q2: 预加载脚本中如何访问 DOM?

javascript 复制代码
// 预加载脚本可以访问 DOM,但要小心时机
window.addEventListener('DOMContentLoaded', () => {
  // DOM 已加载,可以安全操作
  document.body.style.backgroundColor = 'lightgray'
})

// 也可以注入脚本
window.addEventListener('load', () => {
  const script = document.createElement('script')
  script.textContent = `console.log('Injected script')`
  document.head.appendChild(script)
})

Q3: 如何在多个窗口间复用同一个渲染进程?

javascript 复制代码
// 使用 BrowserView 而不是多个 BrowserWindow
import { BrowserView, BrowserWindow } from 'electron'

const win = new BrowserWindow()
const view1 = new BrowserView()
const view2 = new BrowserView()

win.setBrowserView(view1)
win.setBrowserView(view2)

// 或使用 WebContents 的 addWorkSpace API
相关推荐
恋猫de小郭2 小时前
手机直接运行 Codex/OpenCode/Claude Code ,实时管理你的 AI Coding
前端·openai·ai编程
Canace2 小时前
为什么不要让LLM帮我们写文档
前端·人工智能
之歆2 小时前
前端性能优化:从路由懒加载到打包优化
前端·性能优化
米丘2 小时前
ESTree 规范 (acorn@8.15.0示例)
前端·javascript·编译器
天下权2 小时前
OpenLayers 地图绘制与交互实战:从零构建一个完整的绘制系统
前端·gis
饺子不吃醋2 小时前
深入理解浏览器渲染流程
前端·javascript
我命由我123452 小时前
React - 组件优化、children props 与 render props、错误边界
前端·javascript·react.js·前端框架·html·ecmascript·js
木斯佳2 小时前
前端八股文面经大全:快手前端一面 (2026-04-07)·面经深度解析
前端·ai·性能优化·hooks·移动端适配
小陈工2 小时前
Python Web开发入门(十三):API版本管理与兼容性——让你的接口优雅地“长大”
开发语言·前端·人工智能·python·安全·oracle