contextBridge
contextBridge 是 Electron 提供的安全 API,用于在隔离的上下文之间安全地暴露 API。
为什么需要 contextBridge
在启用 contextIsolation 后,渲染进程的 JavaScript 运行在隔离的上下文中,无法直接访问 preload 脚本中的变量。contextBridge 提供了一种安全的方式来暴露特定 API。
基本用法
javascript
// preload.js
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
doSomething: () => console.log('Hello from preload'),
version: '1.0.0'
})javascript
// renderer.js
// 现在可以访问暴露的 API
window.myAPI.doSomething() // 'Hello from preload'
console.log(window.myAPI.version) // '1.0.0'API 详情
exposeInMainWorld(apiKey, api)
javascript
contextBridge.exposeInMainWorld('apiKey', {
// 字符串、数字、布尔值、数组、对象
version: '1.0.0',
count: 42,
isActive: true,
list: [1, 2, 3],
config: { theme: 'dark' },
// 函数
ping: () => 'pong',
// Promise
asyncMethod: async () => {
return await someAsyncOperation()
},
// 包含 DOM 类型(但有限制)
// Date, ArrayBuffer, Error 等
getDate: () => new Date()
})数据类型支持
支持的类型
| 类型 | 说明 |
|---|---|
| 基本类型 | string, number, boolean, null, undefined |
| 复杂类型 | Object, Array |
| 特殊类型 | Date, ArrayBuffer, Error, RegExp |
| 函数 | 普通函数和 async 函数 |
| Promise | 返回 Promise 的函数 |
不支持的类型
| 类型 | 原因 |
|---|---|
| DOM 元素 | 跨上下文不安全 |
| 函数返回的 DOM | 同上 |
| Class 实例 | 原型链会丢失 |
| Symbol | 无法序列化 |
使用示例
暴露 IPC 接口
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => {
const validChannels = ['toMain']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
const validChannels = ['fromMain']
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
},
invoke: async (channel, data) => {
const validChannels = ['dialog:open', 'file:read']
if (validChannels.includes(channel)) {
return await ipcRenderer.invoke(channel, data)
}
throw new Error('Invalid channel')
}
})暴露 Node.js 功能
javascript
// preload.js
const { contextBridge } = require('electron')
const os = require('os')
contextBridge.exposeInMainWorld('systemAPI', {
platform: process.platform,
homedir: os.homedir(),
hostname: os.hostname(),
cpus: os.cpus().length,
totalMemory: os.totalmem(),
freeMemory: os.freemem()
})暴露存储 API
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('storeAPI', {
get: (key) => ipcRenderer.invoke('store:get', key),
set: (key, value) => ipcRenderer.invoke('store:set', key, value),
delete: (key) => ipcRenderer.invoke('store:delete', key),
clear: () => ipcRenderer.invoke('store:clear')
})安全考虑
1. 不要暴露整个模块
javascript
// ❌ 危险!暴露了所有功能
contextBridge.exposeInMainWorld('electron', require('electron'))
// ❌ 危险!暴露了整个 ipcRenderer
contextBridge.exposeInMainWorld('ipc', ipcRenderer)
// ✅ 安全:只暴露需要的方法
contextBridge.exposeInMainWorld('api', {
send: (channel, data) => {
// 验证 channel
ipcRenderer.send(channel, data)
}
})2. 验证所有输入
javascript
contextBridge.exposeInMainWorld('fileAPI', {
read: (path) => {
// 类型检查
if (typeof path !== 'string') {
throw new TypeError('Path must be a string')
}
// 路径安全检查
if (path.includes('..') || path.startsWith('/etc/')) {
throw new Error('Access denied')
}
return ipcRenderer.invoke('file:read', path)
}
})3. 使用通道白名单
javascript
const ALLOWED_CHANNELS = {
invoke: ['dialog:open', 'file:read', 'store:get'],
send: ['window:minimize', 'window:close'],
on: ['update:progress', 'notification:show']
}
contextBridge.exposeInMainWorld('api', {
invoke: (channel, ...args) => {
if (!ALLOWED_CHANNELS.invoke.includes(channel)) {
throw new Error(`Channel not allowed: ${channel}`)
}
return ipcRenderer.invoke(channel, ...args)
}
})函数的特殊处理
通过 contextBridge 暴露的函数有特殊行为:
javascript
// preload.js
let counter = 0
contextBridge.exposeInMainWorld('counter', {
increment: () => ++counter,
get: () => counter
})
// renderer.js
window.counter.increment() // 1
window.counter.increment() // 2
window.counter.get() // 2函数在 preload 上下文中执行,可以访问该上下文的变量。
TypeScript 支持
typescript
// preload.ts
import { contextBridge, ipcRenderer } from 'electron'
interface ElectronAPI {
invoke: (channel: string, data?: unknown) => Promise<unknown>
send: (channel: string, data?: unknown) => void
on: (channel: string, callback: (data: unknown) => void) => () => void
}
declare global {
interface Window {
electronAPI: ElectronAPI
}
}
contextBridge.exposeInMainWorld('electronAPI', {
invoke: (channel, data) => ipcRenderer.invoke(channel, data),
send: (channel, data) => ipcRenderer.send(channel, data),
on: (channel, callback) => {
const handler = (_: unknown, data: unknown) => callback(data)
ipcRenderer.on(channel, handler)
return () => ipcRenderer.removeListener(channel, handler)
}
})完整示例
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
const CHANNELS = {
invoke: ['dialog:open', 'file:read', 'file:write'],
send: ['window:minimize', 'window:maximize', 'window:close'],
on: ['file:changed', 'update:available']
}
contextBridge.exposeInMainWorld('electronAPI', {
// 系统信息
platform: process.platform,
// IPC 通信
invoke: (channel, ...args) => {
if (!CHANNELS.invoke.includes(channel)) {
return Promise.reject(new Error(`Invalid channel: ${channel}`))
}
return ipcRenderer.invoke(channel, ...args)
},
send: (channel, ...args) => {
if (!CHANNELS.send.includes(channel)) {
return
}
ipcRenderer.send(channel, ...args)
},
on: (channel, callback) => {
if (!CHANNELS.on.includes(channel)) {
return () => {}
}
const handler = (event, ...args) => callback(...args)
ipcRenderer.on(channel, handler)
return () => ipcRenderer.removeListener(channel, handler)
},
once: (channel, callback) => {
if (!CHANNELS.on.includes(channel)) {
return
}
ipcRenderer.once(channel, (event, ...args) => callback(...args))
}
})