javascripthtmlnode.jselectronipcrenderer

Electron: Simplest example to pass variable from JS to HTML, using IPC, Contextbridge and Preload?


I am trying to learn electron, but I'm struggling to understand this basic functionality of IPC. Already tried from documentation or tutorials but everytime they overcomplicate with complex examples or using other libaries like svelte, typescript etc. Also I don't want to show in console, I want to display directly in HTML page. I know these are basics, but any help is appreciated. So far I understood that I need:

main.js

const { app, BrowserWindow, ipcMain } = require("electron");
const path = require("path");

app.whenReady().then(main);

let window;

async function main() {
    window = new BrowserWindow({
        width: 100, 
        height: 100,
        webPreferences: {
            preload: path.join(__dirname + "./preload.js")
        },
    })
    window.on("ready-to-show", window.show);
    window.loadFile(path.join(__dirname, "./index.html"));
}

ipcMain.on("GetMyVar", (event, args) => {
    *???*
})

preload.js

const { ipcRenderer, contextBridge } = require("electron");

const API = {
    window: {
        GetMyVar: () => ipcRenderer.send("GetMyVar", MyVar)  *???*
    },
}
contextBridge.exposeInMainWorld("app", API);

renderer.js

const MyVar = "JUSTSHOWUP!";

index.html

<html>
     <body>
          <h1 id='MyVar'>???</h1>
     </body>
</html>

Thank you!


Solution

  • Electron Inter-Process Communication is a difficult subject to grasp without easy to understand examples.

    Your preload.js script can be written in a number of different ways. In the below code example, I have included two ways to do this.

    preload-1.js uses a simplified, easy to understand direct implementation method. That said, for ease of maintainability and debugging, as your Electron application grows, you will want to split your preload.js script up into smaller individual preload scripts. Bearing in mind that you can only load one preload script per window instance, you may have a need to repeat code between preload scripts.

    preload-2.js uses a more flexible white listed channel naming system where you only need to load the one preload script across all created windows. As a result, your preload script only performs a single function, to communicate between your main process and render process(es). Implementation of the sent and received channel names are kept within your specific code domains.


    main.js (main process)

    No matter which type of preload script you use, implementation will be the same within your main.js file.

    const electronApp = require('electron').app;
    const electronBrowserWindow = require('electron').BrowserWindow;
    const electronIpcMain = require('electron').ipcMain;
    
    const nodePath = require('path');
    
    // Prevent garbage collection.
    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-1.js')
                // preload: nodePath.join(__dirname, 'preload-2.js')
            }
        });
    
        window.loadFile('index.html')
            .then(() => { window.webContents.send('messageFromMain', 'Message from main..!'); })
            .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();
        }
    });
    
    // ---
    
    electronIpcMain.on('messageToMain', (event, message) => {
        console.log('Message to main: ' + message);
    })
    

    preload-1.js (main process)

    // Import the necessary Electron components
    const contextBridge = require('electron').contextBridge;
    const ipcRenderer = require('electron').ipcRenderer;
    
    // Exposed protected methods in the render process
    contextBridge.exposeInMainWorld(
        // Allowed 'ipcRenderer' methods
        'ipcRender', {
            // From render to main
            messageToMain: (message) => {
                ipcRenderer.send('messageToMain', message)
            },
            // From main to render
            messageFromMain: (message) => {
                ipcRenderer.on('messageFromMain', message)
            }
        });
    

    preload-2.js (main process)

    At the end of this preload.js script, I have included a section on how to use it within your main process and render process(es).

    // 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': [
                'messageToMain'
            ],
            // From main to render
            'receive': [
                'messageFromMain'
            ],
            // 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);
                }
            }
        }
    );
    
    /**
     * 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; })
     *          });
     */
    

    index.html (render process)

    To test which form of preload.js script you prefer, just comment out one whilst uncommenting the other.

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Electron IPC Test</title>
        </head>
    
        <body>
            <div>
                <label for="messageFromMain">Message from Main Process: </label>
                <input type="text" id="messageFromMain" disabled>
            </div>
    
            <hr>
    
            <div>
                <label for="messageToMain">Message to Main Process: </label>
                <input type="text" id="messageToMain">
                <input type="button" id="send" value="Send">
            </div>
    
        </body>
    
        <script>
            let messageFromMain = document.getElementById('messageFromMain');
            let messageToMain = document.getElementById('messageToMain');
    
            // Message from main (preload-1.js)
            window.ipcRender.messageFromMain((event, message) => {
                messageFromMain.value = message;
            })
    
            // Message from main (preload-2.js)
            // window.ipcRender.receive('messageFromMain', (message) => {
            //     messageFromMain.value = message;
            // })
    
            // Message to main
            document.getElementById('send').addEventListener('click', () => {
                // (preload-1.js)
                window.ipcRender.messageToMain(messageToMain.value);
    
                // (preload-2.js)
                // window.ipcRender.send('messageToMain', messageToMain.value);
            })
        </script>
    </html>