I am currently using Electron v16. Apparently on this version, we cannot use built-in Node modules in the renderer thread anymore.
The only way to do it is by using electron-preload.js.
I've read this resource:
https://whoisryosuke.com/blog/2022/using-nodejs-apis-in-electron-with-react/
where the author placed the implementation code in electron-main.js by utilizing the ipcMain and the code is invoked through electron-preload.js contextBridge.
My question is:
Is there any difference if put the entire implementation code in electron-preload instead of having the need to invoke an event from here and send it to ipcMain?
Here is the code from the author:
electron-preload.js:
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electron', {
blenderVersion: async (blenderPath) =>
ipcRenderer.invoke('blender:version', blenderPath),
},
});
electron-main.js
ipcMain.handle('blender:version', async (_, args) => {
console.log('running cli', _, args)
let result
if (args) {
const blenderExecutable = checkMacBlender(args)
// If MacOS, we need to change path to make executable
const checkVersionCommand = `${blenderExecutable} -v`
result = execSync(checkVersionCommand).toString()
}
return result
})
I am thinking if doing something like this instead is acceptable (pros/cons?):
electron-preload.js:
const { contextBridge } = require('electron');
contextBridge.exposeInMainWorld('electron', {
blenderVersion: async (blenderPath) =>
console.log('running cli', _, args)
let result
if (args) {
const blenderExecutable = checkMacBlender(args)
// If MacOS, we need to change path to make executable
const checkVersionCommand = `${blenderExecutable} -v`
result = execSync(checkVersionCommand).toString()
}
return result
},
});
Functionally, there is no difference between the two at all...
That said, separating the calling of your code from its implementation goes a long way in "Separate your Concerns". If you have a lot of calls from render to main (or vice versa), your preload.js script will become huge. Trying to manage a file of that size, especially if you are using typescript in your Electron application can become problematic. The overhead of such declarations will grow and grow.
I take an even more thorough, but in my mind, simplified approach...
I use the preload.js script purely as a place to declare "channel names" and implement the communication between the main process and the render process(es) via the use of pure IPC handles such as:
ipcRenderer.send(channel, ...args)
ipcRenderer.on(channel, listener)
ipcRenderer.invoke(channel, ...args).
Having such a simple script means I only need to manage one preload.js script and I use it in every window I ever create.
This is my preload.js script. To use, just add your "channel names" to whatever whitelisted array you need to. EG: In this instance, I have added the channel name test:channel for use only between the main to render process.
preload.js (main process)
// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [],
// From main to render.
'receive': [
'test:channel' // Our channel name
],
// From render to main and back again.
'sendReceive': []
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
In my main.js script, just IPC with the channel name and any associated data, if any.
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const nodePath = require("path");
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.webContents.send('test:channel', 'Hello from the main thread') })
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
Finally, in your render just listen for the channel name and implement functionality when called.
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Electron Test</title>
</head>
<body>
<div id="test"></div> // Outputs "Hello from the main thread"
</body>
<script>
window.ipcRender.receive('test:channel', (text) => {
document.getElementById('test').innerText = text;
})
</script>
</html>
To use this preload.js scipt in the main and render processes, the following implementation would apply.
/**
* Render --> Main
* ---------------
* Render: window.ipcRender.send('channel', data); // Data is optional.
* Main: electronIpcMain.on('channel', (event, data) => { methodName(data); })
*
* Main --> Render
* ---------------
* Main: windowName.webContents.send('channel', data); // Data is optional.
* Render: window.ipcRender.receive('channel', (data) => { methodName(data); });
*
* Render --> Main (Value) --> Render
* ----------------------------------
* Render: window.ipcRender.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', (event, data) => { return someMethod(data); });
*
* Render --> Main (Promise) --> Render
* ------------------------------------
* Render: window.ipcRender.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', async (event, data) => {
* return await promiseName(data)
* .then(() => { return result; })
* });
*/
Remember, you can't return functions, promises or other objects that can't be serialised with the structured clone algorithm. See Object serialization for more information.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With