使用预加载脚本
学习目标
在本教程的这一部分,你将学习什么是预加载脚本,以及如何使用它安全地将特权 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 扩展的 content scripts。要为你的渲染器添加需要特权访问的功能,你可以通过 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()
})
这里使用了两个 Node.js 概念
__dirname
字符串指向当前正在执行的脚本的路径(在此例中,即你的项目根文件夹)。path.join
API 将多个路径片段连接在一起,创建一个适用于所有平台的组合路径字符串。
至此,渲染器可以访问 versions
全局变量,因此让我们在窗口中显示该信息。可以通过 window.versions
或直接 versions
访问此变量。创建一个 renderer.js
脚本,使用 document.getElementById
DOM API 替换 id
属性为 info
的 HTML 元素的显示文本。
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
脚本
<!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) 接口,以便在这两种进程之间传递任意消息。
在本教程的下一部分,我们将向你展示有关为你的应用添加更多功能的资源,然后教你如何将应用分发给用户。