使用预加载脚本
学习目标
在本教程的这一部分,您将学习什么是预加载脚本,以及如何使用预加载脚本将特权 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 模块 | events 、timers 、url |
Polyfilled 全局变量 | Buffer 、process 、clearImmediate 、setImmediate |
有关更多信息,请查看 进程沙箱 指南。
预加载脚本在网页加载到渲染器中之前被注入,类似于 Chrome 扩展的 内容脚本。为了向您的渲染器添加需要特权访问的功能,您可以通过 contextBridge API 定义 全局 对象。
为了演示这个概念,您将创建一个预加载脚本,将应用程序的 Chrome、Node 和 Electron 版本暴露到渲染器中。
添加一个新的 preload.js
脚本,将 Electron 的 process.versions
对象的选定属性暴露到渲染器进程中的 versions
全局变量中。
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
选项
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()
})
此时,渲染器可以访问 versions
全局变量,因此让我们将该信息显示在窗口中。可以通过 window.versions
或简单的 versions
访问此变量。创建一个 renderer.js
脚本,使用 document.getElementById
DOM API 替换 HTML 元素中 info
作为其 id
属性的显示文本。
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
脚本
<!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>
按照上述步骤操作后,您的应用应该看起来像这样
代码应该如下所示
- main.js
- preload.js
- index.html
- renderer.js
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()
}
})
const { contextBridge } = require('electron/renderer')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
})
<!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>
const information = document.getElementById('info')
information.innerText = `This app is using Chrome (v${window.versions.chrome()}), Node.js (v${window.versions.node()}), and Electron (v${window.versions.electron()})`
进程间通信
如上所述,Electron 的主进程和渲染器进程拥有不同的职责,不能互换。这意味着无法直接从渲染器进程访问 Node.js API,也无法从主进程访问 HTML 文档对象模型 (DOM)。
解决此问题的方案是使用 Electron 的 ipcMain
和 ipcRenderer
模块进行进程间通信 (IPC)。要从您的网页向主进程发送消息,您可以使用 ipcMain.handle
设置主进程处理程序,然后公开一个调用 ipcRenderer.invoke
来触发您在预加载脚本中的处理程序的函数。
为了说明这一点,我们将向渲染器添加一个全局函数,称为 ping()
,该函数将返回来自主进程的字符串。
首先,在您的预加载脚本中设置 invoke
调用
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
})
请注意,我们是如何将 ipcRenderer.invoke('ping')
调用包装在辅助函数中,而不是通过 context bridge 直接暴露 ipcRenderer
模块。您 **永远** 不想通过预加载直接暴露整个 ipcRenderer
模块。这将使您的渲染器能够向主进程发送任意 IPC 消息,这将成为恶意代码的强大攻击媒介。
然后,在主进程中设置您的 handle
监听器。我们在加载 HTML 文件 *之前* 进行此操作,以便确保在您从渲染器发出 invoke
调用之前,处理程序已准备就绪。
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'
通道从渲染器向主进程发送消息。
const func = async () => {
const response = await window.versions.ping()
console.log(response) // prints out 'pong'
}
func()
有关使用 ipcRenderer
和 ipcMain
模块的更深入的说明,请查看完整的 进程间通信 指南。
总结
预加载脚本包含在您的网页加载到浏览器窗口中之前运行的代码。它可以访问 DOM API 和 Node.js 环境,通常用于通过 contextBridge
API 将特权 API 暴露给渲染器。
由于主进程和渲染器进程拥有非常不同的职责,因此 Electron 应用程序经常使用预加载脚本来设置进程间通信 (IPC) 接口,以在两种进程之间传递任意消息。
在本教程的下一部分,我们将向您展示有关向应用程序添加更多功能的资源,然后教您如何将应用程序分发给用户。