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,
})

你项目在教程基础上还多了练习代码: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')

待补全(对照教程):

  1. main.js 在 app.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 | 渲染逻辑(主世界) | 用 versions;await ping() |


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

官方摘要:

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

你的复盘句(背这句):

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

相关推荐
月走乂山1 天前
DSH 桌面版用 DeepSeek 报 REQUEST_EXTENSION 的根因定位与修复
electron·node.js·插件系统·deepseek·bug排查·dsh·deepseek harness
mont evergreen2 天前
一封从外部世界寄来的信:二维码数据如何穿越三重门,抵达你的屏幕
electron·vue·事件
fellow993 天前
DSH Desktop:把 DeepSeek Harness 变成真正的桌面应用
electron·agent·ai编程·deepseek
CDwenhuohuo4 天前
electron pc项目打包成桌面端
前端·javascript·electron
不可能片场5 天前
命令行中文变问号 我用环境变量救了场
前端·electron
不可能片场5 天前
AI视频口型对齐 用音频驱动说话镜头
前端·electron
Zoom6 天前
【开源】7 天,我用 Rust 复刻了 FinalShell!Rhost v1.0.0 发布
rust·electron·shell
爱丶不疚7 天前
Electron: Sentry 都采集了什么?你知道吗
前端·electron
格数致用7 天前
03- Luckysheet与Vue3+Electron集成实战指南
electron·vue3·luckysheet