首页
/ 使用 Preload 脚本与 IPC 安全打通主进程与渲染进程:Electron 官方教程(三)实战解读

使用 Preload 脚本与 IPC 安全打通主进程与渲染进程:Electron 官方教程(三)实战解读

2026-09-07 11:21:54作者:毕习沙Eudora

本篇文章围绕 Electron 官方教程第三部分「使用 Preload 脚本」展开,讲解 Preload 脚本的概念、沙箱环境下的可用 API、如何借助 contextBridge 向渲染进程安全地暴露特权能力,并通过 ipcMain.handleipcRenderer.invoke 搭建最小可用的进程间通信(IPC)链路。学完后你可以独立实现一个「主进程持有系统能力、渲染进程负责界面展示、二者通过受控的 preload 桥接层通信」的标准 Electron 应用架构。

学习目标

本教程部分对应仓库中的 docs/tutorial/tutorial-3-preload.md,它是官方教程的第三站,前面依次是 环境准备创建第一个应用,后续还有 为应用添加功能打包应用发布与更新

完成本部分你将掌握两件事:

  1. 理解 preload 脚本是什么、在什么时机运行、拥有哪些权限边界;
  2. 学会用它把「有特权的 API」安全地暴露给渲染进程,并基于 IPC 打通主进程与渲染进程的通信。

为什么需要 preload 脚本

Electron 的主进程是一个完整的 Node.js 环境,除了 Electron 自身模块,还能直接使用 Node.js 内置模块(fspathos 等)以及通过 npm 安装的任意第三方包,因此它对操作系统拥有完整的访问能力。

与之相对,渲染进程负责运行网页内容,出于安全原因默认不运行 Node.js,也接触不到主进程那一整套系统级 API。它只能执行标准的 Web(DOM、Canvas、fetch 等)能力。

这种设计带来的结果是:主进程与渲染进程职责不同、能力互不通用——渲染进程无法直接调用 Node.js API,主进程也无法直接操作网页的 DOM。要弥合两者,Electron 提供了一种特殊脚本,就是 preload:一个运行在渲染进程里、但能"看见"部分 Node.js 与 Electron API 的中间层,充当两个进程之间的桥梁。

用 preload 脚本增强渲染进程

一个 BrowserWindow 的 preload 脚本,运行在一个同时能访问 HTML DOM 和有限 Node.js/Electron API 的上下文里。它会在网页正式加载之前被注入(机制上类似 Chrome 扩展中的 content scripts),先于页面代码执行完毕。

因此,凡是需要特权访问(读文件、查版本、调系统能力等)却又要在页面上使用的功能,正确的做法是:在 preload 中通过 contextBridge API 定义全局对象,再交给渲染进程的页面脚本使用。

Electron 20 起默认沙箱化

需要特别留意的是,从 Electron 20 开始,preload 脚本默认处于沙箱(sandbox)状态,不再拥有完整的 Node.js 环境。从源码看,Electron 将沙箱与非沙箱两类 preload 入口分置于不同目录:lib/sandboxed_renderer/preload.tslib/preload_realm/init.ts,而沙箱模式下渲染进程能 require 到的 API 白名单由 lib/sandboxed_renderer/api/module-list.ts 定义并经由 lib/sandboxed_renderer/api/exports/electron.ts 装配导出。

实际效果就是:preload 里的 require 是一个经过 polyfill 的函数,只能访问下面这组受限 API:

可用 API 详细说明
Electron 模块 渲染进程模块(如 contextBridgeipcRenderer,详见 electron 导出面)
Node.js 模块 eventstimersurl
Polyfilled 全局 Bufferprocessprocess 文档)、clearImmediatesetImmediate

沙箱机制、context isolation 与安全基线更完整的讨论可参考 进程沙箱说明 sandbox.md(原文档配套章节)(若仓库当前版本未携带该文件,请以 security.mdcontext-isolation.md 为准)。

实战:把进程版本号暴露给页面

作为演示,我们做一个 preload 脚本,把 Electron process.versions 中的 Chrome、Node、Electron 版本通过 contextBridge.exposeInMainWorld 暴露到渲染进程的 versions 全局变量中。

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('versions', {
  node: () => process.versions.node,
  chrome: () => process.versions.chrome,
  electron: () => process.versions.electron
  // 这里同样可以暴露普通变量,而不只是函数
})

然后,在 BrowserWindow 构造函数的 webPreferences.preload 选项中传入该脚本的绝对路径,把它挂到渲染进程上:

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

这段代码用到了两个 Node.js 概念:

  • __dirname:指向当前正在执行脚本所在目录的字符串(本例即项目根目录);
  • path.join:把多个路径片段拼成一个完整路径,好处是跨平台统一(Windows 用 \、macOS/Linux 用 /,它都会正确处理)。

在页面里读取暴露的全局

此时渲染进程里已经有了 versions 这个全局对象,可以写 window.versions(或简写 versions)。新建 renderer.js,用 DOM API document.getElementById 把版本信息渲染到页面上:

const information = document.getElementById('info')
information.innerText = `This app is using Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), and Electron (v${versions.electron()})`

然后修改 index.html:新增一个 id="info" 的空元素,并引入 renderer.js

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <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>
  </body>
  <script src="./renderer.js"></script>
</html>

注意 index.html 里同时声明了 Content-Security-PolicyX-Content-Security-Policy,内容为 default-src 'self'; script-src 'self',即只允许加载同源资源与脚本——教程从「最小 Hello World」开始就刻意贯彻安全基线,这一点与官方 security.md 的推荐一致。

运行后应用会输出类似 "This app is using Chrome (v...), Node.js (v...), and Electron (v...)" 的版本信息:

preload 版本展示示例:页面通过 preload 暴露的 versions 对象显示 Chrome、Node.js 与 Electron 版本号

教程配套的完整可运行代码位于仓库 docs/fiddles/tutorial-preload 目录,包含 main.jspreload.jsrenderer.jsindex.html 四个文件;其中 preload.js 使用 require('electron/renderer') 引入 contextBridge,这与教程正文的 require('electron') 写法在沙箱 preload 中等价,均指向渲染进程 API 导出面。

仓库的真实应用也遵循同一模式:Electron 自带默认应用入口 default_app/preload.ts 中先 contextBridge.exposeInMainWorld('electronDefaultApp', {...}) 暴露初始化入口,再在其中通过 ipcRenderer.invoke('bootstrap') 与主进程通信取回 Electron 可执行文件路径,进而渲染版本号——这正是本文所讲桥接层在实际代码中的范本。

进程间通信(IPC):ping/pong 示例

展示版本只需要 preload 单向读取 process.versions 即可,但多数功能需要「页面发起请求 → 主进程处理 → 结果回传」。Electron 为此提供 ipcMain(主进程侧)与 ipcRenderer(渲染进程侧)两个模块组成 IPC 通道。

推荐的请求-响应模式是:主进程用 ipcMain.handle 注册处理器,渲染进程用 ipcRenderer.invoke 调用它。下面给渲染进程增加一个返回字符串 'pong' 的全局函数 ping()

第一步: 在 preload 里增加 invoke 调用:

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('versions', {
  node: () => process.versions.node,
  chrome: () => process.versions.chrome,
  electron: () => process.versions.electron,
  ping: () => ipcRenderer.invoke('ping')
  // 同样可以暴露变量,而不只是函数
})

:::caution IPC 安全红线 注意我们把 ipcRenderer.invoke('ping') 包在了一个辅助函数里,而不是把 ipcRenderer 模块本身交给 context bridge。永远不要通过 preload 直接暴露整个 ipcRenderer 模块——那等于把「向主进程发送任意 IPC 消息」的能力拱手交给渲染进程里的任何代码,一旦页面被注入恶意脚本,它就成了威力巨大的攻击面。官方对这条约束的完整论述见 context-bridge 文档。 :::

第二步: 在主进程中注册 handle 监听器。必须放在加载 HTML 之前(即 whenReady 回调里、loadFile 之前),确保页面一发起 invoke 时处理器已经就绪:

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

第三步: 发送端与接收端就绪后,渲染进程即可通过刚定义的 'ping' 频道发起消息:

const func = async () => {
  const response = await window.versions.ping()
  console.log(response) // 打印出 'pong'
}

func()

由于 invoke 返回 Promise,调用侧必须 await。关于 ipcRenderer / ipcMain 更深入的用法(双向通信、send/on 事件模型、端口消息等),可继续阅读 IPC 完整指南

从源码看这条通信链路如何落地

为了印证上面的流程确实运行在真实实现之上,可以回到本仓库源码追溯关键环节:

  • 渲染侧 IPC 封装ipcRenderer.invoke 的真正实现位于 lib/renderer/ipc-renderer-bindings.ts,它内部会把调用序列化并通过 IpcRenderer 的原生绑定发送到主进程;
  • 主进程消息分发:主进程侧 ipcMain.handle 的注册与分发在 lib/browser/ipc-main-impl.ts 中维护,收到消息后按频道名查找对应 handler 执行并回传结果;
  • 内部消息定义:频道名常量集中定义于 lib/common/ipc-messages.ts,例如 default_app/preload.ts 中调用的 'bootstrap' 频道即属于此类约定——真实应用与教程一样,通过约定的频道字符串完成跨进程协作。

从代码结构可以推断:ipcMain.handle 注册的是一张「频道 → 异步处理函数」的映射表,ipcRenderer.invoke 发出的请求会在主进程中查表执行并经由 Promise 语义把返回值送回调用方,因此整个链路天然支持异步与错误传播,这也是教程推荐 handle/invoke 作为首选通信模式的原因。

总结

preload 脚本是在网页加载进浏览器窗口之前运行的代码,它同时拥有 DOM API 与受限的 Node.js 环境,最常用的职责是通过 contextBridge 把特权 API 安全地暴露给渲染进程。

由于主进程与渲染进程职责差异巨大,Electron 应用通常还会借助 preload 脚本搭起 IPC 接口,让两类进程之间可以互相传递任意消息——本文实现的 versions 只读桥接与 ping/pong 往返,就是这套架构最精简的骨架,也应该是你后续所有 Electron 应用中「页面需要调用系统能力」时的标准模板。

接下来可以进入教程第四部分 为应用添加更多功能,随后学习如何把应用 打包分发发布更新

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388