2.预加载(preload)

学习记录:使用预加载脚本(教程第 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) 接口以在两种进程之间传输任意信息。

通俗理解

预加载脚本 = 窗口里的「安检员 + 传话员」

  1. HTML/页面脚本加载之前 就先运行(类似 Chrome 扩展的 Content Script 注入时机)
  2. 能接触 一部分 Node / Electron API
  3. 通过 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,
})

你项目在教程基础上还多了练习代码:myfnmyObjnum1process 等------有助于理解「能暴露函数、对象、嵌套」,但 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')

待补全(对照教程)

  1. main.jsapp.whenReady() 里、createWindow()********之前注册:

    const { ipcMain } = require('electron')
    app.whenReady().then(() => {
    ipcMain.handle('ping', () => 'pong')
    createWindow()
    })

  2. 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 | 渲染逻辑(主世界) | 用 versionsawait ping() |


九、官方摘要 + 你的复盘句

官方摘要

  • Preload 在网页加载之前 运行,常用 contextBridge 把特权接口交给渲染器。
  • 主/渲染分工不同,靠 preload + IPC 传信息。

你的复盘句(背这句)

主进程管系统和窗口;渲染进程管界面;preload 在页面加载前用 contextBridge********只暴露白名单 API;跨进程办事用 IPC,且只封装 invoke/on****,绝不暴露整个**** ipcRenderer****。****

相关推荐
七十二時_阿川5 小时前
Electron 如何自定义菜单?这篇帮你实现原生体验!
前端·electron
七十二時_阿川5 小时前
Electron App 速查表:生命周期事件、方法、平台差异
前端·electron
七十二時_阿川5 小时前
Electron 多显示器开发?这篇帮你搞定屏幕坐标与窗口定位!
前端·electron
七十二時_阿川6 小时前
Electron Tray API 详解:托盘图标、右键菜单、气泡通知
前端·electron
七十二時_阿川1 天前
Electron WebContents 完全指南:页面渲染、导航控制与安全实战
前端·electron
七十二時_阿川1 天前
Electron 主进程和渲染进程如何通信?这篇讲清楚了
前端·electron
七十二時_阿川1 天前
从零到精通:Electron 窗口管理高级技巧
前端·electron
会周易的程序员1 天前
aiDgeScanner:工业设备扫描与管理的一体化利器——深度解析上位机与扫描端的无缝协作
c++·物联网·typescript·electron·vue·iot·aiot
三声三视1 天前
Electron 在鸿蒙 PC 上启动慢?我把冷启动从 7 秒压到 1.5 秒的完整记录
electron·harmonyos·桌面应用