如何在电子中正确使用 preload.js

我尝试在我的 renderer进程中使用 Node 模块(在本例中是 fs) ,如下所示:

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


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

注意: 当我按下 main_window中的一个按钮时,就会调用 action函数。

但这给出了一个错误:

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

我可以解决这个问题,正如这个被接受的答案所暗示的那样,通过添加这些行到我的 main.js时,初始化的 main_window:

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

但是,根据文件记录,这不是最好的做法,相反,我应该创建一个 preload.js文件,在那里加载这些 Node 模块,然后在我的所有 renderer进程中使用它。像这样:

返回文章页面

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

返回文章页面

const fs = require('fs')


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

返回文章页面

function action() {
window.test()
}

成功了!


现在我的问题是,我应该在 preload.js中编写我的 renderer进程的大部分代码(因为只有在 preload.js中我才能访问 Node 模块) ,然后仅仅调用每个 renderer.js文件中的函数(例如,这里的 main_window.js) ,这难道不是违反直觉的吗?我不明白的是什么?

83639 次浏览

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>

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

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

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.

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.