跳至主要内容

使用预加载脚本

按照教程进行操作

学习目标

在本教程的这一部分,您将学习什么是预加载脚本,以及如何使用预加载脚本将特权 API 安全地暴露到渲染器进程。您还将学习如何使用 Electron 的进程间通信 (IPC) 模块在主进程和渲染器进程之间进行通信。

什么是预加载脚本?

Electron 的主进程是一个 Node.js 环境,拥有完整的操作系统访问权限。除了 Electron 模块 之外,您还可以访问 Node.js 内置模块,以及通过 npm 安装的任何包。另一方面,渲染器进程运行网页,默认情况下不会运行 Node.js,以确保安全性。

为了将 Electron 的不同进程类型连接在一起,我们需要使用一个称为 **预加载** 的特殊脚本。

使用预加载脚本增强渲染器

BrowserWindow 的预加载脚本在一个具有访问 HTML DOM 和 Node.js 以及 Electron API 的有限子集的上下文中运行。

预加载脚本沙箱

从 Electron 20 开始,预加载脚本默认情况下是 **沙箱化** 的,不再具有访问完整 Node.js 环境的权限。实际上,这意味着您有一个 polyfilled require 函数,它只能访问有限的 API 集。

可用 API详情
Electron 模块渲染器进程模块
Node.js 模块eventstimersurl
Polyfilled 全局变量BufferprocessclearImmediatesetImmediate

有关更多信息,请查看 进程沙箱 指南。

预加载脚本在网页加载到渲染器中之前被注入,类似于 Chrome 扩展的 内容脚本。为了向您的渲染器添加需要特权访问的功能,您可以通过 contextBridge API 定义 全局 对象。

为了演示这个概念,您将创建一个预加载脚本,将应用程序的 Chrome、Node 和 Electron 版本暴露到渲染器中。

添加一个新的 preload.js 脚本,将 Electron 的 process.versions 对象的选定属性暴露到渲染器进程中的 versions 全局变量中。

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

contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
// we can also expose variables, not just functions
})

要将此脚本附加到您的渲染器进程,请在 BrowserWindow 构造函数中将它的路径传递给 webPreferences.preload 选项

main.js
const { app, BrowserWindow } = require('electron')
const path = require('node:path')

const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

win.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()
})
信息

这里使用了两个 Node.js 概念

  • __dirname 字符串指向当前正在执行脚本的路径(在本例中,为项目的根文件夹)。
  • path.join API 将多个路径段连接在一起,创建了一个在所有平台上都可用的组合路径字符串。

此时,渲染器可以访问 versions 全局变量,因此让我们将该信息显示在窗口中。可以通过 window.versions 或简单的 versions 访问此变量。创建一个 renderer.js 脚本,使用 document.getElementById DOM API 替换 HTML 元素中 info 作为其 id 属性的显示文本。

renderer.js
const information = document.getElementById('info')
information.innerText = `This app is using Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), and Electron (v${versions.electron()})`

然后,通过添加一个新的元素,并将 info 作为其 id 属性来修改您的 index.html,并附加您的 renderer.js 脚本

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Hello from Electron renderer!</title>
</head>
<body>
<h1>Hello from Electron renderer!</h1>
<p>👋</p>
<p id="info"></p>
</body>
<script src="./renderer.js"></script>
</html>

按照上述步骤操作后,您的应用应该看起来像这样

Electron app showing This app is using Chrome (v102.0.5005.63), Node.js (v16.14.2), and Electron (v19.0.3)

代码应该如下所示

const { app, BrowserWindow } = require('electron/main')
const path = require('node:path')

const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

win.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()

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

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

进程间通信

如上所述,Electron 的主进程和渲染器进程拥有不同的职责,不能互换。这意味着无法直接从渲染器进程访问 Node.js API,也无法从主进程访问 HTML 文档对象模型 (DOM)。

解决此问题的方案是使用 Electron 的 ipcMainipcRenderer 模块进行进程间通信 (IPC)。要从您的网页向主进程发送消息,您可以使用 ipcMain.handle 设置主进程处理程序,然后公开一个调用 ipcRenderer.invoke 来触发您在预加载脚本中的处理程序的函数。

为了说明这一点,我们将向渲染器添加一个全局函数,称为 ping(),该函数将返回来自主进程的字符串。

首先,在您的预加载脚本中设置 invoke 调用

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

contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
ping: () => ipcRenderer.invoke('ping')
// we can also expose variables, not just functions
})
IPC 安全性

请注意,我们是如何将 ipcRenderer.invoke('ping') 调用包装在辅助函数中,而不是通过 context bridge 直接暴露 ipcRenderer 模块。您 **永远** 不想通过预加载直接暴露整个 ipcRenderer 模块。这将使您的渲染器能够向主进程发送任意 IPC 消息,这将成为恶意代码的强大攻击媒介。

然后,在主进程中设置您的 handle 监听器。我们在加载 HTML 文件 *之前* 进行此操作,以便确保在您从渲染器发出 invoke 调用之前,处理程序已准备就绪。

main.js
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')

const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('ping', () => 'pong')
createWindow()
})

一旦您设置了发送方和接收方,您现在就可以通过您刚刚定义的 'ping' 通道从渲染器向主进程发送消息。

renderer.js
const func = async () => {
const response = await window.versions.ping()
console.log(response) // prints out 'pong'
}

func()
信息

有关使用 ipcRendereripcMain 模块的更深入的说明,请查看完整的 进程间通信 指南。

总结

预加载脚本包含在您的网页加载到浏览器窗口中之前运行的代码。它可以访问 DOM API 和 Node.js 环境,通常用于通过 contextBridge API 将特权 API 暴露给渲染器。

由于主进程和渲染器进程拥有非常不同的职责,因此 Electron 应用程序经常使用预加载脚本来设置进程间通信 (IPC) 接口,以在两种进程之间传递任意消息。

在本教程的下一部分,我们将向您展示有关向应用程序添加更多功能的资源,然后教您如何将应用程序分发给用户。