使用预加载脚本
学习目标
在本教程的这一部分,您将学习什么是预加载脚本,以及如何使用它来安全地将特权 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 模块 | events 、timers 、url |
Polyfill 全局变量 | 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')
调用包装在一个辅助函数中,而不是通过上下文桥直接公开 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) 接口,以在两种进程之间传递任意消息。
在本教程的下一部分中,我们将向您展示有关向您的应用程序添加更多功能的资源,然后教您如何将您的应用程序分发给用户。