学习记录:使用预加载脚本(教程第 3 部分)
一、本节位置与目标
|--------------|--------------------------------------------------------------|
| 项目 | 内容 |
| 教程位置 | 第 3 部分(前面:建项目、开窗口;后面:加功能、打包、发布) |
| 学习目标 | 理解预加载脚本;用 contextBridge 安全暴露 API;用 IPC 让主进程与渲染进程通信 |
本节要解决的核心问题 :主进程很强(Node + 系统),渲染进程很像网页(默认不能乱用 Node)------中间怎么安全地搭桥?
二、三种进程能力对比(先建立表格)
|---------------|-----------------------|-----------------------------------------------------------------|-------------------------------|
| 进程 | 环境 | 能做什么 | 不能做什么 |
| 主进程 | 完整 Node.js + Electron | 生命周期、窗口、文件、系统 API | 不能直接操作页面 DOM |
| 渲染进程 | 类似浏览器网页 | DOM、Web API、Vue/React UI | 默认不能直接 require('fs') 等 Node |
| 预加载脚本 | 介于两者之间 | 在页面加载之前 注入;有限 Node/Electron;用 contextBridge 暴露白名单 API | 不是给用户随便写业务 UI 的地方 |
分工原则(背下来):
- 主进程 ↔ 渲染进程:职责不可互换
- 渲染器要「特权能力」→ 走 preload + contextBridge + IPC
- 主进程要「页面信息」→ 也走 IPC,不要指望直接摸 DOM
三、什么是预加载脚本?
预加载脚本包含在浏览器窗口加载网页之前运行的代码。 其可访问 DOM 接口和 Node.js 环境,并且经常在其中使用contextBridge接口将特权接口暴露给渲染器。
由于主进程和渲染进程有着完全不同的分工,Electron 应用通常使用预加载脚本来设置进程间通信 (IPC) 接口以在两种进程之间传输任意信息。
通俗理解
预加载脚本 = 窗口里的「安检员 + 传话员」:
- 在 HTML/页面脚本加载之前 就先运行(类似 Chrome 扩展的 Content Script 注入时机)
- 能接触 一部分 Node / Electron API
- 通过 contextBridge.exposeInMainWorld,把你允许 的能力挂到页面的
window上(主世界 worldId 0)
Electron 20+:预加载默认沙盒化
从 Electron 20 起,preload 默认沙盒化,不再是「完整 Node 随便用」:
- 只有 polyfill 过的 require,只能加载有限模块
- 常见可用:Electron 模块、部分 Node 模块、部分全局对象
学习笔记 :不要假设 preload 里能 require 任意 npm 包;以官方「进程沙盒化」文档为准。
四、实战一:把版本号暴露给页面
4.1 流程(四步)
main.js 指定 preload 路径
↓
preload.js 用 contextBridge 暴露 versions
↓
index.html 引入 renderer.js
↓
renderer.js 读 versions.xxx() 更新 DOM
4.2 关键代码与职责
① main.js ------ 挂上 preload
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
}
|------------------|------------------------------------------|
| 概念 | 作用 |
| __dirname | 当前正在执行的脚本所在目录(你的 main.js 所在文件夹,一般是项目根) |
| path.join(...) | 拼跨平台路径,避免手写 \ / / |
你项目里已配置:
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
② preload.js ------ 暴露 API(目标:主世界 world 0)
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
})
你项目在教程基础上还多了练习代码:myfn、myObj、num1、process 等------有助于理解「能暴露函数、对象、嵌套」,但 process: () => process********整包暴露要谨慎(安全与克隆规则),生产环境更推荐只暴露需要的字段。
③ renderer.js ------ 当普通网页 JS 用
versions.chrome() // 等价 window.versions.chrome()
你当前实现:
const information = document.getElementById('info')
information.innerText = `... ${versions.chrome()} ...`
// + myfn、myObj 等
④ index.html
<p id="info"></p>占位<script src="./renderer.js"></script>- CSP:
script-src 'self'→ 只加载本地脚本,符合安全基线
五、实战二:IPC ------ 主进程与渲染进程说话
5.1 为什么需要 IPC?
|-----------------------|---------------------------------------|
| 需求 | 正确做法 |
| 页面按钮 → 读本地文件 | 渲染器 invoke → 主进程 handle 读文件 |
| 主进程 → 通知页面更新 | webContents.send + preload 里封装 on |
| 渲染器直接 require('fs') | ❌ 默认不应这样做 |
5.2 教程里的 ping / pong 完整链路
sequenceDiagram
participant R as renderer.js (world 0)
participant P as preload.js (world 999)
participant M as main.js (主进程)
R->>P: window.versions.ping()
P->>M: ipcRenderer.invoke('ping')
M-->>P: return 'pong'
P-->>R: Promise resolve 'pong'
|------------|-------------|------------------------------------------|
| 步骤 | 文件 | 代码要点 |
| 1 暴露封装好的调用 | preload.js | ping: () => ipcRenderer.invoke('ping') |
| 2 主进程注册处理 | main.js | ipcMain.handle('ping', () => 'pong') |
| 3 页面发起调用 | renderer.js | await window.versions.ping() |
你 preload 已写好 ping:
ping: () =>ipcRenderer.invoke('ping')
待补全(对照教程):
-
main.js 在
app.whenReady()里、createWindow()********之前注册:const { ipcMain } = require('electron')
app.whenReady().then(() => {
ipcMain.handle('ping', () => 'pong')
createWindow()
}) -
renderer.js 里异步测试:
const func = async () => {
const response = await window.versions.ping()
console.log(response) // 'pong'
}
func()
5.3 IPC 安全(必背红线)
// ❌ 永远不要这样
contextBridge.exposeInMainWorld('ipc', ipcRenderer)
// ✅ 只暴露白名单方法
ping: () => ipcRenderer.invoke('ping')
原因 :整包 ipcRenderer 会让页面代码能向主进程发任意频道的消息,等于把后台钥匙交给前台,恶意脚本危害极大。
Web 类比 :像只提供 api.getUser(),而不是把整个 fetch + 管理员 Token 挂在 window 上。
六、和你已学知识的串联
|-----------------------------------|--------------------------------------------|
| 前面学过的 | 本节怎么用 |
| app / BrowserWindow | webPreferences.preload 把脚本绑到窗口 |
| contextBridge.exposeInMainWorld | 注入到 world 0 ,给 renderer.js / Vue |
| exposeInIsolatedWorld | 本节教程不用;日常开发也极少用 |
| Web 的 window.SDK | ≈ exposeInMainWorld('versions', {...}) |
| Web 的 fetch 调后端 | ≈ invoke 调主进程 |
七、对接 Vue3 的预习笔记
main.js → 创建窗口、ipcMain.handle
preload.js → contextBridge.exposeInMainWorld('electronAPI', {...})
src/main.ts (Vue) → window.electronAPI.ping()
建议统一一个名字(如 electronAPI),Vue 里可再包一层 composable:useElectron(),组件不直接散落 window.xxx。
八、本节文件职责清单(复习用)
|---------------------|------------|-------------------------------|
| 文件 | 角色 | 本节要点 |
| main.js | 主进程 | preload 路径;ipcMain.handle |
| preload.js | 预加载(隔离世界) | contextBridge;封装 invoke |
| index.html | 页面结构 | CSP;#info;引 renderer |
| renderer.js | 渲染逻辑(主世界) | 用 versions;await ping() |
九、官方摘要 + 你的复盘句
官方摘要:
- Preload 在网页加载之前 运行,常用
contextBridge把特权接口交给渲染器。 - 主/渲染分工不同,靠 preload + IPC 传信息。
你的复盘句(背这句):
主进程管系统和窗口;渲染进程管界面;preload 在页面加载前用 contextBridge********只暴露白名单 API;跨进程办事用 IPC,且只封装 invoke/on****,绝不暴露整个**** ipcRenderer****。****