ryno
ryno

Reputation: 966

Using the electron ipcRenderer from a front-end javascript file

I'm in the process of learning to use Electron, and while trying to have my application communicate with the front end I am aware I need to use the ipcRenderer to gain a reference to the DOM elements and then pass that information to ipcMain.

I tried to follow much of the advice suggested here and here, but both of these examples use require('electron').ipcMain and whenever I try to include my script that will be interacting with the front-end into my HTML, nothing occurs since Uncaught ReferenceError: require is not defined. I've been searching for a few hours and haven't had any luck finding a solution - so clearly I'm doing something wrong.

My main.js is very simple, I just create my window and then I create an ipc listener as so:

const { app, BrowserWindow } = require("electron");
const ipc = require('electron').ipcMain;

function createWindow() {
    const window = new BrowserWindow({
        transparent: true,
        frame: false,
        resizable: false,
        center: true,
        width: 410,
        height: 550,
    });
    window.loadFile("index.html");
}

app.whenReady().then(createWindow);

ipc.on('invokeAction', (event, data) => {
    var result = "test result!";
    event.sender.send('actionReply', result);
})

Within the file that I wish to manipulate the DOM with, I attempt to get the element ID and then add an event listener as seen here:

const ipc = require('electron').ipcRenderer;
const helper = require("./api");


var authenticate_button = ipcRenderer.getElementById("authenticate-button");

var authButton = document.getElementById("authenticate-button");
authButton.addEventListener("click", () => {
    ipc.once('actionReply', (event, response) => {
        console.log("Hello world!");
    })
    ipc.send('invokeAction');
});

function onAuthenticateClick() {
    helper.authenticateLogin(api_public, api_secret, access_public, access_secret);
}

and finally, my HTML only consists of a button that I wish to attach my event listener to:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Project Test</title>
    <link rel="stylesheet" href="style.css" />
</head>

<body>
    <div class="main-container">
        <button id="authenticate-button" type="submit" onclick="">Authenticate</button>
        <p id="status-label">Not Authenticated</p>
    </div>

    <script src="script.js"></script>
</body>
</html>

If anyone could help point me in the right direction as to how to get this basic functionality to work, it would be very helpful!

Upvotes: 6

Views: 20711

Answers (2)

bikz
bikz

Reputation: 567

As mentioned by AlekseyHoffman, the reason you can't access ipcRenderer in your frontend js file is because you have nodeIntegration set to false. That said, there's a reason it's set to false by default now; it makes your app far less secure.

Let me suggest an alternate approach: rather than trying to access ipcRenderer directly from your frontend js by setting nodeIntegration to true, access it from preload.js. In preload.js, you can selectively expose ipcMain functions (from your main.js file) you want to access on the frontend (including those that can send data back from main.js), and call them via ipcRenderer there. In your frontend js, you can access the preload.js object that exposes those functions; preload.js will then call those main.js functions via ipcRenderer and return the data back to the frontend js that called it.

Here's a simple, but fully working example (these files should be sufficient to build an electron app with two-way communication between main.js and frontend. In this example, all of the following files are in the same directory.):

main.js

// boilerplate code for electron..
const {
    app,
    BrowserWindow,
    ipcMain,
    contextBridge
} = require("electron");
const path = require("path");  
let win;

/**
 * make the electron window, and make preload.js accessible to the js
 * running inside it (this will allow you to communicate with main.js
 * from the frontend).
 */
async function createWindow() {

    // Create the browser window.
    win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: false, // is default value after Electron v5
            contextIsolation: true, // protect against prototype pollution
            enableRemoteModule: false,
            preload: path.join(__dirname, "./preload.js") // path to your preload.js file
        }
    });

    // Load app
    win.loadFile(path.join(__dirname, "index.html"));
}
app.on("ready", createWindow);

// end boilerplate code... now on to your stuff

/** 
 * FUNCTION YOU WANT ACCESS TO ON THE FRONTEND
 */
ipcMain.handle('myfunc', async (event, arg) => {
  return new Promise(function(resolve, reject) {
    // do stuff
    if (true) {
        resolve("this worked!");
    } else {
        reject("this didn't work!");
    }
  });  
});

Note, I'm using an example of ipcMain.handle because it allows two-way communication and returns a Promise object - i.e., when you access this function from the frontend via preload.js, you can get that Promise back with the data inside it.

preload.js:

// boilerplate code for electron...
const {
    contextBridge,
    ipcRenderer
} = require("electron");

// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
    const replaceText = (selector, text) => {
        const element = document.getElementById(selector)
        if (element) element.innerText = text
    }

    for (const type of ['chrome', 'node', 'electron']) {
        replaceText(`${type}-version`, process.versions[type])
    }
})

// end boilerplate code, on to your stuff..

/**
 * HERE YOU WILL EXPOSE YOUR 'myfunc' FROM main.js
 * TO THE FRONTEND.
 * (remember in main.js, you're putting preload.js
 * in the electron window? your frontend js will be able
 * to access this stuff as a result.
 */
contextBridge.exposeInMainWorld(
    "api", {
        invoke: (channel, data) => {
            let validChannels = ["myfunc"]; // list of ipcMain.handle channels you want access in frontend to
            if (validChannels.includes(channel)) {
                // ipcRenderer.invoke accesses ipcMain.handle channels like 'myfunc'
                // make sure to include this return statement or you won't get your Promise back
                return ipcRenderer.invoke(channel, data); 
            }
        },
    }
);

renderer process (i.e. your frontend js file - I'll call it frontend.js):

// call your main.js function here
console.log("I'm going to call main.js's 'myfunc'");
window.api.invoke('myfunc', [1,2,3])
    .then(function(res) {
        console.log(res); // will print "This worked!" to the browser console
    })
    .catch(function(err) {
        console.error(err); // will print "This didn't work!" to the browser console.
    });

index.html

<!DOCTYPE html>
<html>

<head>
    <title>My Electron App</title>
</head>

<body>
    <h1>Hello Beautiful World</h1>
    <script src="frontend.js"></script> <!-- load your frontend script -->
</body>
</html>

package.json

{
  "name": "myapp",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  }
}

The files above should be sufficient to have a fully working electron app with communication between main.js and the frontend js. Put them all in one directory with the names main.js, preload.js, frontend.js, and index.html, and package.json and launch your electron app using npm start. Note that in this example I am storing all the files in the same directory; make sure to change these paths to wherever they are stored on your system.

See these links for more info and examples:

Electron documentation on inter-process communication

An overview of why IPC is needed and the security issues of setting nodeintegration to true

Upvotes: 10

AlekseyHoffman
AlekseyHoffman

Reputation: 2694

The require is not defined because you didn't enable nodeIntegration on the window. Set it to true in your window config:

const window = new BrowserWindow({
  transparent: true,
  frame: false,
  resizable: false,
  center: true,
  width: 410,
  height: 550,
  webPreferences: {
    nodeIntegration: true
  }
})

Upvotes: 4

Related Questions