Safely expose main process modules of Electron to renderer process through IPC.
npm install @lpfreelance/electron-bridgeelectron-bridge use Inter-Process Communication through
electron-bridge
├── demo/ # a demo to quickly test common modules.
├── cli/ # electron-bridge-cli package to generate schemas.
├── schemas/ # schemas of the Electron's main process modules to generate and expose.
└── src/ # source files generated from schemas.
`
Install
`shell script
$ npm install --save @lpfreelance/electron-bridge
`
Naming convention
Here is a brief description of the names used, so that we are on the same page:
| Path | File | Symbol | Description |
|-------------------:|:-----------------------|-------------------|-----------------------------------------|
| ./bridge/main/ | my-something.bridge.ts | MySomethingBridge | Class used in the main process. |
| ./bridge/preload/ | my-something.module.ts | MySomethingModule | Object used in the preload script. |
| ./bridge/renderer/ | my-something.api.ts | MySomethingApi | Interface used in the renderer process. |
| | | | |
| ./bridge/renderer/ | renderer.ts | Window | Augment Window with bridge's api. |
With this example, you must expect IPC channels to be named eb.mySomething..
Finally common bridges means exposed Electron's main features (e.g. nativeTheme, powerMonitor, etc.). It also
contains homebrew bridges for the benefit of all developers.
Bridges
This table shows you currently implemented bridges:
$3
| Bridge | Description |
|-----------------:|-----------------------------------------------------------------------------------|
| autoUpdater | cf Documentation |
| dialog | cf Documentation |
| nativeTheme | cf Documentation |
| powerMonitor | cf Documentation |
| powerSaveBlocker | cf Documentation |
| safeStorage | cf Documentation |
$3
| Bridge | Description |
|-----------:|---------------------------------------------------------------------------------------|
| fileSystem | Very simple wrapper for Node.js file system module. |
| path | Wrapper for Node.js path module. |
| store | JSON key/value storage solution using safeStorage to encrypt / decrypt data. |
You can see usage of each bridge in demo/.
Usage
$3
In your electron entry-point:
1. Instantiate a BridgeService.
2. Add bridges you which to use on the renderer process side.
3. Register all modules to actually listen to IPC handlers.
> electron.dev.ts
`typescript
import {app, BrowserWindow} from 'electron';
import {BridgeService, DialogBridge} from '@lpfreelance/electron-bridge/main';
let win: BrowserWindow;
let bridgeService: BridgeService;
app.enableSandbox();
const createWindow = () => {
win = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
nativeWindowOpen: true,
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
// ...
}
});
win.loadFile('index.html');
// Create bridge service.
bridgeService = new BridgeService();
// Append bridges that you need and register all IPC handlers.
bridgeService.add(new DialogBridge())
.registerAll();
win.on('closed', () => {
// Release bridges resources
bridgeService.releaseAll();
win = null;
bridgeService = null;
});
};
`
You can also add your own bridge (see Custom bridge).
Now that we have registered IPC handlers, we must declare a preload script.
$3
Using context bridge, it will allow us to expose bridges on the renderer process.
With PreloadService, we can expose only what we want to use:
> electron.preload.ts
`typescript
import {PreloadService} from '@lpfreelance/electron-bridge/preload';
const preloadService = new PreloadService();
preloadService.add('dialog')
.expose();
`
Here you add the modules matching the bridges you added in electron.dev.ts file.
You just need to use the camelCase name of the bridge, like in Electron documentation (e.g. nativeTheme, powerMonitor,
etc.).
You can also add your own module. This is further explained below (see Create a module).
You must call expose() in order to expose your modules using context bridge.
Now Electron needs to know that this script must be preloaded. When creating a BrowserWindow, we just need to set the
preload path of our script like below:
> electron.dev.ts
`typescript
import * as path from 'path';
// ...
win = new BrowserWindow({
// ...
webPreferences: {
// ...
preload: path.join(__dirname, 'electron.preload.js')
}
});
`
$3
In your renderer process, aka your application. You can now access exposed bridges like this:
> index.html
`html
electron-bridge-test
`
As you can see, you can simply use the native dialog module from your renderer process.
You'll notice that it reflects the same API exposed by Electron. This way, you can directly look at Electron's
documentation.
$3
You can now execute the following command to run electron:
`shell script
$ tsc electron.dev.ts electron.preload.ts && electron electron.dev.js
`
Custom bridge
You will now learn how to write your own bridge which is composed of three files.
You should first take a look to understand how things works together.
But ultimately you'll want to use a Schema thanks to electron-bridge-cli in cli/.
$3
> Side: main process
You must implement a Bridge interface in order to register it in the main process:
> src/bridge/main/app.bridge.ts
`typescript
import {App, ipcMain} from 'electron';
import {Bridge} from '@lpfreelance/electron-bridge/main';
export class AppBridge implements Bridge {
constructor(private app: App) {
}
public register(): void {
ipcMain.handle('eb.app.getVersion', (event: any) => {
return this.app.getVersion();
});
ipcMain.handle('eb.app.getName', (event: any) => {
return this.app.getName();
});
ipcMain.handle('eb.app.getPath', (event: any, path: string) => {
return this.app.getPath(path);
});
// allocate resources you'll need.
}
public release(): void {
ipcMain.removeHandler('eb.app.getVersion');
ipcMain.removeHandler('eb.app.getName');
ipcMain.removeHandler('eb.app.getPath');
// release resources you no longer need.
}
}
`
$3
> Side: preload script
Then, you must write your module using BridgeModule interface:
> src/bridge/preload/app.module.ts
`typescript
import {ipcRenderer} from 'electron';
import {BridgeModule} from '@lpfreelance/electron-bridge/preload';
export const AppModule: BridgeModule = {
name: 'app',
readonly: true,
api: {
getVersion: async () => {
return await ipcRenderer.invoke('eb.app.getVersion');
},
getName: async () => {
return await ipcRenderer.invoke('eb.app.getName');
},
getPath: async (path: string) => {
return await ipcRenderer.invoke('eb.app.getPath', path);
}
}
};
`
You can now import and add your custom module in the preload script:
> src/electron.preload.ts
`typescript
import {PreloadService} from '@lpfreelance/electron-bridge/preload';
import {AppModule} from './bridge/module/app.module.ts';
const preloadService = new PreloadService();
preloadService.add('dialog')
.add(AppModule)
.expose();
`
$3
> Side: your beloved IDE
Let's write an exported interface for our bridge:
> src/bridge/renderer/app.api.ts
`typescript
export interface AppApi {
getName(): Promise;
getVersion(): Promise;
getPath(path: string): Promise;
}
`
And augment the Window interface:
> src/bridge/renderer/renderer.ts
`typescript
import {AppApi} from './app.api';
declare global {
interface Window {
app: AppApi;
}
}
`
$3
Use your preferred bundler to bundle your application and target each Electron's process. You can find an example using
webpack with this repository configuration file: ./webpack.dev.js.
You should edit this configuration to match your project.
You can then bundle with:
`shell script
$ webpack --config webpack.dev.js
`
$3
Side: renderer process
We can now use our custom module:
> index.html
`html
`
Security
This library provides a pattern to quickly access main process features. You are still responsible regarding features
you expose to the renderer process (cf Context Isolation).
You can quickly implement a logic in your application using the fileSystem module and test it. But at the end, you
should create your own custom module and always check for bad behaviors.
You can see an example with the store bridge. When StoreBridge is created, you give an absolute path (parent)
where to store JSON files. You can then create multiple JSON files in this directory using store.withStore('my/path/db').
Without safety checks, calling store.withStore('/root/my-store') would allow any users to create a file outside
the given parent path.
In this implementation, attempting to resolve the store's path outside the given parent path will throw an error.
Schemas
A schema allows you to write a single file to describe a bridge between the main process and the renderer process.
You can then use electron-bridge-cli in cli/ to generate a bridge file, a module file and an api file.
Actually, this package uses schemas/ with electron-bridge-cli
to generate all files under src/`.