我正在为自己的目的创建一个电子应用程序。我的问题是,当我在我的HTML页面内使用节点函数时,它抛出了一个错误:

'require()'没有定义。

是否有办法在所有HTML页面中使用Node功能?如果有可能,请给我一个如何做到这一点的例子或提供一个链接。下面是我试图在我的HTML页面中使用的变量:

  var app = require('electron').remote; 
  var dialog = app.dialog;
  var fs = require('fs');

这些是我在电子中所有HTML窗口中使用的值。


你是否在BrowserWindow初始化时使用nodeIntegration: false ?如果是,设置为true(默认值为true)。

并且像这样在HTML中包含你的外部脚本(而不是<script> src="./index.js" </script>):

<script>
   require('./index.js')
</script>

最后,我成功了。将此代码添加到HTML文档Script Element中。

很抱歉回复晚了。我使用下面的代码来做这件事。

window.nodeRequire = require;
delete window.require;
delete window.exports;
delete window.module;

使用nodeRequire而不是require。

很好。


从版本5开始,nodeIntegration的默认值从true变为false。 您可以在创建浏览器窗口时启用它:

app.on('ready', () => {
    mainWindow = new BrowserWindow({
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false,
        }
    });
});

出于安全原因,你应该保持nodeIntegration: false,并使用一个预加载脚本,通过窗口变量将你所需要的从Node/Electron API暴露给渲染器进程(视图)。来自电子文档:

预加载脚本继续可以访问require和其他Node.js特性


例子

main.js

const mainWindow = new BrowserWindow({
  webPreferences: {
    preload: path.join(app.getAppPath(), 'preload.js')
  }
})

preload.js

const { remote } = require('electron');

let currWindow = remote.BrowserWindow.getFocusedWindow();

window.closeCurrentWindow = function(){
  currWindow.close();
}

renderer.js

let closebtn = document.getElementById('closebtn');

closebtn.addEventListener('click', (e) => {
  e.preventDefault();
  window.closeCurrentWindow();
});

首先,@Sathiraumesh解决方案给电子应用程序留下了巨大的安全问题。想象一下,你的应用程序正在为messenger.com添加一些额外的功能,例如,当你有未读消息时,工具栏的图标会改变或闪烁。所以在你的main.js文件中,你像这样创建新的BrowserWindow(注意我故意拼错了messenger.com):

app.on('ready', () => {
    const mainWindow = new BrowserWindow({
        webPreferences: {
            nodeIntegration: true
        }
    });
    mainWindow.loadURL(`https://messengre.com`);
});

如果messengre.com是一个恶意网站,想要伤害你的电脑怎么办?如果你设置了nodeIntegration: true,这个站点可以访问你的本地文件系统,并且可以执行以下命令:

require('child_process').exec('rm -r ~/');

您的主目录消失了。

解决方案 只暴露你需要的,而不是一切。这是通过使用require语句预加载javascript代码来实现的。

// main.js
app.on('ready', () => {
    const mainWindow = new BrowserWindow({
        webPreferences: {
            preload: `${__dirname}/preload.js`
        }
    });
    mainWindow.loadURL(`https://messengre.com`);
});
// preload.js
window.ipcRenderer = require('electron').ipcRenderer;
// index.html
<script>
    window.ipcRenderer.send('channel', data);
</script>

现在糟糕的messengre.com不能删除你的整个文件系统。


你必须在webPreferences中启用nodeIntegration才能使用它。见下文,

const { BrowserWindow } = require('electron')
let win = new BrowserWindow({
  webPreferences: {
    nodeIntegration: true
  }
})
win.show()

在电子5.0中有一个突破性的api变化(关于存储库的公告)。在最近的版本中,nodeIntegration默认设置为false。

由于Node.js集成了Electron, DOM中插入了一些额外的符号,如module, exports, require。这给一些库带来了问题,因为它们想要插入具有相同名称的符号。要解决这个问题,你可以在Electron中关闭节点集成:

但是如果你想保留使用Node.js和Electron api的能力,你必须在包含其他库之前重命名页面中的符号:

<head>
    <script>
        window.nodeRequire = require;
        delete window.require;
        delete window.exports;
        delete window.module;
    </script>
    <script type="text/javascript" src="jquery.js"></script>
</head>

编辑2022


我已经发表了一篇关于Electron历史的大文章,它的安全性提供了影响不同框架版本安全性的变化的额外背景(以及采取的最佳方法)。

原来的答案


我希望这个答案能引起一些注意,因为这里的大多数答案在你的电子应用程序中留下了很大的安全漏洞。事实上,这个答案本质上是你在电子应用程序中使用require()时应该做的事情。(只是有一个新的电子API,使它在v7中更干净一点)。

我写了一个详细的解释/解决方案在github使用最新的电子api,你可以要求()的东西,但我将简要解释为什么你应该遵循一个方法使用预加载脚本,contextBridge和ipc。

这个问题

Electron应用程序很棒,因为我们可以使用node,但这种功能是一把双刃剑。如果我们不小心,我们就会让别人通过我们的应用程序访问节点,而有了节点,坏人就可以破坏你的机器或删除你的操作系统文件(我想还有其他事情)。

As brought up by @raddevus in a comment, this is necessary when loading remote content. If your electron app is entirely offline/local, then you are probably okay simply turning on nodeIntegration:true. I still would, however, opt to keep nodeIntegration:false to act as a safeguard for accidental/malicious users using your app, and prevent any possible malware that might ever get installed on your machine from interacting with your electron app and using the nodeIntegration:true attack vector (incredibly rare, but could happen)!

问题是什么样子的

当您(以下任何一种情况):

是否启用了nodeIntegration:true 使用远程模块

所有这些问题都使你的呈现进程不间断地访问节点。如果您的渲染进程被劫持,您可以认为一切都丢失了。

我们的解决方案是什么

解决方案是不给渲染器直接访问节点(即。Require()),但是为了让我们的电子主进程访问Require,并且在我们的渲染进程需要使用Require时,将一个请求编组到主进程。

在Electron的最新版本(7+)中,我们在渲染端设置了ipcRenderer绑定,在主端设置了ipcMain绑定。在ipcMain绑定中,我们设置了使用所需模块()的侦听器方法。这很好,因为我们的主进程可以要求它想要的一切。

我们使用contextBridge将ipcRenderer绑定传递给我们的应用程序代码(使用),因此当我们的应用程序需要在main中使用所需的模块时,它通过IPC(进程间通信)发送消息,主进程运行一些代码,然后我们将我们的结果发送回消息。

大致上,这就是你要做的。

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>

免责声明

我是secure- electronic -template的作者,这是一个构建电子应用程序的安全模板。我很关心这个话题,并且已经为此工作了几个星期(在这个时间点上)。


我所要做的只是在我的html页面中要求一个js文件,因为我正在遵循的教程。但是,我打算使用远程模块,所以安全性是最重要的。我在上面修改了迈克尔的答案,所以我发布了出来,纯粹是为了那些像我一样花了几个小时寻找安全替代“要求”的人。如果代码不正确,请随时指出来。

main.js

const electron = require('electron');
const app=electron.app;
const BrowserWindow=electron.BrowserWindow;
const ipcMain=electron.ipcMain;

const path=require('path');
const url=require('url');

let win;

function createWindow(){
    win=new BrowserWindow({
        webPreferences:{
            contextIsolation: true,
            preload: path.join(__dirname, "preload.js")
        }
    });
    win.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file',
        slashes: true
    }));

    win.on('close', function(){
        win=null
    });
}

app.on('ready', createWindow);

preload.js

const electron=require('electron');
const contextBridge=electron.contextBridge;

contextBridge.exposeInMainWorld(
    "api", {
        loadscript(filename){
            require(filename);
        }
    }
);

index . html

<!DOCTYPE html>
<html>
    <head>
        <title>Hello World App</title>
    </head>
    <body>
        <h1>Hello World</h1>
        <button id="btn">Click</button>
    </body>
    <script>
        window.api.loadscript('./index.js');
    </script>
</html>

index.js

const btn = document.getElementById('btn');
btn.addEventListener('click', function(){
    console.log('button clicked');
});

我特别想知道这是否仍然存在安全风险。谢谢。


看起来Electron的安全性是这样进化的(来源)。

Electron 1 nodeIntegration默认为true

Renderer可以完全访问Node API—如果Renderer加载远程代码,则存在巨大的安全风险。

Electron 5 nodeIntegration默认为false

当设置为false时,将使用预加载脚本向Renderer公开特定的API。(不管nodeIntegration的值如何,预加载脚本总是可以访问Node api)

//preload.js
window.api = {
    deleteFile: f => require('fs').unlink(f)
}

Electron 5 contextIsolation默认为true(实际上在Electron 11中仍然默认为false)

这将导致预加载脚本在单独的上下文中运行。你不能再做窗户了。API = ....你现在要做的是:

//preload.js
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('api', {
    deleteFile: f => require('fs').unlink(f)
})

电子6 require()节点内置沙盒渲染器不再隐式加载远程版本

如果Renderer有沙盒设置为true,你必须做:

//preload.js
const { contextBridge, remote } = require('electron')

contextBridge.exposeInMainWorld('api', {
    deleteFile: f => remote.require('fs').unlink(f)
})

Electron 10 enableRemoteModule默认为false(远程模块在Electron 12中已弃用)

remote模块用于当你需要从沙盒渲染器中访问Node api时(如上面的例子);或者当你需要访问仅对主进程可用的电子api时(如对话框,菜单)。如果没有remote,您需要编写如下所示的显式IPC处理程序。

//preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('api', {
    displayMessage: text => ipcRenderer.invoke("displayMessage", text)
})

//main.js
const { ipcMain, dialog } = require('electron')

ipcMain.handle("displayMessage", text => dialog.showMessageBox(text))

Electron 10弃用节点集成标志(在Electron 12中删除)

建议

总是设置{nodeIntegration: false, contextIsolation: true, enableRemoteModule: false}。

为了最大的安全性,设置{sandbox: true}。您的预加载脚本将必须使用IPC调用主进程来完成所有工作。

如果sandbox为false,你的预加载脚本可以直接访问Node API,如require('fs'). readfile。你是安全的,只要你不这样做

//bad
contextBridge.exposeInMainWorld('api', {
    readFile: require('fs').readFile
})

如果你只是不关心任何安全问题,并且想要在浏览器窗口上被JavaScript正确地解释,那么在main.js代码中有一个额外的标志:

webPreferences: { nodeIntegration:没错, nodeIntegrationInWorker:没错, nodeIntegrationInSubFrames:没错, enableRemoteModule:没错, contextIsolation: false //required标志 } //其余的代码…


为了现实性和完整性,我添加了我的一块蛋糕。以下是我对这个话题的重要看法。请记住这篇文章的日期是2022年10月,电子版本是21.1.1。 在电子文档中有一篇文章叫做进程间通信,其中以非常清晰的方式描述了这个主题。

下面的代码只是前面提到的站点上示例代码的一个副本。

main.js文件:

const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')

function createWindow () {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  ipcMain.on('set-title', (event, title) => {
    const webContents = event.sender
    const win = BrowserWindow.fromWebContents(webContents)
    win.setTitle(title)
  })

  mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
  createWindow()
  
  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

总结:

在webPreferences中只定义预加载脚本,并让所有那些nodeIntegration, nodeIntegrationInWorker, nodeIntegrationInSubFrames, enableRemoteModule, contextIsolation应用默认值。

下一个文件是preload.js:

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

contextBridge.exposeInMainWorld('electronAPI', {
    setTitle: (title) => ipcRenderer.send('set-title', title)
})

在这里,electronAPI对象将被注入到浏览器上下文中,因此将有一个窗口。电子api对象,它将有一个名为setTitle的成员函数。当然你也可以添加其他属性。

setTitle函数只调用ipcRenderer。发送是进程间通信桥或隧道的一端。

你在这里发送的内容在另一端,在main.js文件中,ipcMain。在函数。在这里,您注册了设置标题事件。

下面的例子继续使用index.html文件:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Hello World!</title>
  </head>
  <body>
    Title: <input id="title"/>
    <button id="btn" type="button">Set</button>
    <script src="./renderer.js"></script>
  </body>
</html>

加载renderer.js脚本:

const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
    const title = titleInput.value
    window.electronAPI.setTitle(title)
});

在那里你可以访问window.electronAPI.setTitle函数,它是你在preload.js中定义的,它将标题发送到ipcRenderer中,然后这个标题从main.js中的ipcMain中掉出来,触发一个事件并导致一个函数运行,该函数依次设置应用程序的标题。

所以我要再次强调阅读文档。有更多关于IPC的示例。另外,请阅读上下文隔离章节,它非常简短和清晰。