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