Electron: 沙箱 + preload CJS

2025年10月27日星期一

本文档解决方案 Powered by GPT-5-Thinking & Codex,有问题请找它们😡

Electron 自 v28 版本后已经支持了 ESM,但若要保证沙箱化(sandbox: true)的情况下,就无法使用 ESM Preload,因此我们采用 preload CJS + main/renderer ESM 方案。

启用 sandbox: true 时,preload 不能用 ESM,必须打成“单文件”的 CJS,并且不要外部化三方依赖。

若要保证 CJS preload 正常运行,需要满足两件事:

  1. 把 preload 依赖全都打包进单文件
  2. 维持 sandbox: true、contextIsolation: true、nodeIntegration: false 的组合(Electron v28 以后都是这个默认配置,无需显式指定),main 进程指向 CJS preload 打包后的文件。

preload 用到的第三方依赖全都打包进单文件

沙箱不允许 runtime 再去 require 其它模块,所以得在构建期把需要的代码打进 src/preload/index.ts 的产物里。

Electron 应用基于 electron-vite 开发,可以通过 electron-vite.config.ts 配置 CJS,我们当前在 src/preload/index.ts 中用到了第三方依赖 @electron-toolkit/preload

对于 Node/Electron API 相关 API 无需打包,在启用 sandbox 的 preload 里本来就会注入受控的 Node polyfill 和 electron 模块供预加载脚本 调用

  • electron-vite.config.ts
export default defineConfig({
  preload: {
    plugins: [externalizeDepsPlugin({ exclude: ['@electron-toolkit/preload'] })],
    build: {
      rollupOptions: {
        input: resolve(__dirname, 'src/preload/index.ts'),
        output: { format: 'cjs', entryFileNames: 'index.cjs' },
      },
    },
  }
})

main 进程配置

  • src/main/index.ts
const mainWindow = new BrowserWindow({
  width: 720,
  height: 88,
  webPreferences: {
    preload: join(__dirname, '../preload/index.cjs'), // index.cjs 而非 index.mjs,与上一步配置对应
  },
})

迁移到 CJS preload 的文件变动

  electron.vite.config.ts

  - Preload build now keeps @electron-toolkit/preload inline and defines explicit Rollup options for both main (ESM) and preload (CJS)
    outputs, including entryFileNames: 'index.cjs'.

    main: {
       plugins: [externalizeDepsPlugin()],
       resolve: { ... },
  +    build: {
  +      rollupOptions: {
  +        input: resolve(__dirname, 'src/main/index.ts'),
  +        output: { format: 'es' },
  +      },
  +    },
    },
    preload: {
  -    plugins: [externalizeDepsPlugin()],
  +    plugins: [externalizeDepsPlugin({ exclude: ['@electron-toolkit/preload'] })],
       resolve: { ... },
  +    build: {
  +      rollupOptions: {
  +        input: resolve(__dirname, 'src/preload/index.ts'),
  +        output: { format: 'cjs', entryFileNames: 'index.cjs' },
  +      },
  +    },
    },

  src/main/index.ts

  - Switched the preload script path back to index.cjs for main window

       webPreferences: {
  -      preload: join(__dirname, '../preload/index.mjs'),
  +      preload: join(__dirname, '../preload/index.cjs'),
       },

  src/preload/index.ts

  - Dropped import process from 'node:process' and instead silenced the lint warning to keep using the runtime-provided process.

  -import process from 'node:process'
  ...
   // just add to the DOM global.
  +// eslint-disable-next-line node/prefer-global/process
   if (process.contextIsolated) {

CJS preload 配置不当会出现的问题

  • sandbox:true 情况下,未正常配置 cjs preload
VM4 sandbox_bundle:2 Unable to load preload script: D:/xxx/out/preload/index.cjs
  executeSandboxedPreloadScripts @ VM4 sandbox_bundle:2
  VM4 sandbox_bundle:2 Error: module not found: node:process
      at preloadRequire (VM4 sandbox_bundle:2:146685)
      at <anonymous>:3:21
      at runPreloadScript (VM4 sandbox_bundle:2:146954)
      at executeSandboxedPreloadScripts (VM4 sandbox_bundle:2:146227)
      at VM4 sandbox_bundle:2:156625
      at VM4 sandbox_bundle:2:156827
      at ___electron_webpack_init__ (VM4 sandbox_bundle:2:156831)
      at VM4 sandbox_bundle:2:156954

不要在 src/preload/index.ts 手动import node from ‘node:process’,若出现linter警告,禁用这一行警告即可。因为 preload 脚本本身就会被注入受控的 Node polyfill。

if (process.contextIsolated) {
  try {
    contextBridge.exposeInMainWorld('electron', electronAPI)
    contextBridge.exposeInMainWorld('api', api)
  }
  catch (error) {
    console.error(error)
  }
}
else {
  window.electron = electronAPI
  window.api = api
}
  • module not found: @electron-toolkit/preload
VM4 sandbox_bundle:2 Unable to load preload script: D:/xxxx/out/preload/index.cjs
  executeSandboxedPreloadScripts @ VM4 sandbox_bundle:2
  VM4 sandbox_bundle:2 Error: module not found: @electron-toolkit/preload
      at preloadRequire (VM4 sandbox_bundle:2:146685)
      at <anonymous>:3:17
      at runPreloadScript (VM4 sandbox_bundle:2:146954)
      at executeSandboxedPreloadScripts (VM4 sandbox_bundle:2:146227)
      at VM4 sandbox_bundle:2:156625
      at VM4 sandbox_bundle:2:156827
      at ___electron_webpack_init__ (VM4 sandbox_bundle:2:156831)
      at VM4 sandbox_bundle:2:156954