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 正常运行,需要满足两件事:
- 把 preload 依赖全都打包进单文件
- 维持 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