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

IT技术 javascript node.js electron
2021-02-25 11:38:51

我正在尝试fs在我的renderer流程中使用 Node module(在本例中为,如下所示:

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

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

注意:action当我按下main_window.

但这给出了一个错误:

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

但是,根据 docs,这不是最好的做法,我应该创建一个preload.js文件并在那里加载这些 Node module,然后在我的所有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()
}

它有效!


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

5个回答

编辑

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

preload.js在 Electron 中使用 的正确方法是在您的应用程序可能需要的任何module周围公开列入白名单的包装器require

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

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

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

此页面还提供有关使用 preload.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);
  });
});

预加载.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));
            }
        }
    }
);

索引.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>
首先,这个答案很棒。如果您还使用typescript,则需要将预加载更改为preload.ts并执行以下操作:stackoverflow.com/questions/56457935/...
2021-04-21 11:38:51
如果您有时间,您应该从链接的评论中移出基本想法,因为现在这个答案看起来只是一个样板答案,而没有回答实际问题。特别是如果链接消失了
2021-05-06 11:38:51
谢谢@Assimilater,我在答案中添加了更多细节。
2021-05-13 11:38:51
@RamKumar,如果它是ipcRenderer,你可以这样做:electronjs.org/docs/api/...您也可以使用删除所有方法。希望这可以帮助!
2021-05-14 11:38:51
如果path.join(__dirname, 'preload.js')不起作用,则使用__static. 我的问题和解决方案stackoverflow.com/questions/60814430/...
2021-05-16 11:38:51

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

主文件

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

预加载.js

const { contextBridge, ipcRenderer} = require('electron')

contextBridge.exposeInMainWorld(
    'electron',
    {
        sendMessage: () => ipcRenderer.send('countdown-start')
    }
)

渲染器.js

document.getElementById('start').addEventListener('click', _ => {
    window.electron.sendMessage()
})
我不明白为什么把 nodeIntegration 等放在根配置和 wwebPreferences 中……救了我的命……但是,你救了我的命。谢谢!
2021-04-27 11:38:51

考虑这个插图

Electron 主预加载和渲染器

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

环境/过程 描述
主要的 更接近操作系统的 API(低级)。这些包括文件系统、基于操作系统的通知弹出窗口、任务栏等。这些都是通过 Electron 的核心 API 和 Node.js 的组合而实现的
预载 为了防止主要环境中可用的强大 API 泄​​漏,最近的一个附录有关更多详细信息,请参阅Electron v12 更新日志问题 #23506
渲染器 现代 Web 浏览器的 API,例如 DOM 和前端 JavaScript(高级)。这是通过 Chromium 实现的。

上下文隔离和节点集成

设想 contextIsolation nodeIntegration 评论
一个 false false 不需要预加载。Node.js 在 Main 中可用,但在 Renderer 中不可用。
false true 不需要预加载。Node.js 在 Main 和 Renderer 中可用。
C true false 需要预加载。Node.js 在 Main 和 Preload 中可用,但在 Renderer 中不可用。默认. 推荐
D true true 需要预加载。Node.js 在 Main、Preload 和 Renderer 中可用。

如何正确使用预紧力?

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

  1. Main进程中,使用:
  2. Preload进程中,将用户定义的端点暴露给 Renderer 进程。
  3. Renderer进程中,使用公开的用户定义端点来:
    • 向主发送消息
    • 接收来自 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 只是作为一种媒介,而不是包装器。预加载应该非常小。

在 OP 的情况下fs应该在 Main 端实现。

我会接受专门用于 contextIsolation 的场景 A 和 B 配置,最高可达 Electron 的 v12。自 v12 及更高版本contextIsolation : true(默认)。在 contextIsolation 设置为 false required 的情况下,这可以通过 contextBridge 安全地实现;来到nodeItegration属性:如果预加载脚本呈现那些忽略nodeItegration任何布尔值,它将被设置为 tho 。
2021-05-12 11:38:51

我看到你的回答有点跑题了,所以...

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

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

Zac 举了一个超级安全的例子:通过发送消息。但是电子接受您的解决方案

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

请注意,这两种方法都是请求返回数据,或执行操作。直接托管“原生”Node 库是错误的。这是记录器“无辜”共享的示例使用代理对象仅公开选定的方法就足够了。

在同一篇文章中的一个例子是使用交流ipc并没有让我们从思考中解脱出来……所以记得过滤你的输入。

最后,引用官方文档

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

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

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

本周我再次拿起了 Electron,这是一个棘手的概念,但当我看到它的推理时,它是完全有道理的。

我们生活在一个安全非常重要的时代。公司被勒索赎金,数据被盗。哪里都有坏人。这就是为什么您不能让任何人仅仅因为他们碰巧通过您的应用程序发现漏洞就能够在您的 PC 上执行代码。

因此,Electron 正在通过压制它来促进良好的行为。

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

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

使用上下文桥的exposeInMainWorld 函数。

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

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