跳到主内容

使用预加载脚本

学习目标

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

什么是预加载脚本?

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

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

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

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

预加载脚本沙箱化

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

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

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

预加载脚本在网页加载到渲染器之前注入,类似于 Chrome 扩展的 content scripts。要为你的渲染器添加需要特权访问的功能,你可以通过 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 替换 id 属性为 info 的 HTML 元素的显示文本。

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

然后,修改你的 index.html 文件,添加一个 id 属性为 info 的新元素,并附加你的 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) 接口,以便在这两种进程之间传递任意消息。

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