Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 771
Amir Shabani Avatar asked Sep 05 '19 14:09

Amir Shabani


People also ask

How do I enable contextIsolation?

In Electron 5.0. 0, context isolation will be enabled by default. To prepare for this change, set {contextIsolation: false} in the webPreferences for this window, or ensure that this window does not rely on context isolation being disabled, and set {contextIsolation: true}.

What is Contextbridge in electron?

Create a safe, bi-directional, synchronous bridge across isolated contexts. Process: Renderer. An example of exposing an API to a renderer from an isolated preload script is given below: // Preload (Isolated World)

How to use preload script in electron?

How to use preload script in electron You can use preload.js which will be loaded before other scripts run in the page. This script will always have access to both electron APIs and node APIs (and also the browser APIs) no matter whether node integration is turned on or off.

How do I add a preload script to the renderer process?

Add a new preload.js script that exposes selected properties of Electron's process.versions object to the renderer process in a versions global variable. To attach this script to your renderer process, pass its path to the webPreferences.preload option in the BrowserWindow constructor:

Does this script have access to electron and node APIs?

This script will always have access to both electron APIs and node APIs (and also the browser APIs) no matter whether node integration is turned on or off. Using preload.js, you can expose any functionality or APIs by setting an global on window

How to run Node JS code in electron renderer?

However, you can run Node.js code and bridge it to the renderer thread through an Electron Preload script located at /src-electron/electron-preload. [js|ts]. Use contextBridge (from the electron package) to expose the stuff that you need for your UI.


Video Answer


1 Answers

Edit

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> 
like image 143
reZach Avatar answered Oct 04 '22 18:10

reZach