Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Electron Preload vs Electron Main

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
  },
});
like image 490
kzaiwo Avatar asked Jan 26 '26 14:01

kzaiwo


1 Answers

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.

like image 90
midnight-coding Avatar answered Jan 28 '26 08:01

midnight-coding



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!