electron

Preload 脚本与上下文隔离

By AI-Writer 10 min read

前言

Electron 的渲染进程默认运行在浏览器的沙箱环境中,无法直接访问 Node.js API 或 Electron 原生模块。Preload 脚本是连接主进程与渲染进程的桥梁,通过 contextBridge 将有限的 API 安全地暴露给渲染进程。正确使用 Preload 是构建安全 Electron 应用的关键。

为什么需要 Preload

假设你有一个读取文件内容的需求:

typescript
// ❌ 渲染进程直接访问 Node.js fs 模块(不安全)
const fs = require('fs');
const content = fs.readFileSync('./config.json');

// ❌ 渲染进程直接调用主进程的 IPC
const { ipcRenderer } = require('electron');
ipcRenderer.invoke('file:read', './config.json');

上述做法的问题:

  • 渲染进程拥有完整的 Node.js 权限,任何前端代码都能读写本地文件、执行系统命令
  • 如果应用加载了第三方远程内容(如 WebView),后果不堪设想

Preload 脚本在 Chromium 加载页面之前运行,拥有 Node.js 访问权限,但页面加载后,它通过 contextBridge 将 API 暴露给渲染进程,而不是直接暴露整个 Node.js 环境。

contextBridge 基础

typescript
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';

// 暴露一个安全的 API 对象给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
  // 文件操作
  readFile: (path: string) => ipcRenderer.invoke('file:read', path),
  writeFile: (path: string, data: string) =>
    ipcRenderer.invoke('file:write', path, data),

  // 窗口控制
  minimizeWindow: () => ipcRenderer.invoke('window:minimize'),
  closeWindow: () => ipcRenderer.invoke('window:close'),

  // 事件订阅(返回移除函数)
  onNetworkChange: (callback: (status: string) => void) => {
    const handler = (_event: Electron.IpcRendererEvent, status: string) =>
      callback(status);
    ipcRenderer.on('network:status-change', handler);
    return () => ipcRenderer.removeListener('network:status-change', handler);
  },
});
typescript
// renderer.ts — 渲染进程中使用
// TypeScript 类型声明(可选但推荐)
declare global {
  interface Window {
    electronAPI: {
      readFile: (path: string) => Promise<{ success: boolean; data?: string; error?: string }>;
      writeFile: (path: string, data: string) => Promise<{ success: boolean; error?: string }>;
      minimizeWindow: () => Promise<void>;
      closeWindow: () => Promise<void>;
      onNetworkChange: (callback: (status: string) => void) => () => void;
    };
  }
}

// 使用示例
const result = await window.electronAPI.readFile('./config.json');
if (result.success) {
  console.log(result.data);
}

// 事件订阅
const unsubscribe = window.electronAPI.onNetworkChange((status) => {
  console.log('网络状态:', status);
});
// 组件卸载时取消订阅
unsubscribe();

安全配置组合

contextIsolation: true(必须)

contextIsolation 将渲染进程的 JavaScript 上下文与 Node.js 上下文隔离,防止相互污染:

typescript
// main.ts
const win = new BrowserWindow({
  width: 1200,
  height: 800,
  webPreferences: {
    // ✅ 推荐:启用上下文隔离
    contextIsolation: true,
    // ✅ 推荐:启用沙箱(Chromium 沙箱,不是 Node.js 沙箱)
    sandbox: true,
    // ❌ 禁止:禁用 Node.js 集成(渲染进程不运行 Node.js)
    nodeIntegration: false,
    // 必须指定 preload 脚本
    preload: path.join(__dirname, 'preload.js'),
  },
});

配置对比表

nodeIntegrationcontextIsolation安全性适用场景
falsetrue安全(推荐)生产应用
falsefalse不安全仅学习测试
truefalse极不安全弃用
truetrue不推荐(contextBridge 会失效)旧代码兼容

最佳实践:始终使用 nodeIntegration: false + contextIsolation: true + sandbox: true + preload 的组合。

sandbox 参数

sandbox: true 启用 Chromium 的进程沙箱,渲染进程无法:

  • 直接调用原生系统调用
  • 访问文件系统(即使 Node.js 可用)
  • 创建子进程

它是对 contextIsolation 的补充,不是替代:

typescript
// 最安全的配置
webPreferences: {
  nodeIntegration: false,
  contextIsolation: true,
  sandbox: true,          // Chromium 进程沙箱
  preload: path.join(__dirname, 'preload.js'),
}

Preload 执行时机

Preload 脚本在页面 DOM 构建之前执行,运行在独立的 Node.js 上下文中:

plaintext
Electron 启动
  └─→ 主进程加载
        └─→ BrowserWindow 创建
              └─→ Preload 脚本执行(Node.js 环境,可访问 Node/Electron API)
                    └─→ contextBridge 注册 API
                          └─→ 渲染进程页面加载(HTML/CSS/JS)
                                └─→ window.electronAPI 可用

这意味着 Preload 脚本无法访问渲染进程的 DOM,但可以访问 Node.js 和 Electron 主进程的 API。

API 暴露设计

遵循最小权限原则

只暴露业务必需的 API,不要暴露系统级能力:

typescript
// ❌ 暴露过多权限
contextBridge.exposeInMainWorld('electronAPI', {
  // 暴露了完整的 fs 模块,危险
  fs: require('fs'),
  // 暴露了 shell.openExternal,任意 URL 都可打开
  openExternal: (url: string) => shell.openExternal(url),
});

// ✅ 只暴露具体操作
contextBridge.exposeInMainWorld('electronAPI', {
  // 只读配置目录
  readConfig: () => ipcRenderer.invoke('config:read'),
  // 只允许打开预定义的 URL
  openDoc: (path: string) => ipcRenderer.invoke('shell:open-doc', path),
});

模块化 API 结构

typescript
// preload.ts — 按模块组织 API
const electronAPI = {
  // 文件模块
  file: {
    read: (path: string) => ipcRenderer.invoke('file:read', path),
    write: (path: string, data: string) =>
      ipcRenderer.invoke('file:write', path, data),
  },

  // 窗口模块
  window: {
    minimize: () => ipcRenderer.invoke('window:minimize'),
    maximize: () => ipcRenderer.invoke('window:maximize'),
    close: () => ipcRenderer.invoke('window:close'),
    isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
  },

  // 应用模块
  app: {
    getVersion: () => ipcRenderer.invoke('app:get-version'),
    quit: () => ipcRenderer.invoke('app:quit'),
  },

  // 事件订阅
  on: {
    networkChange: (cb: (s: string) => void) =>
      ipcRenderer.on('network:status-change', (_e, s) => cb(s)),
  },
};

contextBridge.exposeInMainWorld('electronAPI', electronAPI);
typescript
// renderer.ts — 使用
window.electronAPI.file.read('./config.json');
window.electronAPI.window.minimize();
window.electronAPI.app.getVersion();

处理返回值与错误

typescript
// preload.ts — 统一包装返回值
contextBridge.exposeInMainWorld('electronAPI', {
  safeRead: (path: string) =>
    ipcRenderer.invoke('file:read', path).then((result) => {
      if (result.error) {
        throw new Error(result.error);
      }
      return result.data;
    }),
});

Electron Forge + TypeScript 的 Preload 配置

typescript
// vite.preload.config.mjs
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    outDir: '.vite/build',
    lib: {
      entry: 'src/preload.ts',
      formats: ['cjs'],
      fileName: () => 'preload.js',
    },
    rollupOptions: {
      external: ['electron'],
    },
    minify: false,
  },
});
typescript
// main.ts — 注册 preload
import path from 'path';
import { BrowserWindow } from 'electron';

function createWindow() {
  return new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: true,
    },
  });
}

常见问题

渲染进程访问不到 window.electronAPI

检查以下几点:

typescript
// 1. 确认 preload 路径正确
webPreferences: {
  preload: path.join(__dirname, 'preload.js'), // 注意不是 .ts
}

// 2. 确认 contextIsolation 为 true
webPreferences: {
  contextIsolation: true, // 必须为 true
}

// 3. 确认 contextBridge 调用正确
contextBridge.exposeInMainWorld('electronAPI', { ... });

Preload 脚本报错 “require is not defined”

Preload 脚本运行在 Node.js 环境(不是浏览器环境),但 Vite 打包后可能出现问题。确保 Vite 配置正确:

javascript
// vite.preload.config.mjs
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['electron', 'path', 'fs'],
    },
  },
});

TypeScript 类型不生效

创建类型声明文件:

typescript
// src/preload/types.d.ts
export interface ElectronAPI {
  file: {
    read: (path: string) => Promise<{ success: boolean; data?: string; error?: string }>;
    write: (path: string, data: string) => Promise<{ success: boolean; error?: string }>;
  };
  window: {
    minimize: () => Promise<void>;
    maximize: () => Promise<void>;
    close: () => Promise<void>;
  };
  app: {
    getVersion: () => Promise<string>;
    quit: () => Promise<void>;
  };
  on: {
    networkChange: (callback: (status: string) => void) => () => void;
  };
}

declare global {
  interface Window {
    electronAPI: ElectronAPI;
  }
}

小结

  • Preload 脚本是渲染进程访问 Node.js 和 Electron API 的唯一安全途径
  • contextBridge.exposeInMainWorld 将 API 注入渲染进程的 window 对象
  • 最佳配置nodeIntegration: false + contextIsolation: true + sandbox: true
  • API 暴露遵循最小权限原则,只提供业务必需的接口
  • TypeScript 类型声明通过 declare global 扩展 Window 接口实现

下一篇文章我们将介绍 窗口管理与系统交互,包括 BrowserWindow 高级配置、系统托盘、全局快捷键等内容。

#electron #preload #contextBridge #安全 #sandbox

评论

A

Written by

AI-Writer

Related Articles

electron
#8

React / Vue 与 Electron 集成

使用 electron-vite 工具链整合 Vite + React/Vue + Electron,配置路由(React Router / Vue Router),实现主进程与渲染进程状态共享(Zustand IPC 桥接)

Read More
electron
#3

IPC 通信机制详解

深入理解 Electron 的 IPC 模块:invoke/handle 双向异步模式、ipcRenderer.on 事件监听、channel 命名规范、进程间数据传输规则

Read More
electron
#5

窗口管理与系统交互

掌握 BrowserWindow 高级配置(frame、transparent、kiosk)、多窗口管理、系统托盘 Tray、全局快捷键 globalShortcut,以及屏幕与窗口状态监控

Read More