ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Electron 如何在主进程与 preload 脚本中启用 ES Modules?

Electron 如何在主进程与 preload 脚本中启用 ES Modules? Electron 如何在主进程与 preload 脚本中启用 ES Modules【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron如果你的 Electron 应用想用import语句加载模块而不是 CommonJS 的require需要满足两个前提项目使用的 Electron 版本不低于 28.0.0ESM 支持在electron28.0.0加入并且清楚不同进程使用哪套模块加载器。Chromium 和 Node.js 各自有一套 ESM 实现Electron 会按上下文选择主进程走 Node.js 的 ESM loader渲染器页面走 Chromium 的 ESM loaderpreload 脚本则在可用时走 Node.js 的 ESM loader。本文按“搭好项目 → 主进程启用 ESM → preload 启用 ESM → 验证”的顺序把这条路径完整走一遍所有行为细节以 ES Modules (ESM) in Electron 为准。ESM 支持矩阵先确认你的脚本运行在哪个上下文官方给出的支持矩阵如下摘自 esm.mdProcessESM LoaderESM Loader in PreloadApplicable RequirementsMainNode.jsN/Aready事件前必须充分使用awaitRenderer (Sandboxed)ChromiumUnsupported沙箱 preload 不能使用 ESM importRenderer (Unsandboxed Context Isolated)ChromiumNode.jsESM preload 必须用.mjs扩展名空内容页面上 preload 在页面加载后才运行Renderer (Unsandboxed Non Context Isolated)ChromiumNode.js同上由此可以直接得出两条操作结论主进程启用 ESM 只需要处理文件扩展名和package.jsonpreload 启用 ESM 则必须先解除沙箱再处理扩展名和contextIsolation相关的限制。搭建可运行的 Electron 项目先按 Building your First App 的流程初始化项目这一步与是否使用 ESM 无关但入口文件随后会指向 ESM 脚本mkdir my-electron-app cd my-electron-app npm init npm install electron --save-devnpm init时把 entry point 指向你的 ESM 入口文件例如index.mjs。Electron 可执行文件应放在devDependencies中并通过scripts里的electron .命令以开发模式运行{ name: my-electron-app, main: index.mjs, type: module, scripts: { start: electron . }, devDependencies: { electron: ^28.0.0 } }上面是组合示例main指向入口、scripts: { start: electron . }来自官方教程的模板type: module则来自仓库测试夹具 spec/fixtures/esm/package/package.json。注意type: module只对主进程这类 Node.js 上下文生效对 preload 脚本无效见下文。另外教程特别提醒Electron 打包工具链要求node_modules真实落在磁盘上如果你使用 Yarn Berry 或 pnpm需要分别设置nodeLinker: node-modules或nodeLinker: hoisted否则安装策略不兼容。主进程启用 ESM两种满足其一的启用条件主进程运行在 Node.js 上下文中要让某个文件按 ESM 处理以下条件满足其一即可esm.md文件以.mjs结尾最近的父级package.json中设置了type: module。仓库测试夹具提供了两种形态的最小主进程入口可以直接作为模板。第一种是.mjs入口spec/fixtures/esm/entrypoint.mjs仓库示例import * as electron from electron; console.log(ESM Launch, ready:, electron.app.isReady()); process.exit(0);第二种是依赖type: module的包入口package.json只多一个main: index.mjsspec/fixtures/esm/package/index.mjsimport * as electron from electron; console.log(ESM Package Launch, ready:, electron.app.isReady()); process.exit(0);运行npm run start后如果入口加载成功终端会打印对应的一行日志ready: false表示打印时ready事件尚未触发这正是 ESM 异步加载的体现。ready事件前必须充分使用awaitESM 是异步加载的ready事件之前只会执行主进程入口自身 import 的副作用。而某些 API例如app.setPath必须在ready事件之前调用因此需要利用 Node.js ESM 的 top-levelawait把每一个必须在ready前完成的 Promise 都await掉。仓库夹具 spec/fixtures/esm/top-level-await.mjs 演示了这一点import * as electron from electron; // Cheeky delay await new Promise((resolve) setTimeout(resolve, 500)); console.log(Top level await, ready:, electron.app.isReady()); process.exit(0);仓库测试断言在 top-levelawait期间electron.app.isReady()仍为false即 Electron 会等待 top-levelawait完成才宣告 app ready。这一点在动态import()时尤其危险。静态 import 不受影响但如果在顶层调用动态 import 而不await等它 resolve 时 app 很可能已经ready了。esm.md 中的原始示例文档给出的就是“缺少 await”的错误写法注释指明了修复位置// add an await call here to guarantee that path setup will finish before ready import(./set-up-paths.mjs) app.whenReady().then(() { console.log(This code may execute before the above import) })修复方式就是把那行动态 import 改为await import(./set-up-paths.mjs)保证路径设置在ready之前完成。从转译后的 CJS 代码迁移时的时间差异Babel、TypeScript 等转译器历史上会把import语法转成 CommonJS 的require调用例如babel/plugin-transform-modules-commonjs插件具体产物取决于importInterop配置。require是同步加载模块代码的如果你把转译成 CJS 的代码迁移到原生 ESM要注意两者加载时序的差异否则依赖“模块加载即执行完毕”的逻辑可能出现时序问题。preload 脚本启用 ESMpreload 脚本要使用 Node.js 的 ESM loader条件比主进程多按顺序处理以下四步。1. 解除沙箱从 Electron 20 开始preload 脚本默认是沙箱化的沙箱 preload 以纯 JavaScript 运行没有 ESM 上下文不能写 ESM import。如果需要拆分模块官方建议用 bundler如 webpack打包 preload 代码此时electronAPI 仍通过require(electron)加载Process Sandboxing。要在 preload 里用 ESM必须把渲染器进程设为非沙箱在BrowserWindow构造参数的webPreferences中设置sandbox: false。注意解除沙箱带有安全风险尤其当进程中存在不受信任的代码或内容时文档明确提醒这一点。2. 文件必须使用.mjs扩展名preload 脚本会忽略package.json中的type: module字段所以 ESM preload 必须用.mjs文件扩展名即使主进程是靠type: module启用 ESM 的。3. 动态import()需要 context isolationpreload 里的静态import语句没有额外限制但如果是通过 Node 的 ESM loader 做动态import()则要求渲染器进程启用了contextIsolation该选项自 Electron 12 起默认开启Context Isolation// ❌ these wont work without context isolation const fs await import(node:fs) await import(./foo)原因是渲染器进程中 Chromium 的动态import()通常优先生效没有 context isolation 时无法判断动态 import 语句里 Node.js 是否可用启用 context isolation 后来自 preload 隔离上下文的import()才能路由到 Node.js 模块加载器。4. 空内容页面的竞态问题如果渲染器加载的页面响应体完全为空Content-Length: 0非沙箱的 ESM preload 不会阻塞页面加载可能导致竞态条件。两种解法均出自 esm.md让响应体里有一点内容例如html/html或者换回 CommonJS preload.js或.cjs它会阻塞页面加载。仓库中的完整 ESM preload 示例仓库测试夹具 spec/fixtures/esm/import-meta/ 给出了一个可对照的主进程 preload 组合。主进程入口main.mjs 简化自仓库示例去掉了测试用的断言与退出逻辑import { app, BrowserWindow } from electron; import { fileURLToPath } from node:url; async function createWindow() { const mainWindow new BrowserWindow({ show: false, webPreferences: { preload: fileURLToPath(new URL(preload.mjs, import.meta.url)), sandbox: false, contextIsolation: false } }); await mainWindow.loadFile(index.html); } app.whenReady().then(() createWindow());对应的 preload.mjs仓库示例展示import.meta在 ESM preload 中可用import { fileURLToPath } from node:url; window.importMetaPath fileURLToPath(import.meta.url);两点说明其一该夹具里 preload 路径用fileURLToPath(new URL(preload.mjs, import.meta.url))计算即 ESM 主进程里定位自身目录的写法其二夹具显式设置了contextIsolation: false因此 preload 里对window的赋值能被页面直接读到——如果保持 context isolation 开启preload 与页面不在同一个window上下文暴露 API 应改走contextBridgeContext Isolation。若你的 preload 需要上文第 3 条的动态import()则必须启用 context isolation。验证 ESM 是否真正生效仓库测试套件 spec/esm-spec.ts 展示了两种可复用的验证方式。主进程入口以 ESM 入口启动应用检查退出码和标准输出。仓库测试对 entrypoint.mjs 的期望是退出码为 0、stdout 恰好为ESM Launch, ready: false测试断言值即文档示例级别的预期输出。对你自己的应用npm run start后终端打印出你写在入口里的日志即说明 ESM 入口被正确加载。preload仓库测试为每个测试窗口挂上preload-error事件监听 preload 加载错误再用webContents.executeJavaScript读取 preload 暴露到页面的全局判断其类型是否符合预期let error null; w.webContents.on(preload-error, (_, __, err) { error err; }); await w.loadFile(index.html); // preload 中执行了 import { resolve } from path; window.resolvePath resolve; const exposedType await w.webContents.executeJavaScript(typeof window.resolvePath); expect(exposedType).to.equal(function);以上取自 spec/esm-spec.ts 的测试代码作为示例展示。在真实应用中等价做法是监听preload-error若无错误且页面脚本能读到 preload 挂上去的对象/函数说明 ESM preload 已被加载并执行。仓库测试还覆盖了几个值得知道的边界import electron/main、import electron/renderer、import electron/common、import electron/utility都可以正常导入而类似import electron/lol这样的不存在入口会抛ERR_MODULE_NOT_FOUND主进程或Cannot find package electronpreloadESM preload 的导入链完成前页面加载会被推迟preload 里的 top-levelawait会阻塞页面加载。限制与边界版本门槛ESM 支持自electron28.0.0起提供低于该版本的主进程入口和 ESM preload 均不可用。渲染器页面本身页面里的import走 Chromium 的 ESM loader既不能访问 Node.js 内置模块也不能从node_modules加载 npm 包import { exists } from node:fs在页面中无效。需要给渲染器引入 npm 包时官方建议使用 webpack、Vite 等 bundler 编译成客户端可消费的代码。沙箱 preload永远无法使用 ESM import只能靠 bundler 拆分子模块electronAPI 继续用require(electron)加载。preload 的模块判定只看扩展名type: module对 preload 无效.mjs是唯一开关。动态import()preload 中未经 context isolation 的动态import()无法走 Node.js 的 ESM loader。空页面响应体为空的页面上非沙箱 ESM preload 不阻塞页面加载需要按上文方法规避竞态。完成以上配置后你的项目应同时具备npm run start能加载.mjs或type: module下的主进程入口并打印日志以及一个.mjs命名的非沙箱 preload 成功执行、且preload-error事件没有触发。若 preload 报错优先按本文顺序核对渲染器是否sandbox: false、扩展名是否为.mjs、是否涉及未启用 context isolation 的动态import()、加载的页面响应体是否为空。更多行为细节可回查 ESM 指南 与仓库测试 spec/esm-spec.ts。【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进