I'm updating an entire app written a few years ago using Electron v1.8.8. As far as I know, Electron changed its paradigm and now the default expected way of communicating between the main and renderer process is using a module in the middle named preload.js.
To get/set global variables in the old way you would first require the remote module:
var { remote } = require('electron');
And then getting/setting it like so:
remote.getGlobal('sharedObj').get('user')
I've been trying to expose an api object in preload.js to achieve the same old functionality, but with no success.
How I would achieve this without setting nodeIntegration: true, contextIsolation: false
?
CodePudding user response:
To send messages between the main thread and a render thread requires the understanding of Inter-Process Communication and Context Isolation.
You are correct in that preload.js
acts like a middle man. From my experience, using preload.js
as a script whose sole purpose is to communicate data between the main and render threads via the use of defined channels allows for a simple, easy to read, separation of concern. IE: Don't place domain specific functions within your preload.js
script. Only add functions that are used for the purpose of performing specific methods of communication.
Below is a typical preload.js
file. I have defined a couple of channels to communicate between your main thread and your render thread. PS: You can use any name / naming convention to name your channels.
preload.js
(main thread)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [
'renderThread:saysHi'
],
// From main to render.
'receive': [
'mainThread:saysHi'
],
// 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);
}
}
}
);
Whilst your main.js
file may look a little different than that shown below, the main points of interest are the reception and transmission of the defined channels renderThread:saysHi
and mainThread:saysHi
.
main.js
(main thread)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require("path");
let appWindow;
function createAppWindow() {
const appWindow = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
fullscreen: false,
resizable: true,
movable: true,
minimizable: true,
maximizable: true,
enableLargerThanScreen: true,
closable: true,
focusable: true,
fullscreenable: true,
frame: true,
hasShadow: true,
backgroundColor: '#fff',
show: false,
icon: nodePath.join(__dirname, 'icon.png'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
worldSafeExecuteJavaScript: true,
enableRemoteModule: false,
devTools: (! electronApp.isPackaged),
preload: nodePath.join(__dirname, 'preload.js')
}
});
appWindow.loadFile('index.html')
.then(() => {
// Main thread saying hi to the render thread.
appWindow.webContents.send('mainThread:saysHi', 'Hello from the main thread.'); })
.then(() => {
appWindow.show(); })
return appWindow;
}
// Listen for the render thread saying hi.
electronIpcMain.on('renderThread:saysHi', (event, message) => {
console.log(message); });
}
electronApp.on('ready', () => {
appWindow = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
Your index.html
file will import your Javascript file which would contain the code to listen out and send messages on the specified channels.
index.html
(render thread)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="main-thread-message"></div>
<input type="button" id="render-thread-button" value="Click to say hi to the main thread">
</body>
<script type="module" src="index.js"></script>
</html>
index.js
(render thread)
let mainThreadMessage = document.getElementById('main-thread-message');
let renderThreadButton = document.getElementById('render-thread-button');
// IIFE - Immediately Invoke Function Expression
(function() {
window.ipcRender.receive('mainThread:saysHi', (message) => {
mainThreadMessage.textContent = message;
});
renderThreadButton.addEventLister('click', () => {
window.ipcRender.send('renderThread:saysHi', 'Hello from the render thread.');
});
})();
In the above code, the global object window
contains ipcRender.send
and ipcRender.receive
which is the structure used in your preload.js
script underneath the line contextBridge.exposeInMainWorld
. You can rename ipcRender
and receive
/ send
/ invoke
to anything you like, though if you do, you would also use the same reference when using it 'html js' side.