人人都会AI编程

8.2 预加载脚本 preload 核心用法

更新时间:2026-07-11

在 Electron 应用里,preload 脚本是连接主进程和渲染进程的安全桥梁。它的职责很纯粹:在渲染进程的全局作用域中,预先注入一组经过严格筛选的 API,让页面能够安全地调用那些原本只属于主进程的系统能力。

你不必把它想得过于复杂。本质上,preload 就是一个在页面加载之前运行的 Node.js 环境脚本,它有权限使用 require 加载 Electron 和 Node.js 的模块,但它不会直接把整个 require 能力暴露给渲染进程。这一点是它的核心设计目标:能力可用,权限可控

8.2.1 contextBridge 暴露安全 API

从 Electron 12 开始,官方推荐启用 contextIsolation: true(这也是默认值),这意味着渲染进程的 JavaScript 环境与 preload 脚本的 Node.js 环境是完全隔离的。你想让渲染进程使用什么功能,就必须通过 contextBridge.exposeInMainWorld 显式地“挂载”到 window 对象上。

这是一种白名单机制,你在 preload 里写了什么,渲染进程就只能用到什么,杜绝了恶意脚本直接拿到 require('fs') 的可能。

下面是一个日常开发中最常见的 pattern:暴露一个安全的 electronAPI 对象,里面只提供具体的方法,而不暴露任何模块引用。

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  // 打开文件选择对话框
  openFileDialog: () => ipcRenderer.invoke('dialog:openFile'),
  
  // 获取应用版本号
  getAppVersion: () => ipcRenderer.invoke('app:getVersion'),
  
  // 监听主进程发来的消息(比如更新进度)
  onUpdateProgress: (callback) => {
    ipcRenderer.on('update:progress', (_event, value) => callback(value));
  },
  
  // 移除监听
  removeUpdateProgressListener: () => {
    ipcRenderer.removeAllListeners('update:progress');
  }
});

在渲染进程的代码里,你就可以像调用普通函数一样使用它:

// renderer.js (在页面里)
document.getElementById('btn').addEventListener('click', async () => {
  const filePath = await window.electronAPI.openFileDialog();
  console.log('选择的文件:', filePath);
});

window.electronAPI.onUpdateProgress((progress) => {
  console.log(`下载进度: ${progress}%`);
});

注意,在整个渲染进程中,你始终不需要(也不应该)写 require('electron'),所有系统能力都通过 window.electronAPI 的有限接口来获得。

8.2.2 主进程配合 invoke/handle 模式

上面的例子用了 ipcRenderer.invoke,它需要主进程用 ipcMain.handle 来响应。这是一种返回 Promise 的双向通信方式,非常适合“请求-响应”风格的调用,代码清晰且易于维护。

主进程侧对应代码:

// main.js
const { ipcMain, dialog, app } = require('electron');

ipcMain.handle('dialog:openFile', async () => {
  const result = await dialog.showOpenDialog({ properties: ['openFile'] });
  return result.filePaths[0] || null;
});

ipcMain.handle('app:getVersion', () => {
  return app.getVersion();
});

如果需要从主进程主动推送消息给渲染进程(比如下载进度、系统通知),则保留传统的 webContents.send 模式,在 preload 中暴露一个基于回调的监听方法。

// main.js 中向渲染进程发送消息
mainWindow.webContents.send('update:progress', 45);

preload 里通过 ipcRenderer.on 接收,再传给回调,这部分在上面的 preload 示例中已经体现。

8.2.3 常见错误与注意事项

即使 preload 的设计已经很安全,日常开发中还是容易出现两个典型错误。

错误一:把整个 ipcRenderer 暴露出去

// ❌ 极度危险的做法
contextBridge.exposeInMainWorld('electronAPI', {
  ipcRenderer: ipcRenderer
});

这样做等于给了渲染进程随意发送任何 IPC 消息的能力,并且可以监听所有频道,一旦页面存在 XSS 漏洞,攻击者就能够调用你主进程中的所有 ipcMain.handleipcMain.on 处理函数,后果难以控制。正确的做法永远是只暴露具体的方法,每个方法内部写死 IPC 频道和参数结构。

错误二:在 preload 中直接操作 DOM

preload 脚本可以访问 DOM,但你不应该在里面操作 DOM,因为这容易造成渲染进程代码和 preload 间的耦合混乱。preload 的唯一职责就是注入 API,页面逻辑全部交给渲染进程的 JS 脚本处理。

// ❌ 不推荐
window.addEventListener('DOMContentLoaded', () => {
  document.getElementById('status').textContent = 'Preload 完成';
});

这种行为应该放到渲染进程自己的 renderer.js 中,preload 保持纯净。

错误三:误用同步 IPC 导致窗口卡死

老版本的 Electron 常常使用 ipcRenderer.sendSync,它是同步阻塞的,会冻结整个渲染进程直到主进程返回。在 contextIsolation 环境下,同步 IPC 更是一个风险源。现在最佳实践是全部使用异步的 invoke/handle 模式,它会返回 Promise,既安全又不阻塞 UI。

8.2.4 实战:一个安全的“本地文件读取”例子

让我们串联起一个完整的小场景:页面上有一个按钮,点击后弹出系统原生的文件选择框,选中一个文本文件,主进程读取内容后返回给页面显示。这个需求很常见,正好能展示 preload 的正确用法。

preload.js

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

contextBridge.exposeInMainWorld('fileAPI', {
  // 读取文本文件,返回内容字符串
  readTextFile: () => ipcRenderer.invoke('file:readText')
});

主进程 main.js 片段

const { ipcMain, dialog } = require('electron');
const fs = require('fs');

ipcMain.handle('file:readText', async () => {
  const { filePaths } = await dialog.showOpenDialog({
    filters: [{ name: 'Text Files', extensions: ['txt', 'md'] }]
  });
  if (filePaths.length === 0) return null;
  const content = fs.readFileSync(filePaths[0], 'utf-8');
  return content;
});

渲染进程 renderer.js

document.getElementById('readBtn').addEventListener('click', async () => {
  try {
    const text = await window.fileAPI.readTextFile();
    if (text !== null) {
      document.getElementById('content').textContent = text;
    }
  } catch (err) {
    console.error('读取文件失败:', err);
  }
});

整个流程中,渲染进程完全不知道 fs 模块的存在,甚至不知道自己在和系统文件对话框交互,它只是调用了 window.fileAPI.readTextFile() 并得到了一个字符串。主进程对每次调用做了完整的权限控制(比如这里限制只能选择文本文件),安全性自始至终处于可控状态。


preload 脚本的用法并不复杂,但它是构建安全 Electron 应用的基石。记住两条金律:永远使用 contextBridge 暴露白名单 API,永远使用异步 IPC。这样写出来的代码不仅安全可靠,而且接口清晰,新人接手时也能一眼看懂主进程和渲染进程之间的通信契约。