系列篇1: Electron 通信方式总结
背景和前言
Electron底层是基于浏览器内核实现的,所以它和浏览器一样有多进程的概念,就是有一个主进程和N多个渲染进程,那么我们在开发过程中如何实现这些渲染进程之间的通信,是我们后续开发过程中必须要解决的问题
接下来我会输出一系列的文章和解决方案,欢迎各位开发和读者订阅和指点
Electron通信方式
先看下Demo默认配置
.main.ts
mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: false,
},
})
preload.ts
import { ipcRenderer } from 'electron'
window.ipcRenderer = ipcRenderer;
模式 1:渲染器进程到主进程(单向)ipcRenderer / ipcMain
render.ts
// 发送事件
const sendMessageToMain = () => {
window.ipcRenderer.send('messageToMain', 'hello main');
}
main.ts
// 监听事件
ipcMain.on('messageToMain', (event, arg) => {
console.log('messageToMain 主进程收到的消息:', arg, event.sender.id)
});
结论: 这种方式主要用于总渲染进程给主进程发消息,并且不支持消息回调
模式 2:主进程到渲染器进程 ipcRenderer / ipcMain
main.ts
// 发送事件
mainWindow?.webContents.send('mainSendMessageToRanderer', '主进程广播的消息', arg, event.sender.id)
mainWindow?.webContents 这个是接受消息的主体即渲染在任意window上的渲染进程主题
render.ts
// 监听事件
window.ipcRenderer.on('mainSendMessageToRanderer', (event, arg) => {
console.log('从主进程收到的消息:', arg, event);
})
结论: 模式1和模式2只是单项发送数据和监听事件不支持,监听回调
模式 3:渲染器进程到主进程(双向) ipcRenderer / ipcMain
render.ts
// 发送事件
const sendMessageToMainCallback = async () => {
window.ipcRenderer.invoke('messageToMainCallback', '从渲染进程发送的消息').then(response => {
console.log('从主进程收到的响应:', response);
}).catch(err => {
console.error('从主进程收到的错误:', err);
})
}
main.ts
// 监听事件并且返回
ipcMain.handle('messageToMainCallback', async (event, arg) => {
console.log('messageToMainCallback 主进程收到的消息:', arg, event.sender.id)
const sum = await new Promise((resolve) => {
let total = 0;
for (let i = 1; i <= 99; i++) {
total += i;
}
resolve(total);
});
return sum;
});
结论: 前三种模式我们看到,消息通信是主进程和渲染进程之间的通信,那如果我们想实现渲染进程和渲染进程通信我们怎么办啊?
很遗憾,Electron官方并没有直接的Api支持这种模式,它不允许渲染进程之间相互通信
模式 4:渲染器进程到主进程(双向)MessageChannel 基于 port
注意: 要使用这种模式下面的属性必须打开,否则将导致收不到从主进程发送的消息,导致 port1 是空的
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
render.ts
const sendMessageToMainFromPort = () => {
const { port1, port2 } = new MessageChannel();
window.ipcRenderer.postMessage('port', null, [port1]);
if (port2) {
port2.onmessage = (event: { data: any }) => {
console.log('从主进程收到的消息:', event.data);
};
port2.postMessage({ message: 'hello main' });
}
}
main.ts
// 监听port消息
ipcMain.on('port', (event) => {
const port = event.ports[0]
console.log('主进程收到的port:', port)
port.on('message', (event) => {
const data = event.data
console.log('主进程发送的消息:', data)
port.postMessage(data)
})
port.start()
})
不打开也可以但是需要做中转
preload.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer))
contextBridge.exposeInMainWorld('ipcRendererCustom', {
createMessageChannelPort: () => {
const { port1, port2 } = new MessageChannel()
ipcRenderer.postMessage('port', '', [port1]);
return {
postMessage: (message: object) => port2.postMessage(message),
onmessage: (listener: ((this: MessagePort, ev: MessageEvent<any>) => any) | null) => port2.onmessage = listener,
};
},
});
reder.ts
// 发送消息
const sendMessageToMainFromPort = () => {
const port = window.ipcRendererCustom.createMessageChannelPort();
port.onmessage((event) => {
console.log('从主进程收到的消息:', event.data);
});
port.postMessage({ message: 'hello main' });
}
main.ts
// 监听port消息
ipcMain.on('port', (event) => {
const port = event.ports[0]
port.on('message', (event) => {
const data = event.data
console.log('主进程发送的消息:', data)
port.postMessage(data)
})
port.start()
})
模式 5:渲染器进程到主进程(双向)MessageChannel 基于 port
render.ts 让渲染进程触发事件
// 发送消息, 让渲染进程发消息
const sendMessageToMainFromMainPort = () => {
window.ipcRenderer.send('request-main-channel')
window.ipcRenderer.once('provide-main-channel', (event) => {
const [port] = event.ports
console.log('port:', port)
port.onmessage = (event) => {
console.log('received result:', event.data)
}
port.postMessage({ message: 'hello main' })
})
}
main.ts
// 在主进程中, 监听消息并且把端口发送给渲染进程
ipcMain.on('request-main-channel', (event) => {
const { port1, port2 } = new MessageChannelMain();
port2.on('message', (event) => {
console.log('主进程收到的消息:', event.data);
port2.postMessage('这是测试数据');
});
port2.start();
event.sender.postMessage('provide-main-channel', null, [port1]);
});
结论: 基于端口的消息专递我们看到是实现点对点的数据传递,他还是需要借助主进程去完成消息转发
模式 6:渲染器进程到主进程(双向)worker渲染进程基于 port
其实如果我们不想让主进程承担太多的逻辑, 那可以新启动一个后台渲染进程,把事件都挂在在这个后台渲染进程中
main.ts
在创建主window的地方,同时创建后台渲染进程
async function createWindow() {
workerWindow = new BrowserWindow({
show: false,
webPreferences: {
preload: path.join(__dirname, 'workerProload.js'),
nodeIntegration: true,
contextIsolation: false,
}
})
const workerPath = path.join(__dirname, `../src/renderer/worker.html`)
await workerWindow.loadFile(workerPath);
// 创建主窗口
}
workerProload.ts
import { ipcRenderer } from 'electron'
ipcRenderer.on('new-client', (event) => {
const [port] = event.ports
port.onmessage = (event) => {
console.log('worker: 接受数据: ', event.data)
port.postMessage('从worker返回的数据给其他渲染进程')
}
port.start()
})
render.ts
const sendMessageToWorkerFromMainPort = () => {
window.ipcRenderer.send('request-worker-channel')
window.ipcRenderer.once('provide-worker-channel', (event) => {
const [port] = event.ports
console.log('port:', port)
port.onmessage = (event) => {
console.log('received result:', event.data)
}
port.postMessage('hello worker')
})
}
main.ts
// 给我另一个渲染进程发
ipcMain.on('request-worker-channel', (event) => {
console.log('workerWindow: event.sender: ', event)
const { port1, port2 } = new MessageChannelMain()
workerWindow?.webContents.postMessage('new-client', null, [port2])
event.sender.postMessage('provide-worker-channel', null, [port1])
})
小结
基于上面的通信模式如果仅仅是在项目比较简单的项目中实现通信已经足够了, 但是如果你的项目很复杂,你觉得你会把越来越多的业务通信放到主进程吗,显然不合理,我们需要自己开发一个优秀的框架,能够实现渲染进程和渲染进程之间的通信而不再自己单独维护一套逻辑
关于自己实现通信框架,后续文章再说, 我们还有一个问题需要处理: Electron开发中不可能没有Web界面嵌入, 那我们如何实现Web界面和Electron通信将是我们需要解决的第二个问题,下一篇文章 我将详细介绍