研究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')

View Code

不过只能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实现。

 

本文为原创文章,转载请保留原出处,方便溯源,如有错误地方,谢谢指正。

本文地址 :http://www.cnblogs.com/lovesong/p/11180336.html