Correctly utilizing preload.js in Electron: A comprehensive guide
P粉197639753
P粉197639753 2023-08-27 20:25:30
0
2
502
<p>I tried using a node module (in this case <code>fs</code>) in my <code>renderer</code> process like this: </p> <pre class="brush:php;toolbar:false;">// main_window.js const fs = require('fs') function action() { console.log(fs) }</pre> <p><sup>Note: When I press the button in <code>main_window</code>, the <code>action</code> function is called. </sup></p> <p>But this produces the error: </p> <pre class="brush:php;toolbar:false;">Uncaught ReferenceError: require is not defined at main_window.js:1</pre> <p>I was able to solve this problem, as this accepted answer suggests, by adding these lines to my <code>main.js</code> when initializing <code>main_window</code> Medium: </p> <pre class="brush:php;toolbar:false;">// main.js main_window = new BrowserWindow({ width: 650, height: 550, webPreferences: { nodeIntegration: true } })</pre> However, according to the documentation, this is not the best practice, I should create a <code>preload.js</code> file and load these Node modules in it, and then in all my <code>renderer</code> Use it in the process.Like this:<p><br /></p> <p><code>main.js</code>:</p> <pre class="brush:php;toolbar:false;">main_window = new BrowserWindow({ width: 650, height: 550, webPreferences: { preload: path.join(app.getAppPath(), 'preload.js') } })</pre> <p><code>preload.js</code>:</p> <pre class="brush:php;toolbar:false;">const fs = require('fs') window.test = function() { console.log(fs) }</pre> <p><code>main_window.js</code>:</p> <pre class="brush:php;toolbar:false;">function action() { window.test() }</pre> <p>And it works! </p> <hr /> <p>Now my question is, isn't it counterintuitive that I should write most of the code for the <code>renderer</code> process in <code>preload.js</code> (since only I only have access to the Node module in <code>preload.js</code>) and then just call the functions in each <code>renderer.js</code> file (like here, <code>main_window. js</code>)? What don't I understand here? </p>
P粉197639753
P粉197639753

reply all(2)
P粉615829742

Consider this example

Not everything in the official documentation can be implemented directly anywhere in the code. You must need to have a concise understanding of the environment and processes.

Environment/Process describe
main API is closer to the operating system (low level). These include the file system, operating system-based notification popups, taskbar, and more. These are implemented through a combination of Electron's core API and Node.js
Preloading Recent Appendix to prevent leaks of powerful APIs available in the main environment. See Electron v12 Changelog and Issue#23506 for more details.
Renderer APIs for modern web browsers such as DOM and front-end JavaScript (advanced). This is achieved through Chromium.

Context isolation and node integration

Scenes contextIsolation nodeIntegration Remark
A Fake Fake No preloading required. Node.js is available in Main but not in Renderer.
B Fake true No preloading required. Node.js is available in Main and Renderer.
C true Fake Requires preloading. Node.js is available in main load and preload, but not in renderer. default. recommend.
D true true Requires preloading. Node.js is available in Main, Preload and Renderer.

How to use preloading correctly?

You must use Electron's inter-process communication (IPC) in order for the main process and the renderer process to communicate.

  1. In the main process, use:
  2. In the preloading process, expose the user-defined endpoint to the renderer process.
  3. In the renderer process, use the public user-defined endpoint to:
    • Send message to Main
    • Receive messages from Main

Implementation example

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 )
} )

Preloading

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 Promise?

Keep commitments to the same process/environment whenever possible. Your promises on main should remain on main. Your commitment to the renderer should also remain on the renderer. Don't make a commitment to jump from the main program to the preloader to the renderer.

File system

Most of your business logic should still be on the main or renderer side, but it should never be in preloading. This is because preloading exists almost exclusively as a medium. The preload should be very small.

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

P粉323374878

Edited 2022


I've published a larger article about the history of Electron (how security has changed in Electron versions) and other security considerations that Electron developers can take to ensure that new applications Use preload files correctly in the program.

Edit 2020


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

The correct way to use preload.js in Electron is to expose a whitelist wrapper around any modules that your app may require.

From a security perspective, anything exposed require or retrieved via a require call in preload.js is dangerous (see my comment for more explanation why). This is especially true if your application loads remote content (which many applications do).

In order to do this correctly, you need to create a new window in BrowserWindow as I detail below. Setting these options forces your Electron application to communicate via IPC (Inter-Process Communication) and isolates the two environments from each other. Setting up your application like this allows you to validate anything in the backend that might be a require module without it being tampered with by the client.

Below, you'll find a short example of what I'm talking about and how it might look in your app. If you are new to this, I might recommend using secure-electron-template (I am the author of it) to incorporate all these security best practices from the start when building electron applications .

This page also has good information on the architecture required when using preload.js to make secure applications.


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>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template