使用预加载脚本
学习目标
在本教程的这一部分中,你将学习什么是预加载脚本,以及如何使用它安全地将特权 API 暴露给渲染器进程。你还将学习如何使用 Electron 的进程间通信 (IPC) 模块在主进程和渲染器进程之间进行通信。
什么是预加载脚本?
Electron 的主进程是一个 Node.js 环境,拥有完整的操作系统访问权限。除了 Electron 模块之外,你还可以访问 Node.js 内置模块以及通过 npm 安装的任何包。另一方面,出于安全原因,渲染器进程运行网页,默认情况下不运行 Node.js。
为了将 Electron 的不同进程类型连接起来,我们需要使用一个名为预加载的特殊脚本。
使用预加载脚本增强渲染器
BrowserWindow 的预加载脚本在一个既可以访问 HTML DOM 又可以访问 Node.js 和 Electron API 有限子集的上下文中运行。
从 Electron 20 开始,预加载脚本默认处于沙盒中,不再拥有完整的 Node.js 环境访问权限。实际上,这意味着你有一个填充了 require
函数,该函数只能访问有限的 API 集。
可用 API | 详情 |
---|---|
Electron 模块 | 渲染器进程模块 |
Node.js 模块 | events , timers , url |
填充的全局变量 | 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 元素中 id
属性为 info
的显示文本。
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) 接口,以便在这两种进程之间传递任意消息。
在本教程的下一部分中,我们将向你展示如何为你的应用添加更多功能的资源,然后教你如何将你的应用分发给用户。