一、为什么需要理解进程模型?
很多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