{T}

IPC 通信最佳实践

本文总结 Electron 进程间通信(IPC)的最佳实践,帮助你编写安全、高效、可维护的代码。

架构设计

分层架构

code
┌─────────────────────────────────────────────────────────────────────────┐
│                              应用层                                      │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                        渲染进程 UI 组件                             │  │
│  │  - React/Vue/Svelte 组件                                          │  │
│  │  - 调用 Service 层方法                                              │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                             Service 层                                   │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                     渲染进程 Service                                │  │
│  │  - 封装 window.electronAPI 调用                                    │  │
│  │  - 处理业务逻辑                                                     │  │
│  │  - 错误处理与重试                                                   │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │ contextBridge (preload)
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                              IPC 层                                      │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                     Preload + IPC Channel                          │  │
│  │  - 通道白名单验证                                                   │  │
│  │  - 参数验证                                                        │  │
│  │  - 数据序列化                                                       │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │ IPC 通信
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                             Handler 层                                   │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                     主进程 IPC Handler                             │  │
│  │  - 处理 IPC 请求                                                    │  │
│  │  - 调用底层服务                                                     │  │
│  │  - 返回结果/错误                                                    │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                             服务层                                       │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                     主进程服务 (Node.js)                            │  │
│  │  - 文件系统操作                                                     │  │
│  │  - 数据库访问                                                       │  │
│  │  - 原生 API 调用                                                    │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘

模块划分

code
IPC 通道命名规范:
┌─────────────┬────────────────────────────────────────────────────┐
│   模块前缀    │                    功能描述                         │
├─────────────┼────────────────────────────────────────────────────┤
│ dialog:*    │ 文件对话框、消息对话框等                              │
│ file:*      │ 文件读写、监听等                                     │
│ window:*    │ 窗口控制(最小化、最大化等)                          │
│ store:*     │ 本地存储操作                                         │
│ app:*       │ 应用级别操作(退出、重启等)                          │
│ notification:* │ 系统通知                                          │
│ menu:*      │ 菜单操作                                             │
│ shell:*     │ 系统集成(打开外部链接等)                            │
│ clipboard:* │ 剪贴板操作                                           │
└─────────────┴────────────────────────────────────────────────────┘

安全原则

1. 启用安全配置

javascript
// main.js
const win = new BrowserWindow({
  webPreferences: {
    // 必须启用的安全配置
    contextIsolation: true,    // 启用上下文隔离
    nodeIntegration: false,    // 禁用 Node.js 集成
    sandbox: true,             // 启用沙箱
    
    // 预加载脚本
    preload: path.join(__dirname, 'preload.js'),
    
    // 其他安全配置
    webSecurity: true,         // 启用同源策略
    allowRunningInsecureContent: false
  }
})

2. 通道白名单

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

// 定义允许的通道
const CHANNELS = {
  invoke: [
    'dialog:openFile',
    'dialog:saveFile',
    'file:read',
    'file:write',
    'store:get',
    'store:set'
  ],
  send: [
    'window:minimize',
    'window:maximize',
    'window:close'
  ],
  on: [
    'file:changed',
    'update:available'
  ]
}

// 验证函数
function validateChannel(type, channel) {
  if (!CHANNELS[type]?.includes(channel)) {
    throw new Error(`Invalid IPC channel: ${channel}`)
  }
}

contextBridge.exposeInMainWorld('electronAPI', {
  invoke: (channel, ...args) => {
    validateChannel('invoke', channel)
    return ipcRenderer.invoke(channel, ...args)
  },
  
  send: (channel, ...args) => {
    validateChannel('send', channel)
    ipcRenderer.send(channel, ...args)
  },
  
  on: (channel, callback) => {
    validateChannel('on', channel)
    const handler = (event, ...args) => callback(...args)
    ipcRenderer.on(channel, handler)
    return () => ipcRenderer.removeListener(channel, handler)
  }
})

3. 参数验证

javascript
// preload.js
const VALIDATORS = {
  path: (value) => {
    if (typeof value !== 'string') {
      throw new TypeError('Path must be a string')
    }
    if (value.includes('..')) {
      throw new Error('Path traversal not allowed')
    }
    if (value.length > 260) {
      throw new Error('Path too long')
    }
    return true
  },
  
  content: (value) => {
    if (typeof value !== 'string') {
      throw new TypeError('Content must be a string')
    }
    if (value.length > 10 * 1024 * 1024) { // 10MB
      throw new Error('Content too large')
    }
    return true
  },
  
  options: (value) => {
    if (value && typeof value !== 'object') {
      throw new TypeError('Options must be an object')
    }
    return true
  }
}

contextBridge.exposeInMainWorld('fileAPI', {
  read: (path) => {
    VALIDATORS.path(path)
    return ipcRenderer.invoke('file:read', path)
  },
  
  write: (path, content) => {
    VALIDATORS.path(path)
    VALIDATORS.content(content)
    return ipcRenderer.invoke('file:write', path, content)
  }
})

4. 主进程验证

javascript
// main.js
const path = require('path')
const fs = require('fs/promises')

// 允许访问的目录
const ALLOWED_DIRS = [
  app.getPath('documents'),
  app.getPath('downloads'),
  app.getPath('desktop')
]

function isPathAllowed(filePath) {
  const resolved = path.resolve(filePath)
  return ALLOWED_DIRS.some(dir => resolved.startsWith(dir))
}

ipcMain.handle('file:read', async (event, filePath) => {
  // 路径验证
  if (!isPathAllowed(filePath)) {
    throw new Error('Access denied: path not in allowed directories')
  }
  
  return fs.readFile(filePath, 'utf-8')
})

性能优化

1. 减少通信次数

javascript
// ❌ 多次 IPC 调用
for (const file of files) {
  const content = await window.api.invoke('file:read', file)
  processContent(content)
}

// ✅ 批量处理
const contents = await window.api.invoke('file:readMultiple', files)
contents.forEach(processContent)
javascript
// main.js - 批量处理
ipcMain.handle('file:readMultiple', async (event, files) => {
  return Promise.all(
    files.map(file => fs.promises.readFile(file, 'utf-8'))
  )
})

2. 数据精简

javascript
// ❌ 返回完整数据
ipcMain.handle('user:get', async (event, id) => {
  return await db.getUser(id) // 包含所有字段
})

// ✅ 只返回需要的数据
ipcMain.handle('user:getSummary', async (event, id) => {
  const user = await db.getUser(id)
  return {
    id: user.id,
    name: user.name,
    avatar: user.avatar
  }
})

3. 流式传输大文件

javascript
// main.js
ipcMain.handle('file:stream', async (event, filePath, chunkSize = 65536) => {
  const stream = fs.createReadStream(filePath, { highWaterMark: chunkSize })
  const sender = event.sender
  
  stream.on('data', (chunk) => {
    sender.send('file:chunk', {
      data: chunk.toString('base64'),
      size: chunk.length
    })
  })
  
  stream.on('end', () => {
    sender.send('file:end')
  })
  
  stream.on('error', (error) => {
    sender.send('file:error', { message: error.message })
  })
})
javascript
// preload.js
contextBridge.exposeInMainWorld('fileAPI', {
  stream: (path, callbacks) => {
    ipcRenderer.invoke('file:stream', path)
    
    const handlers = {
      'file:chunk': (event, data) => callbacks.onChunk?.(data),
      'file:end': () => {
        callbacks.onEnd?.()
        cleanup()
      },
      'file:error': (event, error) => {
        callbacks.onError?.(error)
        cleanup()
      }
    }
    
    const cleanup = () => {
      Object.entries(handlers).forEach(([channel, handler]) => {
        ipcRenderer.removeListener(channel, handler)
      })
    }
    
    Object.entries(handlers).forEach(([channel, handler]) => {
      ipcRenderer.on(channel, handler)
    })
    
    return cleanup
  }
})

4. 缓存策略

javascript
// renderer/services/cache.js
class IPCCache {
  constructor(ttl = 60000) {
    this.cache = new Map()
    this.ttl = ttl
  }
  
  async get(key, fetcher) {
    const cached = this.cache.get(key)
    if (cached && Date.now() - cached.timestamp < this.ttl) {
      return cached.value
    }
    
    const value = await fetcher()
    this.cache.set(key, { value, timestamp: Date.now() })
    return value
  }
  
  invalidate(key) {
    this.cache.delete(key)
  }
  
  clear() {
    this.cache.clear()
  }
}

export const ipcCache = new IPCCache()

// 使用
const config = await ipcCache.get('config', () => 
  window.api.invoke('config:get')
)

错误处理

1. 错误类型定义

javascript
// shared/errors.js
const ErrorCodes = {
  // 通用错误
  UNKNOWN: 'UNKNOWN',
  INVALID_PARAMS: 'INVALID_PARAMS',
  
  // 文件错误
  FILE_NOT_FOUND: 'FILE_NOT_FOUND',
  FILE_READ_ERROR: 'FILE_READ_ERROR',
  FILE_WRITE_ERROR: 'FILE_WRITE_ERROR',
  PERMISSION_DENIED: 'PERMISSION_DENIED',
  
  // 网络错误
  NETWORK_ERROR: 'NETWORK_ERROR',
  TIMEOUT: 'TIMEOUT',
  
  // 业务错误
  USER_NOT_FOUND: 'USER_NOT_FOUND',
  DUPLICATE_ENTRY: 'DUPLICATE_ENTRY'
}

class IPCError extends Error {
  constructor(code, message, details = {}) {
    super(message)
    this.name = 'IPCError'
    this.code = code
    this.details = details
  }
  
  toJSON() {
    return {
      name: this.name,
      code: this.code,
      message: this.message,
      details: this.details
    }
  }
  
  static fromJSON(json) {
    return new IPCError(json.code, json.message, json.details)
  }
}

module.exports = { ErrorCodes, IPCError }

2. 主进程错误处理

javascript
// main/ipc/file.js
const { ErrorCodes, IPCError } = require('../../shared/errors')

ipcMain.handle('file:read', async (event, filePath) => {
  try {
    // 参数验证
    if (!filePath || typeof filePath !== 'string') {
      throw new IPCError(ErrorCodes.INVALID_PARAMS, 'Invalid file path')
    }
    
    // 权限检查
    if (!isPathAllowed(filePath)) {
      throw new IPCError(ErrorCodes.PERMISSION_DENIED, 'Access denied')
    }
    
    // 读取文件
    const content = await fs.promises.readFile(filePath, 'utf-8')
    return { success: true, data: content }
    
  } catch (error) {
    if (error instanceof IPCError) {
      throw error
    }
    
    // 转换原生错误
    if (error.code === 'ENOENT') {
      throw new IPCError(ErrorCodes.FILE_NOT_FOUND, 'File not found', { path: filePath })
    }
    if (error.code === 'EACCES') {
      throw new IPCError(ErrorCodes.PERMISSION_DENIED, 'Permission denied', { path: filePath })
    }
    
    throw new IPCError(ErrorCodes.FILE_READ_ERROR, error.message, {
      originalError: error.message
    })
  }
})

3. 渲染进程错误处理

javascript
// renderer/services/file.js
import { ErrorCodes, IPCError } from '../../shared/errors'

class FileService {
  async readFile(path) {
    try {
      const result = await window.electronAPI.invoke('file:read', path)
      return result.data
    } catch (error) {
      const ipcError = error.code ? error : new IPCError(ErrorCodes.UNKNOWN, error.message)
      
      // 根据错误类型处理
      switch (ipcError.code) {
        case ErrorCodes.FILE_NOT_FOUND:
          this.showNotification('文件不存在')
          break
        case ErrorCodes.PERMISSION_DENIED:
          this.showNotification('没有权限访问该文件')
          break
        default:
          this.showNotification('读取文件失败')
      }
      
      throw ipcError
    }
  }
  
  showNotification(message) {
    // 显示通知
  }
}

export const fileService = new FileService()

4. 超时处理

javascript
// renderer/utils/ipc.js
export function invokeWithTimeout(channel, args, timeout = 5000) {
  return Promise.race([
    window.electronAPI.invoke(channel, args),
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('IPC_TIMEOUT')), timeout)
    )
  ])
}

// 带重试的调用
export async function invokeWithRetry(channel, args, options = {}) {
  const { maxRetries = 3, delay = 1000, timeout = 5000 } = options
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await invokeWithTimeout(channel, args, timeout)
    } catch (error) {
      if (error.message === 'IPC_TIMEOUT' && i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, delay))
        continue
      }
      throw error
    }
  }
}

架构模式

1. 服务类封装

javascript
// renderer/services/electron.js
class ElectronService {
  constructor() {
    this.api = window.electronAPI
  }
  
  // 文件操作
  async openFile(options = {}) {
    try {
      const result = await this.api.invoke('dialog:openFile', options)
      return result
    } catch (error) {
      console.error('Failed to open file:', error)
      throw error
    }
  }
  
  async saveFile(path, content) {
    return this.api.invoke('file:save', path, content)
  }
  
  // 事件订阅
  onFileChanged(callback) {
    return this.api.on('file:changed', callback)
  }
  
  // 窗口控制
  minimizeWindow() {
    this.api.send('window:minimize')
  }
}

export const electronService = new ElectronService()

2. 模块化 IPC 处理

javascript
// main/ipc/index.js
const { ipcMain } = require('electron')
const fileHandlers = require('./file')
const windowHandlers = require('./window')
const storeHandlers = require('./store')

function registerIPCHandlers() {
  // 注册所有处理器
  const handlers = [
    ...fileHandlers,
    ...windowHandlers,
    ...storeHandlers
  ]
  
  handlers.forEach(({ channel, handler, type = 'handle' }) => {
    if (type === 'handle') {
      ipcMain.handle(channel, handler)
    } else {
      ipcMain.on(channel, handler)
    }
  })
}

module.exports = { registerIPCHandlers }
javascript
// main/ipc/file.js
const fs = require('fs/promises')

const handlers = [
  {
    channel: 'file:read',
    handler: async (event, path) => {
      return fs.readFile(path, 'utf-8')
    }
  },
  {
    channel: 'file:write',
    handler: async (event, path, content) => {
      await fs.writeFile(path, content, 'utf-8')
      return true
    }
  }
]

module.exports = handlers

3. TypeScript 类型安全

typescript
// types/ipc.d.ts
export interface IPCChannels {
  invoke: {
    'file:read': (path: string) => Promise<string>
    'file:write': (path: string, content: string) => Promise<void>
    'dialog:openFile': (options?: OpenDialogOptions) => Promise<string[]>
  }
  send: {
    'window:minimize': () => void
    'window:maximize': () => void
    'window:close': () => void
  }
  on: {
    'file:changed': (callback: (path: string) => void) => () => void
    'update:available': (callback: (version: string) => void) => () => void
  }
}

type InvokeChannel = keyof IPCChannels['invoke']
type SendChannel = keyof IPCChannels['send']
type OnChannel = keyof IPCChannels['on']

declare global {
  interface Window {
    electronAPI: {
      invoke: <K extends InvokeChannel>(
        channel: K,
        ...args: Parameters<IPCChannels['invoke'][K]>
      ) => ReturnType<IPCChannels['invoke'][K]>
      
      send: <K extends SendChannel>(
        channel: K,
        ...args: Parameters<IPCChannels['send'][K]>
      ) => void
      
      on: <K extends OnChannel>(
        channel: K,
        callback: Parameters<IPCChannels['on'][K]>[0]
      ) => ReturnType<IPCChannels['on'][K]>
    }
  }
}

测试指南

1. 单元测试 IPC 处理器

javascript
// tests/ipc/file.test.js
const { ipcMain } = require('electron')
const fs = require('fs/promises')
const { registerFileHandlers } = require('../../main/ipc/file')

jest.mock('fs/promises')

describe('File IPC Handlers', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    registerFileHandlers()
  })
  
  describe('file:read', () => {
    it('should read file content', async () => {
      const mockContent = 'Hello World'
      fs.readFile.mockResolvedValue(mockContent)
      
      const handler = ipcMain.handle.mock.calls.find(
        call => call[0] === 'file:read'
      )[1]
      
      const result = await handler({}, '/path/to/file.txt')
      
      expect(result).toBe(mockContent)
      expect(fs.readFile).toHaveBeenCalledWith('/path/to/file.txt', 'utf-8')
    })
    
    it('should throw error for invalid path', async () => {
      const handler = ipcMain.handle.mock.calls.find(
        call => call[0] === 'file:read'
      )[1]
      
      await expect(handler({}, '')).rejects.toThrow('Invalid file path')
    })
  })
})

2. 测试 preload 脚本

javascript
// tests/preload.test.js
const { contextBridge, ipcRenderer } = require('electron')

// 模拟 electron
jest.mock('electron', () => ({
  contextBridge: {
    exposeInMainWorld: jest.fn()
  },
  ipcRenderer: {
    invoke: jest.fn(),
    send: jest.fn(),
    on: jest.fn()
  }
}))

describe('Preload Script', () => {
  beforeEach(() => {
    require('../../preload')
  })
  
  it('should expose electronAPI', () => {
    expect(contextBridge.exposeInMainWorld).toHaveBeenCalledWith(
      'electronAPI',
      expect.objectContaining({
        invoke: expect.any(Function),
        send: expect.any(Function),
        on: expect.any(Function)
      })
    )
  })
  
  it('should validate channels', () => {
    const { invoke } = contextBridge.exposeInMainWorld.mock.calls[0][1]
    
    expect(() => invoke('invalid:channel')).toThrow('Invalid IPC channel')
    expect(() => invoke('file:read')).not.toThrow()
  })
})

3. 集成测试

javascript
// tests/integration/ipc.test.js
const { Application } = require('spectron')
const path = require('path')

describe('IPC Integration Tests', () => {
  let app
  
  beforeEach(async () => {
    app = new Application({
      path: electronPath,
      args: [path.join(__dirname, '../../')]
    })
    await app.start()
  })
  
  afterEach(async () => {
    if (app && app.isRunning()) {
      await app.stop()
    }
  })
  
  it('should open file dialog', async () => {
    const result = await app.webContents.executeJavaScript(`
      window.electronAPI.invoke('dialog:openFile', { title: 'Test' })
    `)
    
    expect(result).toBeDefined()
  })
  
  it('should handle file read error', async () => {
    await expect(
      app.webContents.executeJavaScript(`
        window.electronAPI.invoke('file:read', '/nonexistent/file.txt')
      `)
    ).rejects.toThrow()
  })
})

调试技巧

1. IPC 日志中间件

javascript
// main/utils/ipcLogger.js
function createIPCLogger(options = {}) {
  const { logErrors = true, logTiming = true, logArgs = true } = options
  
  return (channel, handler) => {
    return async (event, ...args) => {
      const start = Date.now()
      
      if (logArgs) {
        console.log(`[IPC IN] ${channel}`, args)
      }
      
      try {
        const result = await handler(event, ...args)
        
        if (logTiming) {
          console.log(`[IPC OUT] ${channel} (${Date.now() - start}ms)`)
        }
        
        return result
      } catch (error) {
        if (logErrors) {
          console.error(`[IPC ERR] ${channel}`, error)
        }
        throw error
      }
    }
  }
}

// 使用
const ipcLogger = createIPCLogger()

ipcMain.handle('file:read', ipcLogger('file:read', async (event, path) => {
  return fs.promises.readFile(path, 'utf-8')
}))

2. 开发工具扩展

javascript
// preload.js (仅开发环境)
if (process.env.NODE_ENV === 'development') {
  contextBridge.exposeInMainWorld('devTools', {
    getChannels: () => Object.keys(VALID_CHANNELS),
    logIPC: (channel, direction, data) => {
      console.log(`[IPC ${direction}] ${channel}:`, data)
    },
    measureIPC: async (channel, ...args) => {
      const start = performance.now()
      const result = await ipcRenderer.invoke(channel, ...args)
      const duration = performance.now() - start
      console.log(`[IPC TIMING] ${channel}: ${duration.toFixed(2)}ms`)
      return result
    }
  })
}

3. Electron DevTools 扩展

javascript
// main.js
const { app, BrowserWindow } = require('electron')

async function installDevTools() {
  if (process.env.NODE_ENV === 'development') {
    const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer')
    
    await installExtension(REACT_DEVELOPER_TOOLS)
    
    // 安装 Electron DevTools 扩展
    // 可查看 IPC 通信
  }
}

app.whenReady().then(async () => {
  await installDevTools()
  createWindow()
})

完整模板

项目结构

code
project/
├── main/
│   ├── index.js              # 主进程入口
│   ├── ipc/
│   │   ├── index.js          # IPC 注册
│   │   ├── file.js           # 文件相关
│   │   ├── window.js         # 窗口相关
│   │   └── store.js          # 存储相关
│   └── services/
│       ├── fileService.js
│       └── storeService.js
├── preload/
│   └── index.js              # Preload 脚本
├── renderer/
│   ├── services/
│   │   ├── electronService.js
│   │   └── cache.js
│   └── utils/
│       └── ipc.js
├── shared/
│   └── errors.js             # 共享错误定义
├── types/
│   └── ipc.d.ts              # 类型定义
└── tests/
    ├── unit/
    └── integration/

preload/index.js (生产级模板)

javascript
const { contextBridge, ipcRenderer } = require('electron')

// ============ 通道定义 ============
const CHANNELS = {
  invoke: [
    'dialog:openFile',
    'dialog:saveFile',
    'file:read',
    'file:write',
    'store:get',
    'store:set',
    'store:delete'
  ],
  send: [
    'window:minimize',
    'window:maximize',
    'window:close'
  ],
  on: [
    'file:changed',
    'update:available'
  ]
}

// ============ 验证器 ============
const validators = {
  path: (value) => {
    if (typeof value !== 'string') throw new TypeError('Path must be string')
    if (value.includes('..')) throw new Error('Path traversal not allowed')
    return true
  },
  
  content: (value) => {
    if (typeof value !== 'string') throw new TypeError('Content must be string')
    return true
  }
}

// ============ 核心 API ============
contextBridge.exposeInMainWorld('electronAPI', {
  // 系统信息
  platform: process.platform,
  versions: {
    node: process.versions.node,
    chrome: process.versions.chrome,
    electron: process.versions.electron
  },
  
  // 文件操作
  file: {
    open: (options = {}) => ipcRenderer.invoke('dialog:openFile', options),
    save: (defaultPath) => ipcRenderer.invoke('dialog:saveFile', defaultPath),
    read: (path) => {
      validators.path(path)
      return ipcRenderer.invoke('file:read', path)
    },
    write: (path, content) => {
      validators.path(path)
      validators.content(content)
      return ipcRenderer.invoke('file:write', path, content)
    }
  },
  
  // 窗口控制
  window: {
    minimize: () => ipcRenderer.send('window:minimize'),
    maximize: () => ipcRenderer.send('window:maximize'),
    close: () => ipcRenderer.send('window:close')
  },
  
  // 存储
  store: {
    get: (key) => ipcRenderer.invoke('store:get', key),
    set: (key, value) => ipcRenderer.invoke('store:set', key, value),
    delete: (key) => ipcRenderer.invoke('store:delete', key)
  },
  
  // 事件订阅
  on: (channel, callback) => {
    if (!CHANNELS.on.includes(channel)) {
      throw new Error(`Invalid channel: ${channel}`)
    }
    const handler = (event, ...args) => callback(...args)
    ipcRenderer.on(channel, handler)
    return () => ipcRenderer.removeListener(channel, handler)
  }
})

常见问题解答

Q: 如何实现请求取消?

A: 使用 AbortController:

javascript
// renderer.js
async function fetchWithAbort(signal) {
  const result = await window.api.invoke('data:fetch', { signal })
  return result
}

const controller = new AbortController()
fetchWithAbort(controller.signal)
  .then(console.log)
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Request was cancelled')
    }
  })

// 取消
controller.abort()
javascript
// main.js
const pendingRequests = new Map()

ipcMain.handle('data:fetch', async (event, options) => {
  const requestId = Date.now()
  
  // 存储取消标志
  pendingRequests.set(requestId, false)
  
  try {
    for (let i = 0; i < 100; i++) {
      // 检查是否取消
      if (pendingRequests.get(requestId)) {
        throw new Error('AbortError')
      }
      await doSomething()
    }
    
    return result
  } finally {
    pendingRequests.delete(requestId)
  }
})

// 取消处理
ipcMain.on('request:cancel', (event, requestId) => {
  pendingRequests.set(requestId, true)
})

Q: 如何处理多个窗口的 IPC 通信?

A: 通过 event.sender 区分窗口:

javascript
// main.js
const windowHandlers = new Map()

ipcMain.handle('register:window', (event, windowId) => {
  windowHandlers.set(windowId, event.sender)
})

ipcMain.handle('send:to:window', (event, targetId, channel, data) => {
  const target = windowHandlers.get(targetId)
  if (target) {
    target.send(channel, data)
  }
})

// 广播到所有窗口
function broadcast(channel, data) {
  windowHandlers.forEach(webContents => {
    webContents.send(channel, data)
  })
}

Q: 如何实现 IPC 消息队列?

A: 使用队列管理器:

javascript
// renderer/utils/queue.js
class IPCQueue {
  constructor(concurrency = 1) {
    this.queue = []
    this.running = 0
    this.concurrency = concurrency
  }
  
  async add(channel, ...args) {
    return new Promise((resolve, reject) => {
      this.queue.push({
        channel,
        args,
        resolve,
        reject
      })
      this.process()
    })
  }
  
  async process() {
    if (this.running >= this.concurrency || this.queue.length === 0) {
      return
    }
    
    this.running++
    const { channel, args, resolve, reject } = this.queue.shift()
    
    try {
      const result = await window.api.invoke(channel, ...args)
      resolve(result)
    } catch (error) {
      reject(error)
    } finally {
      this.running--
      this.process()
    }
  }
}

export const ipcQueue = new IPCQueue(3)

Q: 如何监控 IPC 性能?

A: 实现性能监控:

javascript
// main/utils/performance.js
class IPCMonitor {
  constructor() {
    this.metrics = new Map()
  }
  
  record(channel, duration, success) {
    if (!this.metrics.has(channel)) {
      this.metrics.set(channel, {
        count: 0,
        totalTime: 0,
        errors: 0,
        maxTime: 0,
        minTime: Infinity
      })
    }
    
    const metric = this.metrics.get(channel)
    metric.count++
    metric.totalTime += duration
    metric.maxTime = Math.max(metric.maxTime, duration)
    metric.minTime = Math.min(metric.minTime, duration)
    if (!success) metric.errors++
  }
  
  getStats(channel) {
    const metric = this.metrics.get(channel)
    if (!metric) return null
    
    return {
      count: metric.count,
      avgTime: metric.totalTime / metric.count,
      maxTime: metric.maxTime,
      minTime: metric.minTime,
      errorRate: metric.errors / metric.count
    }
  }
  
  getAllStats() {
    const stats = {}
    this.metrics.forEach((_, channel) => {
      stats[channel] = this.getStats(channel)
    })
    return stats
  }
}

export const ipcMonitor = new IPCMonitor()

参考链接