使用预加载脚本
学习目标
🌐 Learning goals
在本教程的这一部分,你将学习什么是预加载脚本以及如何使用它将特权 API 安全地暴露到渲染进程中。你还将学习如何使用 Electron 的进程间通信(IPC)模块在主进程和渲染进程之间进行通信。
🌐 In this part of the tutorial, you will learn what a preload script is and how to use one to securely expose privileged APIs into the renderer process. You will also learn how to communicate between main and renderer processes with Electron's inter-process communication (IPC) modules.
什么是预加载脚本?
🌐 What is a preload script?
Electron 的主进程是一个具有完全操作系统访问权限的 Node.js 环境。在 Electron 模块 之上,你还可以访问 Node.js 内置模块,以及通过 npm 安装的任何包。另一方面,渲染进程运行网页,并且出于安全原因默认不运行 Node.js。
🌐 Electron's main process is a Node.js environment that has full operating system access. On top of Electron modules, you can also access Node.js built-ins, as well as any packages installed via npm. On the other hand, renderer processes run web pages and do not run Node.js by default for security reasons.
为了将 Electron 的不同进程类型连接在一起,我们需要使用一个称为 preload 的特殊脚本。
🌐 To bridge Electron's different process types together, we will need to use a special script called a preload.
使用预加载脚本增强渲染器
🌐 Augmenting the renderer with a preload script
BrowserWindow 的 preload 脚本在一个可以访问 HTML DOM 以及 Node.js 和 Electron API 的有限子集的环境中运行。
🌐 A BrowserWindow's preload script runs in a context that has access to both the HTML DOM and a limited subset of Node.js and Electron APIs.
从 Electron 20 开始,预加载脚本默认是沙箱化的,并且不再可以访问完整的 Node.js 环境。实际上,这意味着你有一个填充了的 require 函数,它只能访问有限的 API 集。
🌐 From Electron 20 onwards, preload scripts are sandboxed by default and no longer have access
to a full Node.js environment. Practically, this means that you have a polyfilled require
function that only has access to a limited set of APIs.
| Available API | Details |
|---|---|
| Electron modules | Renderer process modules |
| Node.js modules | events, timers, url |
| Polyfilled globals | Buffer, process, clearImmediate, setImmediate |
欲了解更多信息,请查看进程沙箱指南。
🌐 For more information, check out the Process Sandboxing guide.
预加载脚本会在网页在渲染器加载之前注入,类似于 Chrome 扩展的 内容脚本。要向渲染器添加需要特权访问的功能,可以通过 上下文桥 API 定义 全球 对象。
🌐 Preload scripts are injected before a web page loads in the renderer, similar to a Chrome extension's content scripts. To add features to your renderer that require privileged access, you can define global objects through the contextBridge API.
为了演示这个概念,你将创建一个预加载脚本,将应用的 Chrome、Node 和 Electron 版本暴露给渲染进程。
🌐 To demonstrate this concept, you will create a preload script that exposes your app's versions of Chrome, Node, and Electron into the renderer.
添加一个新的 preload.js 脚本,将 Electron 的 process.versions 对象的选定属性暴露给渲染进程中的 versions 全局变量。
🌐 Add a new preload.js script that exposes selected properties of Electron's process.versions
object to the renderer process in a versions global variable.
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 选项:
🌐 To attach this script to your renderer process, pass its path to the
webPreferences.preload option in the BrowserWindow constructor:
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 来替换 info 作为其 id 属性的 HTML 元素的显示文本。
🌐 At this point, the renderer has access to the versions global, so let's display that
information in the window. This variable can be accessed via window.versions or simply
versions. Create a renderer.js script that uses the document.getElementById
DOM API to replace the displayed text for the HTML element with info as its id property.
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 脚本:
🌐 Then, modify your index.html by adding a new element with info as its id property,
and attach your renderer.js script:
<!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>
执行上述步骤后,你的应用应如下所示:
🌐 After following the above steps, your app should look something like this:

代码应该如下所示:
🌐 And the code should look like this:
- 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()})`
进程间通信
🌐 Communicating between processes
如上所述,Electron 的主进程和渲染进程有不同的职责,二者不可互换。这意味着不能直接从渲染进程访问 Node.js API,也不能从主进程访问 HTML 文档对象模型(DOM)。
🌐 As we have mentioned above, Electron's main and renderer process have distinct responsibilities and are not interchangeable. This means it is not possible to access the Node.js APIs directly from the renderer process, nor the HTML Document Object Model (DOM) from the main process.
解决这个问题的方法是使用 Electron 的 ipcMain 和 ipcRenderer 模块进行进程间通信(IPC)。要从你的网页向主进程发送消息,你可以使用 ipcMain.handle 设置一个主进程处理程序,然后在预加载脚本中暴露一个调用 ipcRenderer.invoke 的函数以触发该处理程序。
🌐 The solution for this problem is to use Electron's ipcMain and ipcRenderer modules for
inter-process communication (IPC). To send a message from your web page to the main process,
you can set up a main process handler with ipcMain.handle and
then expose a function that calls ipcRenderer.invoke to trigger the handler in your preload script.
为了说明,我们将在渲染器中添加一个名为 ping() 的全局函数,它将从主进程返回一个字符串。
🌐 To illustrate, we will add a global function to the renderer called ping()
that will return a string from the main process.
首先,在你的预加载脚本中设置 invoke 调用:
🌐 First, set up the invoke call in your preload script:
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 消息到主进程,从而成为恶意代码的强大攻击向量。
🌐 Notice how we wrap the ipcRenderer.invoke('ping') call in a helper function rather
than expose the ipcRenderer module directly via context bridge. You never want to
directly expose the entire ipcRenderer module via preload. This would give your renderer
the ability to send arbitrary IPC messages to the main process, which becomes a powerful
attack vector for malicious code.
然后,在主进程中设置你的 handle 监听器。我们在加载 HTML 文件之前这样做,这样可以确保在你从渲染进程发送 invoke 调用之前,处理程序已经准备就绪。
🌐 Then, set up your handle listener in the main process. We do this before
loading the HTML file so that the handler is guaranteed to be ready before
you send out the invoke call from the renderer.
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' 通道从渲染进程向主进程发送消息。
🌐 Once you have the sender and receiver set up, you can now send messages from the renderer
to the main process through the 'ping' channel you just defined.
const func = async () => {
const response = await window.versions.ping()
console.log(response) // prints out 'pong'
}
func()
有关使用 ipcRenderer 和 ipcMain 模块的更深入说明,请查看完整的 进程间通信 指南。
🌐 For more in-depth explanations on using the ipcRenderer and ipcMain modules,
check out the full Inter-Process Communication guide.
概括
🌐 Summary
预加载脚本包含在网页加载到浏览器窗口之前运行的代码。它可以访问 DOM API 和 Node.js 环境,并且通常用于通过 contextBridge API 向渲染器暴露特权 API。
🌐 A preload script contains code that runs before your web page is loaded into the browser
window. It has access to both DOM APIs and Node.js environment, and is often used to
expose privileged APIs to the renderer via the contextBridge API.
由于主进程和渲染进程的职责截然不同,Electron 应用通常使用预加载脚本来设置进程间通信(IPC)接口,以便在这两类进程之间传递任意消息。
🌐 Because the main and renderer processes have very different responsibilities, Electron apps often use the preload script to set up inter-process communication (IPC) interfaces to pass arbitrary messages between the two kinds of processes.
在教程的下一部分,我们将向你展示如何为应用添加更多功能的资源,然后教你如何将应用分发给用户。
🌐 In the next part of the tutorial, we will be showing you resources on adding more functionality to your app, then teaching you how to distribute your app to users.