预加载脚本 Preload
预加载脚本(preload)是运行在主进程和渲染进程之间的一个特殊脚本,负责把主进程的能力安全地"递"给页面。它是 Electron 应用里负责安全桥接的关键一环。
是什么
Electron 的主进程是一个拥有着完全操作系统访问权限的 Node.js 环境。 除了 Electron 模块,你还可以访问 Node.js 内置模块,以及任何通过 npm 安装的包。
另一方面,渲染进程默认跑在网页页面上,而并非 Node.js里。
为了将 Electron 的不同类型的进程桥接在一起,我们需要使用被称为预加载的特殊脚本。
用人话来说,就是 Electron 应用有主进程和渲染进程两个世界:
- 主进程:Node.js 环境,看不到页面
- 渲染进程:浏览器环境,能看到页面,但碰不到 Node
preload 夹在两者之间,两边的能力都有,用来让他们能相互访问。
预加载脚本在渲染器加载网页之前注入。
若要向渲染器添加需要权限才能访问到功能,可以通过electron模块的contextBridgeAPI定义global对象。
实战:通过 preload 增强渲染器
下面实战一下。
新建一个preload.js文件,该脚本通过versions这一全局变量,将Electron的process.versions对象暴露给渲染器。
//preload.js
const { contextBridge } = require("electron");
contextBridge.exposeInMainWorld("versions", {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
// 除函数之外,我们也可以暴露变量
});为了将脚本附在渲染进程上,在BrowserWindow构造器上使用webPreferences.preload传入脚本路径。
// main.js
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()
})信息
- __dirname 字符串指向当前执行的脚本的路径。在本例中指向项目的根文件夹。
- path.joinAPI将多个路径连接在一起,创建一个跨平台的路径字符串。
提示
__dirname 是 Node 提供的当前文件所在目录,用 path.join 拼出绝对路径,避免相对路径在打包后失效。
于是,现在渲染器能够全局访问versions了。
新建一个render.js脚本。
const information = document.getElementById("info");
information.innerText = `本应用正在使用 Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), 和 Electron (v${versions.electron()})`;然后修改index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<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>
<script src="./render.js"></script>
</body>
</html>现在你的应用应该已经在页面里显示版本号了。
实战:进程间通信
之前说过,Electron 的主进程和渲染进程分工明确、不可互换:渲染进程直接访问不了 Node.js 接口,主进程也碰不到 HTML 的 DOM。解决办法是使用进程间通信(IPC)。
Electron 提供了两个模块来通信:
ipcMain—— 在主进程使用,负责监听ipcRenderer—— 在渲染进程使用,负责发送
下面演示:从网页向主进程发送一条消息,并拿到返回结果(ping → pong)。
1. preload 里暴露 invoke
在 preload 脚本中设置 invoke 调用,把它包装成一个全局函数 ping():
// preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("versions", {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
ping: () => ipcRenderer.invoke("ping"),
// 除函数之外,我们也可以暴露变量
});IPC 安全
注意这里用辅助函数包裹了 ipcRenderer.invoke("ping"),而不是直接把 ipcRenderer 模块暴露给页面。永远不要通过 preload 直接暴露整个 ipcRenderer——那会让渲染进程能向主进程发送任意 IPC 消息,成为恶意代码最强的攻击媒介。
2. 主进程注册 handler
在主进程用 ipcMain.handle 设置处理程序。要在 HTML 文件加载之前注册好,才能保证渲染进程发 invoke 时处理程序已就绪:
// main.js
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();
});3. 渲染进程调用
发送器和接收器都准备好后,渲染进程就能通过 "ping" 通道发消息给主进程了:
// renderer.js
const func = async () => {
const response = await window.versions.ping();
console.log(response); // 打印 'pong'
};
func();提示
更深入的 ipcRenderer 和 ipcMain 用法,见官方进程间通信指南。
附录
背景
早期 Electron 默认允许渲染进程直接用 Node(nodeIntegration: true)。但这样一旦页面被注入恶意脚本,它就能随意读写文件,非常危险。所以现在的默认配置是:
contextIsolation: true—— 页面和 preload 上下文隔离nodeIntegration: false—— 渲染进程不能用 Node APIsandbox: true—— 渲染进程跑在沙箱里
渲染进程被"关进笼子"后,页面要拿系统能力(读文件、取版本号、调主进程),就得走 preload 这条受控通道。
沙盒是什么
沙盒(Sandbox)是一个隔离箱:里面的代码能正常跑,但被限制碰不到外面的系统资源。
其实你天天都在用沙盒——浏览器里的网页就跑在沙盒里。一个网页不能读你电脑里的文件,就是因为它被关在沙盒里;就算代码是恶意的,也只能在箱子里折腾,伤不到箱子外的东西。
Electron 的渲染进程(页面)和浏览器网页一样,也在沙盒里:
┌─ 沙盒 ─────────────────────┐
│ 页面(渲染进程) │
│ 没有 Node.js,不能读文件 │
└─────────────────────────────┘
▲ 通过 IPC 喊话
│
┌─ 沙盒外 ───────────────────┐
│ 主进程:有完整系统权限 │ ← 能读文件、能开窗口
└─────────────────────────────┘页面想读文件只能喊话给沙盒外的主进程代办,这就是 preload 和 IPC 存在的原因。
预加载脚本沙盒化:preload 这个"传话人"自己也住在沙盒里,只是被特许带了几个"通行证"——只能 require 少数几个模块(contextBridge、ipcRenderer、events 等),足够干"传话"这件事。它比页面权限高一点,但比主进程低得多。
