Preload 脚本与上下文隔离
前言
Electron 的渲染进程默认运行在浏览器的沙箱环境中,无法直接访问 Node.js API 或 Electron 原生模块。Preload 脚本是连接主进程与渲染进程的桥梁,通过 contextBridge 将有限的 API 安全地暴露给渲染进程。正确使用 Preload 是构建安全 Electron 应用的关键。
为什么需要 Preload
假设你有一个读取文件内容的需求:
// ❌ 渲染进程直接访问 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 基础
// 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);
},
});// 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 上下文隔离,防止相互污染:
// 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'),
},
});配置对比表
| nodeIntegration | contextIsolation | 安全性 | 适用场景 |
|---|---|---|---|
false | true | 安全(推荐) | 生产应用 |
false | false | 不安全 | 仅学习测试 |
true | false | 极不安全 | 弃用 |
true | true | 不推荐(contextBridge 会失效) | 旧代码兼容 |
最佳实践:始终使用 nodeIntegration: false + contextIsolation: true + sandbox: true + preload 的组合。
sandbox 参数
sandbox: true 启用 Chromium 的进程沙箱,渲染进程无法:
- 直接调用原生系统调用
- 访问文件系统(即使 Node.js 可用)
- 创建子进程
它是对 contextIsolation 的补充,不是替代:
// 最安全的配置
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true, // Chromium 进程沙箱
preload: path.join(__dirname, 'preload.js'),
}Preload 执行时机
Preload 脚本在页面 DOM 构建之前执行,运行在独立的 Node.js 上下文中:
Electron 启动
└─→ 主进程加载
└─→ BrowserWindow 创建
└─→ Preload 脚本执行(Node.js 环境,可访问 Node/Electron API)
└─→ contextBridge 注册 API
└─→ 渲染进程页面加载(HTML/CSS/JS)
└─→ window.electronAPI 可用这意味着 Preload 脚本无法访问渲染进程的 DOM,但可以访问 Node.js 和 Electron 主进程的 API。
API 暴露设计
遵循最小权限原则
只暴露业务必需的 API,不要暴露系统级能力:
// ❌ 暴露过多权限
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 结构
// 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);// renderer.ts — 使用
window.electronAPI.file.read('./config.json');
window.electronAPI.window.minimize();
window.electronAPI.app.getVersion();处理返回值与错误
// 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 配置
// 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,
},
});// 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
检查以下几点:
// 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 配置正确:
// vite.preload.config.mjs
export default defineConfig({
build: {
rollupOptions: {
external: ['electron', 'path', 'fs'],
},
},
});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 高级配置、系统托盘、全局快捷键等内容。
评论
Written by
AI-Writer
Related Articles
React / Vue 与 Electron 集成
使用 electron-vite 工具链整合 Vite + React/Vue + Electron,配置路由(React Router / Vue Router),实现主进程与渲染进程状态共享(Zustand IPC 桥接)
Read MoreIPC 通信机制详解
深入理解 Electron 的 IPC 模块:invoke/handle 双向异步模式、ipcRenderer.on 事件监听、channel 命名规范、进程间数据传输规则
Read More