← [ BACK_TO_NOTES ]

Electron preload 脚本

preload 脚本运行在 renderer 页面加载前,是 Electron 中建立最小权限桥接的安全边界;在默认 sandbox + contextIsolation 下应通过 contextBridge 暴露窄 API,而不是把 Node 或 ipcRenderer 整体交给页面。

#desktop #electron #concept #evergreen

[!info] related notes

Electron preload 脚本

一句话定义

preload 是 BrowserWindow 在页面脚本执行前加载的一段桥接代码。

它的核心职责不是“让 renderer 获得 Node”,而是:

在高权限 Electron/Main 能力与低权限页面之间暴露最小、稳定、可验证的 API。

当前默认安全模型

现代 Electron 的默认模型包含两个关键变化:

  • Electron 12 起,contextIsolation 默认开启;
  • Electron 20 起,renderer sandbox 默认开启。

因此默认 preload:

不再拥有完整 Node.js 环境。

在 sandboxed renderer 中,preload 只有受限的 polyfilled Node/Electron API 子集,例如部分 electron 模块以及 events、timers、url 等。

所以旧心智模型:

preload
= full Node access

在现代默认配置下已经不准确。

contextIsolation

启用 contextIsolation 后:

preload
→ Isolated World

web page
→ Main World

两个 world 不共享同一个普通 window 对象。

因此:

// preload
window.myApi = ...

不能再作为推荐桥接方式。

应使用:

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('appApi', {
  ...
})

preload 应暴露什么

优先暴露 业务语义 API:

window.appApi = {
  openFile: () => Promise<FileInfo>,
  saveSettings: (input) => Promise<void>,
  getVersion: () => Promise<string>,
}

而不是:

window.electron = {
  ipcRenderer,
  fs,
  shell,
  ...
}

前者表达 capability contract。

后者把底层权限面直接暴露给页面。

IPC 的正确边界

典型调用链:

Renderer
→ window.appApi.openFile()
→ preload validates / maps
→ ipcRenderer.invoke('file:open')
→ main ipcMain.handle(...)
→ privileged operation
→ serializable result
→ Renderer

preload 可以:

  • 限制可调用 channel;
  • 校验参数;
  • 映射返回值;
  • 隐藏底层 IPC 细节;
  • 把 Electron API 变成稳定应用 contract。

为什么不要暴露整个 ipcRenderer

Electron 当前明确禁止把整个 ipcRenderer 通过 contextBridge 直接传给 renderer。

从 Electron 29 起,这种整体传递也被 API 层进一步限制。

即使技术上能封装出通用:

send(channel, payload)
invoke(channel, payload)

如果页面可以任意指定 channel,仍然相当于扩大攻击面。

更安全的设计是:

one narrow method
→ one known capability
→ explicit validation

Sandbox 与 preload

sandboxed renderer 没有完整 Node.js 环境。

如果 preload 依赖:

  • 任意 npm CommonJS module;
  • filesystem;
  • child_process;
  • native Node module;

不能默认认为它会继续工作。

更稳的方向是:

renderer/preload
→ IPC
→ main process
→ privileged Node operation

而不是为了方便直接关闭 sandbox。

preload 与 main 的职责

preload

负责:

  • capability exposure;
  • renderer-facing contract;
  • 参数边界;
  • IPC adapter。

main

负责:

  • filesystem;
  • process;
  • native integration;
  • window/session;
  • OS capability;
  • privileged business action。

如果 preload 开始承担大量 domain logic 或 OS 操作,边界通常已经变厚。

preload 与普通前端初始化脚本

普通 renderer script:

  • 负责 UI;
  • 运行在页面环境;
  • 不应拥有 Electron 高权限。

preload:

  • 更早执行;
  • 位于安全边界;
  • 负责受控桥接;
  • 不应该成为 UI bootstrap。

一个推荐结构

main/
  ipc/
    file.ts
    settings.ts

preload/
  index.ts
  api-types.ts

renderer/
  app.tsx

其中:

api-types
→ renderer 可见 contract

preload
→ contract adapter

main/ipc
→ privileged implementation

常见失败模式

万能 IPC

页面可以传任意 channel。

→ capability boundary 失效。

关闭 contextIsolation / sandbox 只为了省事

→ renderer compromise 的权限后果显著扩大。

preload 里写大量业务逻辑

→ main/preload/renderer 责任混乱。

返回复杂高权限对象

应该返回可序列化、最小必要的数据,而不是底层 Electron/Node object。

学习检查

  1. preload 的核心职责为什么不是“给 renderer Node 权限”?
  2. Electron 12 与 Electron 20 分别改变了什么默认安全边界?
  3. contextIsolation 下 preload 与页面为什么不能直接共享普通 window object?
  4. 为什么 openFile() 比 invoke(channel, payload) 更适合作为 renderer API?
  5. sandboxed preload 为什么不能假定拥有完整 Node.js?
  6. preload 与 main process 应怎样分工?
  7. 如果一个 renderer 需要读取本地配置文件,设计 renderer → preload → main 的最小 API。
创建于 2026/4/13 更新于 2026/9/30
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本