@mrtd/electron-ipcb

A small library to help handle IPC with electron and allow a better architecture.

Usage no npm install needed!

<script type="module">
  import mrtdElectronIpcb from 'https://cdn.skypack.dev/@mrtd/electron-ipcb';
</script>

README

electron-ipcb

A small library to help handle IPC with electron and allow a better architecture. I'm using it with React but feel free to try anything else.

Why ?

I love code. Unless I have to repeat myself for hours, which has been the case with electron and its IPC.

The way to communicate between the main and renderer processes is always the same : we have listeners on both side to receive and send requests, everything barely change on every screen but we have to re-write everything.

The purpose of this library is simple :

  • Write the least code possible
  • Help to work with a good architecture
  • Easy maintainability / readability
  • Still allow some freedom

And now ... How ?

The library is centered around 3 main modules :

  • IpcWindow
  • IpcChannel
  • IpcChannelBinding

The aim of IpcWindow is to allow a class to extends it, feed it with Objects build by the two others class so it create and bind every channel without writing anything more. Here you can see an example of a MainWindow :

/windows/main/MainWindow.ts


import  *  as  path  from  'path';
import  *  as  url  from  'url';

import { BrowserWindow, BrowserWindowConstructorOptions } from  'electron';
import  IpcWindow  from  '@mrtd/electron-ipcb';
import  MainBindings  from  './MainBindings'; 

const  options: BrowserWindowConstructorOptions = { width:  1100, height:  700, backgroundColor:  '#191622', webPreferences: { nodeIntegration:  true } };  

class  MainWindow  extends  IpcWindow {
  window: BrowserWindow | undefined;
  
  constructor() {
    super(options, MainBindings);
  }
 
  create() {
    super.create();
    const  window = this.window  as  BrowserWindow;
    
    if (process.env.NODE_ENV === 'development') window.loadURL('http://localhost:4000');
    else window.loadURL(url.format({ pathname:  path.join(__dirname, 'renderer/index.html'), protocol:  'file:', slashes:  true }));
    
    window.on('closed', () => {
    this.window = undefined;
    });
  }
}

export  default  MainWindow;

The window (BrowserWindow) is not instanciated before the create function is called. You might load some content differently but otherwise this is classic Electron. The only missing part here is MainBindings.

MainBindings is built on top of IpcChannel and IpcChannelBinding.

The aim of IpcChannel is to define the structure of the channel : its types and action. In my following examples i'll use systeminformation, a really nice library to get data about your computer.

If i was to create a definition channel with IpcChannel, to query the CPU data (for my MainWindow) :

/windows/main/MainChannels.ts

import { Systeminformation } from  'systeminformation';
import  IpcChannel  from  '@mrtd/electron-ipcb';

export  const  MainCpuChannel = new  IpcChannel<[], [Systeminformation.CpuData]>('system:cpu');

An IpcChannel expect you to define the types of arguments (for the request made by a renderer), the type of the response, sent by the main process and also received by the renderer. Otherwise it only take one parameter, the action name used to dispatch events.

From this object build with IpcChannel, we can create our IpcChannelBinding that will be fed to our IpcWindow and everything will be configured. Lets dive into it !

/windows/main/MainBindings.ts

import { cpu, Systeminformation } from  'systeminformation';
import  IpcChannelBinding  from  '@mrtd/electron-ipcb';
import { MainCpuChannel, MainTimeChannel } from  './MainChannels';

const  MainCpuBinding = new  IpcChannelBinding<[], [Systeminformation.CpuData]>(
  MainCpuChannel,
  (window, e, ...args) =>
    new  Promise<[Systeminformation.CpuData]>((resolve) => {
      cpu((data) =>  resolve([data]));
    })
  );
  
export  default [MainCpuBinding];

Here we create a IpcChannnelBinding by definining its arguments / response type the same way as IpcChannel. It expect as argument :

  • The corresponding channel
  • A handler that will be called every time a renderer process request it.

The expected handler receive the given window, the dispatched event and every arguments that was sent by the renderer. This handler is a function that must return any value or a Promise.

I then choose to export a list containing all ChannelBindings of my main windows here. Now, everything comes together and all the electron part is working as expected, feel free to dive into the source code or documentation if need be.

Renderer process

In the renderer process, we need to use the "Channel" we built (MainCpuChannel). We only have two methods :

  • onRenderer allows to define the callback when we receive a response or error from the main process.
  • request to start a request to the main process on the chosen channel.
MainCpuChannel.onRenderer(
  (event, cpu /* , others response parameters if needed */) => {
    console.log(cpu);
  },
  (event, error) => { // This is the way to handle error thrown in the ChannelBinding handler
    console.error(error);
  }
);  

MainCpuChannel.request(/* ...args if needed */);

As I work with React I define those in the componentDidMount method.