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 = handlers3. 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()