We know that developing Electron applications inevitably involves cross process communication. In the past, Electron built-in remote module, which greatly simplifies the development of cross process communication, but it also brings many problems. Please refer to my previous article for specific details:
https://www.cnblogs.com/liulun/p/15217180.html
After the Electron team removes the remote module, developers can only use ipcRenderer, ipcMain, webContents and other modules to send and receive cross process messages. This is no problem, but it is very troublesome to write. After there are many cross process messages, it is also difficult to manage and maintain. This makes us think about how to implement a unified cross process event component. Now I will introduce a method.
Firstly, this component integrates the events module of NodeJs and the event sending and receiving module of Electron, so these modules are introduced first
let events = require('events') let { ipcRenderer, ipcMain, webContents } = require('electron')
We assume that the class name of this component is Eventer. In the constructor of this class, we instantiate an EventEmitter object to listen to and emit events.
constructor() { this.instance = new events.EventEmitter() //this.instance.setMaxListeners(60) //Infinity this.initEventPipe() }
First, whether the rendering process or the main process uses this module, it will execute this constructor to create an EventEmitter object; However, the EventEmitter object of the rendering process is different from the EventEmitter object of the main process; The EventEmitter objects are also different between different rendering processes, but the EventEmitter objects in the same process are the same and share the same EventEmitter object. Here we use the singleton mode, which is realized through the following line of code:
export let eventer = new Eventer()
In other words, when a process imports this component for the first time, the eventer class is instantiated, and its constructor has been executed. No matter how many times the process imports this class, it all references the same eventer object, and this class will not be instantiated multiple times in the same process.
By default, the EventEmitter instance can register up to 10 listeners for any single event. If you think the number is too small, you can set the number to be larger through the setMaxListeners method. If it is set to Infinity, there is no limit on the number, but try not to do so, otherwise an event will be registered repeatedly and you don't know.
Next, we initialize our own cross process message pipeline in the initEventPipe method
private initEventPipe() { if (ipcRenderer) { ipcRenderer.on('__eventPipe', (e: Electron.IpcRendererEvent, { eventName, eventArgs }) => { this.instance.emit(eventName, e, eventArgs) }) } else if (ipcMain) { ipcMain.handle('__eventPipe', (e: Electron.IpcMainInvokeEvent, { eventName, eventArgs, broadcast }) => { this.instance.emit(eventName, e, eventArgs) if (!broadcast) return webContents.getAllWebContents().forEach((wc) => { if (wc.id != e.sender.id) { wc.send('__eventPipe', { eventName, eventArgs }) } }) }) } }
In this method, we judge whether the current process is the rendering process or the main process by whether ipcRenderer and ipcMain exist;
If it is a rendering process, use ipcRenderer to listen for a process named__ Message of eventPipe; If it is the main process, we listen to a process named__ Message for eventPipe.
No matter which process it is, the callback function for processing this message has two parameters. The first parameter is the message body provided by Electron for cross process messages, and the second parameter is constructed by ourselves (we will talk about it later). Their structures are the same and have eventName and eventArgs attributes;
In this callback function, we launch an event on the EventEmitter object of the current process. The name of the event is the value of the eventName attribute. The event has two parameters, one is the message body provided by Electron for cross process messages, and the other is the value corresponding to eventArgs.
If the current process is the main process, we will further judge whether there is a broadcast attribute. If so, we will continue to send it to all other webContents__ eventPipe message. The message body is composed of eventName and eventArgs attributes.
Here we pass e.sender ID to determine which rendering process sent the message from. When forwarding the message to other webContents, exclude the webContents of the message.
Next, let's look at a series of methods related to event emission
emitInProcess(eventName: string, eventArgs?: any) { this.instance.emit(eventName, eventArgs) }
This method emits events on the EventEmitter object of the current process. It's the simplest. I won't introduce it more.
emitCrossProcess(eventName: string, eventArgs?: any) { if (ipcMain) { webContents.getAllWebContents().forEach((wc) => { wc.send('__eventPipe', { eventName, eventArgs }) }) } else if (ipcRenderer) { ipcRenderer.invoke('__eventPipe', { eventName, eventArgs }) } }
This method sends a cross process message. If the rendering process calls this method, the message is sent to the main process. If the main process calls this method, the message is sent to all rendering processes.
The name of the message is__ eventPipe. The message body is an object composed of two parameters, eventName and EventArgs. The initEventPipe method we talked about earlier has logic to listen to this message.
emitToAllProcess(eventName: string, eventArgs?: any) { this.instance.emit(eventName, eventArgs) if (ipcMain) { webContents.getAllWebContents().forEach((wc) => { wc.send('__eventPipe', { eventName, eventArgs }) }) } else if (ipcRenderer) { ipcRenderer.invoke('__eventPipe', { eventName, eventArgs, broadcast: true }) } }
This method can send messages to all processes. First, it launches the eventName event on its own process, and then determines whether the current process is the main process or the rendering process. If it is the main process, it sends messages to all rendering processes. If it is the rendering process, it sends messages to the main process. When sending messages to the main process, it attaches the broadcast flag. The main process is required to forward messages to all other rendering processes.
emitToWebContents(wcIdOrWc: number | WebContents, eventName: string, eventArgs?: any) { if (ipcMain) { if (typeof wcIdOrWc == 'number') { webContents.getAllWebContents().forEach((wc) => { if (wc.id === wcIdOrWc) wc.send('__eventPipe', { eventName, eventArgs }) }) } else { wcIdOrWc.send('__eventPipe', { eventName, eventArgs }) } } else if (ipcRenderer) { ipcRenderer.sendTo(wcIdOrWc as number, '__eventPipe', { eventName, eventArgs }) } }
This method sends the message to the specified WebContents object. If the current process is the main process, find the WebContents object and call its send method to send the message; If the current process is a rendering process, it is sent to the target WebContents object using the sendTo method of ipcRenderer.
Next, there are several registration events and deregistration methods
on(eventName: string, callBack: (e: any, eventArgs: any) => void) { this.instance.on(eventName, callBack) } once(eventName: string, callBack: (e: any, eventArgs: any) => void) { this.instance.once(eventName, callBack) } off(eventName: string, callBack: (e: any, eventArgs: any) => void) { if (callBack) { this.instance.removeListener(eventName, callBack) } else { this.instance.removeAllListeners(eventName) } }
We won't explain these more.
Legacy problem: we can't send messages to the sub page iframe through this component
This component vividly reflects the sentence: leave simplicity and happiness to users; Leave the complexity and helplessness to yourself;