'How to use preload.js properly in Electron

I'm trying to use Node modules (in this example, fs) in my renderer processes, like this:

// main_window.js
const fs = require('fs')

function action() {
    console.log(fs)
}

Note: The action function gets called when I press a button in my main_window.

But this gives an error:

Uncaught ReferenceError: require is not defined
    at main_window.js:1

I can solve this issue, as suggested by this accepted answer, by adding these lines to my main.js when initializing the main_window:

// main.js
main_window = new BrowserWindow({
    width: 650,
    height: 550,
    webPreferences: {
        nodeIntegration: true
    }
})

But, according to the docs, this isn't the best thing to do, and I should instead, create a preload.js file and load these Node modules there and then use it in all of my renderer processes. Like this:

main.js:

main_window = new BrowserWindow({
    width: 650,
    height: 550,
    webPreferences: {
        preload: path.join(app.getAppPath(), 'preload.js')
    }
})

preload.js:

const fs = require('fs')

window.test = function() {
    console.log(fs)
}

main_window.js:

function action() {
    window.test()
}

And it works!


Now my question is, isn't it counter-intuitive that I should write most of the code of my renderer processes in preload.js (Because only in preload.js I have access to Node modules) and then merely call the functions in each renderer.js file (for example here, main_window.js)? What am I not understanding here?



Solution 1:[1]

Edit 2022


I've published a larger post on the history of Electron (how security has changed throughout Electron versions) and additional security considerations Electron developers can make to ensure the preload file is being used correctly in new apps.

Edit 2020


As another user asked, let me explain my answer below.

The proper way to use the preload.js in Electron is to expose whitelisted wrappers around any module your app may need to require.

Security-wise, it's dangerous to expose require, or anything you retrieve through the require call in your preload.js (see my comment here for more explanation why). This is especially true if your app loads remote content, which many do.

In order to do things right, you need to enable a lot of options on your BrowserWindow as I detail below. Setting these options forces your electron app to communicate via IPC (inter-process communication) and isolates the two environments from each other. Setting up your app like this allows you to validate anything that may be a require'd module in your backend, which is free from the client tampering with it.

Below, you will find a brief example of what I speak about and how it can look in your app. If you are starting fresh, I might suggest using secure-electron-template (which I am the author of) that has all of these security best-practices baked in from the get go when building an electron app.

This page also has good information on the architecture that's required when using the preload.js to make secure apps.


main.js

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

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

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, // turn off remote
      preload: path.join(__dirname, "preload.js") // use a preload script
    }
  });

  // Load app
  win.loadFile(path.join(__dirname, "dist/index.html"));

  // rest of code..
}

app.on("ready", createWindow);

ipcMain.on("toMain", (event, args) => {
  fs.readFile("path/to/file", (error, data) => {
    // Do something with file contents

    // Send result back to renderer process
    win.webContents.send("fromMain", responseObj);
  });
});

preload.js

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

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
    "api", {
        send: (channel, data) => {
            // whitelist channels
            let validChannels = ["toMain"];
            if (validChannels.includes(channel)) {
                ipcRenderer.send(channel, data);
            }
        },
        receive: (channel, func) => {
            let validChannels = ["fromMain"];
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender` 
                ipcRenderer.on(channel, (event, ...args) => func(...args));
            }
        }
    }
);

index.html

<!doctype html>
<html lang="en-US">
<head>
    <meta charset="utf-8"/>
    <title>Title</title>
</head>
<body>
    <script>
        window.api.receive("fromMain", (data) => {
            console.log(`Received ${data} from main process`);
        });
        window.api.send("toMain", "some data");
    </script>
</body>
</html>

Solution 2:[2]

Consider this illustration

Electron Main Preload and Renderer

Not all those in the official documentation are directly implementable anywhere on your code. You must need a concise knowledge on the environments and processes.

Environment/Process Description
Main APIs that are much closer to the OS (low-level). These include the file system, OS-based notification popups, taskbar, etc. These were made possible through the combination of Electron's core APIs and Node.js
Preload A somewhat recent addendum in order to prevent powerful APIs available in the main environment from leaking. For more details, see Electron v12 changelogs and Issue #23506.
Renderer APIs of a modern web browser such as DOM and front-end JavaScript (high-level). This was made possible through Chromium.

Context isolation and Node integration

Scenario contextIsolation nodeIntegration Remarks
A false false Preload is not needed. Node.js is available in the Main but not in the Renderer.
B false true Preload is not needed. Node.js is available in the Main and Renderer.
C true false Preload is needed. Node.js is available in the Main and Preload but not in the Renderer. Default. Recommended.
D true true Preload is needed. Node.js is available in the Main, Preload, and Renderer.

How to use the preload properly?

You have to use Electron's inter-process communication (IPC) in order for the Main and the Renderer processes to communicate.

  1. In the Main process, use the:
  2. In the Preload process, expose user-defined endpoints to the Renderer process.
  3. In the Renderer process, use the exposed user-defined endpoints to:
    • send messages to Main
    • receive messages from Main

Example implementation

Main

/**
 * Sending messages to Renderer
 * `window` is an object which is an instance of `BrowserWindow`
 * `data` can be a boolean, number, string, object, or array
 */
window.webContents.send( 'custom-endpoint', data );

/**
 * Receiving messages from Renderer
 */
ipcMain.handle( 'custom-endpoint', async ( event, data ) => {
    console.log( data )
} )

Preload

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld( 'api', {
    send: ( channel, data ) => ipcRenderer.invoke( channel, data ),
    handle: ( channel, callable, event, data ) => ipcRenderer.on( channel, callable( event, data ) )
} )

Renderer

/**
 * Sending messages to Main
 * `data` can be a boolean, number, string, object, or array
 */
api.send( 'custom-endpoint', data )

/**
 * Receiving messages from Main
 */
api.handle( 'custom-endpoint', ( event, data ) => function( event, data ) {
    console.log( data )
}, event);

How about using promises?

As much as possible, keep promises to the same process/environment. Your promises on the main should stay on the main. Your promises on the renderer should also stay on the renderer. Don't make promises that jump from main-to-preload-to-renderer.

File system

Much of your business logic should still be in the Main or Renderer side but should never be in the Preload. This is because the Preload is just there pretty much as a medium. The Preload should be very minimal.

In OP's case, fs should be implemented on the Main side.

Solution 3:[3]

Things have progressed quickly in Electron, causing some confusion. The latest idiomatic example (as best as I can determine after much gnashing of teeth) is:

main.js

app.whenReady().then(() => {`
    let mainWindow = new BrowserWindow({`
        webPreferences: {
            preload: path.join(__dirname, 'preload.js'),
            contextIsolation: true
        },
        width:640,
        height: 480,
        resizable: false
    })
 ... rest of code

preload.js

const { contextBridge, ipcRenderer} = require('electron')

contextBridge.exposeInMainWorld(
    'electron',
    {
        sendMessage: () => ipcRenderer.send('countdown-start')
    }
)

renderer.js

document.getElementById('start').addEventListener('click', _ => {
    window.electron.sendMessage()
})

Solution 4:[4]

I see you got a little off-topic answer, so...

Yes, you need to split your code into 2 parts:

  • event handling and displaying data (render.js)
  • data preparation / processing: (preload.js)

Zac gave an example of a mega-safe way: by sending messages. But the electron accepts your solution:

// preload.js

const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('nodeCrypto', require('./api/nodeCrypto'))
)


// api/nodeCrypto.js

const crypto = require('crypto')
const nodeCrypto = {
  sha256sum (data) {
    const hash = crypto.createHash('sha256')
    hash.update(data)
    return hash.digest('hex')
  }
}
module.exports = nodeCrypto 

Note that both approaches are requesting return data, or perform the operation. It is a mistake to directly host "native" Node libraries. Here is an example of "innocent" sharing of a logger. And it was enough to expose only selected methods using the proxy object.

In the same article is an example of the use of communication ipc does not relieve us from thinking... So remember to filter your input.

Finally, I will quote the official documentation:

Just enabling contextIsolation and using contextBridge does not automatically mean that everything you do is safe. For instance this code is unsafe.

// ? Bad code
contextBridge.exposeInMainWorld('myAPI', {
  send: ipcRenderer.send
})

// ? Good code
contextBridge.exposeInMainWorld('myAPI', {
  loadPreferences: () => ipcRenderer.invoke('load-prefs')
})

Solution 5:[5]

I picked up Electron this week again and this was a tricky concept to get around to, but as I saw the reasoning it made perfect sense.

We live in an era where security is very important. Companies are being held to ransom, data is stolen. There are bad people everywhere. That's why you don't anyone to be able to execute code on your PC just because they happened to discover a vulnerability through your app.

So Electron is promoting good behavior by clamping down on it.

You can no longer access the system APIs from the render process, at least not the entirety of it. Only those bits that you expose to your render process through the preload file.

So write your UI code on the browser side, and expose functions inside of the preload.js file. Connect your render-side code to the main process using the ContextBridge

Using the exposeInMainWorld function of the context bridge.

Then inside of your render files, you can just refer to that function.

I can't say it's clean, but it works.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 cachique
Solution 4
Solution 5 Cyril Gupta