{T}

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

参考链接