如何在 Electron 中正确使用 preload.js

Posted

技术标签:

【中文标题】如何在 Electron 中正确使用 preload.js【英文标题】:How to use preload.js properly in Electron 【发布时间】:2020-01-08 11:20:49 【问题描述】:

我正在尝试在我的 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_window 时将这些行添加到我的main.js 来解决这个问题,as suggested by this accepted answer:

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

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

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

而且它有效!


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

【问题讨论】:

【参考方案1】:

编辑

正如另一位用户所问,让我在下面解释我的答案。

在 Electron 中使用 preload.js 的正确方法是将您的应用可能需要的任何模块周围的白名单包装器暴露给 require

在安全方面,暴露require 或通过require 调用在preload.js 中检索到的任何内容是很危险的(请参阅my comment here 了解更多解释原因)。如果您的应用加载远程内容(很多人都会这样做),则尤其如此。

为了正确地做事,您需要在BrowserWindow 上启用很多选项,如下所述。设置这些选项会强制您的电子应用程序通过 IPC(进程间通信)进行通信,并将两个环境相互隔离。像这样设置您的应用程序可以让您验证后端中可能是 require'd 模块的任何内容,而客户端不会对其进行篡改。

您将在下面找到一个简短示例,说明我所说的内容以及它在您的应用中的外观。如果您刚开始,我可能会建议使用 secure-electron-template(我是其作者),它在构建电子应用程序时从一开始就包含了所有这些安全最佳实践。

This page 还提供了有关使用 preload.js 制作安全应用程序时所需的架构的良好信息。


ma​​in.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>

【讨论】:

如果您有时间,您应该将基本想法从您在链接的评论中移开,因为现在这个答案看起来只是一个样板答案,没有回答实际问题。特别是如果链接消失了 谢谢@Assimilater,我在回答中添加了更多细节。 @RamKumar ,如果是 ipcRenderer,你可以这样做:electronjs.org/docs/api/…。您也可以使用 remove all 方法。希望这会有所帮助! 首先,这个答案很棒。如果您还使用 typescript,则需要将 preload 更改为 preload.ts 并执行以下操作:***.com/questions/56457935/… 如果path.join(__dirname, 'preload.js') 不起作用,请使用__static。我的问题和解决方案***.com/questions/60814430/…【参考方案2】:

Electron 中的事情进展很快,引起了一些混乱。最新的惯用例子(在我咬牙切齿之后,尽我所能确定)是:

ma​​in.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()
)

【讨论】:

我不明白为什么要将 nodeIntegration 等...放在根配置和 wwebPreferences 中...救了我的命...但是,你救了我的命。谢谢!【参考方案3】:

我看到你的答案有点离题,所以...

是的,您需要将代码分成两部分:

事件处理和显示数据 (render.js) 数据准备/处理:(preload.js)

Zac 举了一个超级安全方式的例子:发送消息。但是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 

请注意,这两种方法都是请求返回数据,或执行操作。直接托管“本机”节点库是错误的。 Here is an example 记录器的“无辜”共享。并且使用代理对象仅公开选定的方法就足够了。

在同一篇文章中是一个使用ipc 通信的例子并不能解除我们的思考……所以记得过滤你的输入。

最后我引用the official documentation:

仅启用contextIsolation 并使用contextBridge 并不意味着您所做的一切都是安全的。例如,这段代码不安全

// ❌ Bad code
contextBridge.exposeInMainWorld('myAPI', 
  send: ipcRenderer.send
)

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

【讨论】:

【参考方案4】:

这周我又拿起了 Electron,这是一个很难解决的概念,但当我看到它的推理时,它非常有意义。

我们生活在一个安全非常重要的时代。公司被勒索赎金,数据被盗。到处都是坏人。这就是为什么你没有任何人能够在你的电脑上执行代码,只是因为他们碰巧通过你的应用发现了一个漏洞。

所以 Electron 正在通过压制它来促进良好的行为。

您不能再从渲染进程访问系统 API,至少不能访问整个渲染进程。只有那些通过预加载文件暴露给渲染进程的位。

所以在浏览器端编写您的 UI 代码,并在 preload.js 文件中公开函数。使用 ContextBridge 将渲染端代码连接到主进程

使用上下文桥的exposeInMainWorld函数。

然后在您的渲染文件中,您可以只引用该函数。

我不能说它很干净,但它确实有效。

【讨论】:

【参考方案5】:

考虑一下这个插图

并非官方文档中的所有内容都可以在您的代码的任何位置直接实现。您必须对环境和流程有一个简明的了解。

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.

上下文隔离和节点集成

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.

如何正确使用预加载?

您必须使用 Electron 的进程间通信 (IPC) 以便 Main 和 Renderer 进程进行通信。

    Main 进程中,使用: BrowserWindow.webContents.send() 向渲染器发送消息的方法 ipcMain.handle() 方法从渲染器接收消息 在Preload进程中,将用户定义的端点暴露给Renderer进程。 在渲染器进程中,使用暴露的用户定义端点来: 向 Main 发送消息 从 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 )
 )

预加载

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

渲染器

/**
 * 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);

您的大部分业务逻辑仍应位于 Main 或 Renderer 端,但绝不应位于 Preload 中。这是因为 Preload 只是作为媒介,而不是包装器。预加载应该非常小。

In OP's case, fs 应该在 Main 端实现。

【讨论】:

我会接受场景 A 和 B 配置,专门用于高达 v12 的 Electron 的 contextIsolation。自 v12 及更高版本contextIsolation : true(默认)。如果 contextIsolation 设置为 false required ,这可以通过 contextBridge 安全地实现;来到nodeItegration 属性:如果预加载脚本显示那些忽略nodeItegration,无论布尔值是什么,它都会被设置为tho。 当我的应用程序启动时,我想使用 preload.js 在辅助或非索引 html 页面中填充表格,但 obvs preload.js 似乎无法访问它。你知道我会怎么做,或者这是否是做事的适当方式?换句话说,index.html 具有指向其他 .html 文件的链接;它是具有table 的其他.html 文件之一,我什至想在从index.hmtl 导航到.html 页面之前填充它 @oldboy 在你的情况下,你可以这样做:(1)​​在渲染器中,向主发送消息。 (2) 在 Main 中,处理消息,然后向 Renderer 发送消息。 (3) 在 Renderer 中,处理消息,填充表格。 Preload 无法访问它的原因是因为 Preload 只运行一次并且它在 Renderer 执行之前。那时它不能再处理实时/传入的更改。 @AbelCallejo 啊,自从我使用 nodejs/electron 编码以来已经很久了,我想避免 xD 我同时决定将其设为 SPA,但这仍然存在问题,因为由preload 填充表格的文档片段甚至在DOM 中被renderer 检测到,直到经过一段明显的延迟之后:( 感谢信息secondary.html 或@987654360 @ 仅在单击index.html 的链接并在BrowserWindow 中访问/查看secondary.html 时执行?

以上是关于如何在 Electron 中正确使用 preload.js的主要内容,如果未能解决你的问题,请参考以下文章

使用 electron-builder 向 macOS info.plist 添加协议的正确方法是啥?

Codesign Electron App - 如何优雅地切换证书?

如何在 Electron 中使用 node_modules?

如何在 Electron 中使用 node_modules?

如何在 Electron 中使用 Typescript 脚本?

如何在Electron应用程序中通过navigator.geolocation获取getCurrentPosition?