'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
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.
- In the Main process, use the:
BrowserWindow.webContents.send()
method to send messages to the RendereripcMain.handle()
method to receive messages from the Renderer
- In the Preload process, expose user-defined endpoints to the Renderer process.
- 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 usingcontextBridge
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 |