研究Electron主进程、渲染进程、webview之间的通讯
背景
由于某个Electron应用,需要主进程、渲染进程、webview之间能够互相通讯。
不过因为Electron仅提供了主进程与渲染进程的通讯,没有渲染进程之间或渲染进程与webview之间通讯的办法,所以只能寻找其他方案来解决。
研究一:ipcMain/ipcRenderer
Electron主进程与渲染进程的通讯,就是用ipcMain/ipcRenderer这两个对象。
// 在主进程中. const { ipcMain } = require('electron') ipcMain.on('asynchronous-message', (event, arg) => { console.log(arg) // prints "ping" event.reply('asynchronous-reply', 'pong') }) ipcMain.on('synchronous-message', (event, arg) => { console.log(arg) // prints "ping" event.returnValue = 'pong' }) //在渲染器进程 (网页) 中。 const { ipcRenderer } = require('electron') console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong" ipcRenderer.on('asynchronous-reply', (event, arg) => { console.log(arg) // prints "pong" }) ipcRenderer.send('asynchronous-message', 'ping')
不过只能ipcRenderer主动发送消息,ipcMain无法主动发送消息给ipcRenderer。
主进程如何主动发送消息给渲染进程?
如果渲染进程的窗口是用BrowserWindow打开的,那么可以通过webContents.send主动向窗口发送消息。
let win = new BrowserWindow({ width: 800, height: 600 }) win.loadURL(`file://${__dirname}/index.html`) win.webContents.on('did-finish-load', () => { win.webContents.send('ping', 'whoooooooh!') })
那么如果想主进程主动向渲染进程发送消息,就可以将创建BrowserWindow的逻辑放在主进程里,所有实例都在主进程里维护,那么主动发消息的问题也就解决了。
渲染进程之间如何进行消息通讯?
Electron虽然没有提供渲染进程之间的通讯,但可以通过主进程中转来达到这个目的。
步骤:
1、ipcRenderer.send消息到主进程。
2、主进程接收到消息,再通过维护的BrowserWindow实例,轮询webContents.send给各个窗口。
3、渲染进程触发订阅主进程事件。
渲染进程与webview之间如何通讯?
由于被打开渲染窗口中,会使用到webview标签(类似iframe)嵌入页面,所以这里也需要互相通讯。
webview是一个标签,它有一个ipc-message事件接收渲染进程的消息,如下。
// In embedder page. const webview = document.querySelector('webview') webview.addEventListener('ipc-message', (event) => { console.log(event.channel) // Prints "pong" }) webview.send('ping’) //在访客页。 const { ipcRenderer } = require('electron') ipcRenderer.on('ping', () => { ipcRenderer.sendToHost('pong') })
必须明确一点的是,上面代码中webview监听ipc-message事件的代码是写在渲染进程中的,不是在webview自己页面代码里。这就有一个很尴尬的问题,事件是有了,但webview页面里并不知道。
经过几番尝试,确实无法在嵌入页面接收到事件。
结论
在Electron提供的功能里,只能做到主进程和渲染进程的互相通信,webview像个弃子一样被隔离开了。
研究二:c++插件
上一个方案走不通后,我又想到是否可以做一个c++插件来实现。
PS:http://nodejs.cn/api/addons.html
c++插件实现思路:
1、在插件里定义两个方法,一个listen(订阅事件),一个trigger(发布事件)。
2、在listen里,将订阅事件的上下文(Local<Context>)、事件名称、回调保存下来。
3、在trigger里,遍历保存的订阅信息,匹配事件名称,然后调用订阅信息中的回调。
这里关键的思想就是,在插件有个全局变量来保存各个进程的订阅信息,所有的进程都使用同一个实例对象(单例)。
但是在require插件时候,我发现每个进程都是各自一个实例,不是单例,做不到共享全局变量。
结论
因为require插件的实例不是单例,所以此方案也夭折了。
研究三:socket
在上面方法验证走不通后,最后选择socket方式来中转消息。
PS:https://www.npmjs.com/package/ws
//in main process const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8888 }); wss.on('connection', function connection(ws) { ws.on('message', function incoming(data) { wss.clients.forEach(function each(client) { if (client.readyState === WebSocket.OPEN) { client.send(data); } }); }); }); //in render process or webview const WebSocket = require('ws'); const busClient = new WebSocket('ws://127.0.0.1:' + busPort + '/'); busClient.on('message', function incoming(data) { console.log(data) }); busClient.send('hello world’);
事件订阅与发布就可以基于上面代码实现。
//in render process or webview var busEvents = {}; const WebSocket = require('ws'); const busClient = new WebSocket('ws://127.0.0.1:' + busPort + '/'); busClient.on('message', function incoming(data) { data = JSON.parse(data); if(busEvents[data.eventName]){ busEvents[data.eventName].apply(this, data.args); } }); function listen(eventName, func) { busEvents[eventName] = func; } function trigger(eventName, args) { busClient.send(JSON.stringify({ eventName, args })) }
总结
Electron主进程、渲染进程、webview之间的通讯,只能通过socket实现。
本文为原创文章,转载请保留原出处,方便溯源,如有错误地方,谢谢指正。