Electron 中的 ES 模块 (ESM)
简介
ECMAScript 模块 (ESM) 格式是加载 JavaScript 包的标准方法。
Chromium 和 Node.js 有它们自己的 ESM 规范实现,Electron 会根据上下文选择使用哪个模块加载器。
本文档旨在概述 Electron 中 ESM 的局限性,以及 Electron 中的 ESM 与 Node.js 和 Chromium 中的 ESM 之间的差异。
此功能已在 electron@28.0.0
中添加。
总结:ESM 支持矩阵
下表概述了 ESM 在哪些地方受支持以及使用哪个 ESM 加载器。
进程 | ESM 加载器 | 预加载中的 ESM 加载器 | 适用要求 |
---|---|---|---|
主进程 | Node.js | 不适用 | |
渲染器(沙盒化) | Chromium | 不支持 | |
渲染器(非沙盒化 & 上下文隔离) | Chromium | Node.js | |
渲染器(非沙盒化 & 非上下文隔离) | Chromium | Node.js |
主进程
Electron 的主进程在 Node.js 上下文中运行,并使用其 ESM 加载器。 使用应遵循 Node 的 ESM 文档。 要在主进程中的文件中启用 ESM,必须满足以下条件之一
- 该文件以
.mjs
扩展名结尾 - 最近的父 package.json 设置了
"type": "module"
有关更多详细信息,请参阅 Node 的确定模块系统文档。
注意事项
在应用程序的 ready
事件之前,你必须大量使用 await
ES 模块是异步加载的。 这意味着只有主进程入口点的导入中的副作用会在 ready
事件之前执行。
这很重要,因为某些 Electron API (例如 app.setPath
) 需要在应用程序的 ready
事件发出之前调用。
由于 Node.js ESM 中提供了顶级 await
,请确保 await
你需要在 ready
事件之前执行的每个 Promise。 否则,你的应用可能会在你执行代码之前就 ready
了。
对于动态 ESM 导入语句(静态导入不受影响),请特别注意这一点。 例如,如果 index.mjs
在顶层调用 import('./set-up-paths.mjs')
,则应用程序很可能在动态导入解析时就已经 ready
了。
// 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')
})
JavaScript 转译器(例如 Babel、TypeScript)在 Node.js 支持 ESM 导入之前,已经通过将这些调用转换为 CommonJS require
调用来支持 ES 模块语法。
示例:@babel/plugin-transform-modules-commonjs
@babel/plugin-transform-modules-commonjs
插件会将 ESM 导入转换为 require
调用。 确切的语法将取决于importInterop
设置。
import foo from "foo";
import { bar } from "bar";
foo;
bar;
// with "importInterop: node", compiles to ...
"use strict";
var _foo = require("foo");
var _bar = require("bar");
_foo;
_bar.bar;
这些 CommonJS 调用同步加载模块代码。 如果你正在将转换后的 CJS 代码迁移到原生 ESM,请注意 CJS 和 ESM 之间的时序差异。
渲染器进程
Electron 的渲染器进程在 Chromium 上下文中运行,并将使用 Chromium 的 ESM 加载器。 实际上,这意味着 import
语句
- 将无法访问 Node.js 内置模块
- 将无法从
node_modules
加载 npm 包
<script type="module">
import { exists } from 'node:fs' // ❌ will not work!
</script>
如果你希望直接将 JavaScript 包通过 npm 加载到渲染器进程中,我们建议使用诸如 webpack 或 Vite 之类的捆绑器来编译你的代码以供客户端使用。
预加载脚本
渲染器的预加载脚本将在可用时使用 Node.js ESM 加载器。 ESM 的可用性将取决于其渲染器的 sandbox
和 contextIsolation
偏好设置的值,并且由于 ESM 加载的异步性质,还存在一些其他注意事项。
注意事项
ESM 预加载脚本必须具有 .mjs
扩展名
预加载脚本将忽略 "type": "module"
字段,因此你必须在你的 ESM 预加载脚本中使用 .mjs
文件扩展名。
沙盒化的预加载脚本不能使用 ESM 导入
沙盒化的预加载脚本作为普通的 JavaScript 运行,没有 ESM 上下文。 如果你需要使用外部模块,我们建议你为你的预加载代码使用一个捆绑器。 加载 electron
API 仍然通过 require('electron')
完成。
有关沙盒化的更多信息,请参阅进程沙盒化文档。
非沙盒化的 ESM 预加载脚本将在没有内容的页面上在页面加载后运行
如果渲染器加载页面的响应主体完全为空(即 Content-Length: 0
),其预加载脚本将不会阻止页面加载,这可能会导致竞争条件。
如果这影响了你,请更改你的响应主体,使其包含某些内容(例如一个空的 html
标签 (<html></html>
))或换回使用 CommonJS 预加载脚本 (.js
或 .cjs
),它将阻止页面加载。
ESM 预加载脚本必须是上下文隔离的才能使用动态 Node.js ESM 导入
如果你的非沙盒化渲染器进程没有启用 contextIsolation
标志,你将无法通过 Node 的 ESM 加载器动态 import()
文件。
// ❌ these won't work without context isolation
const fs = await import('node:fs')
await import('./foo')
这是因为 Chromium 的动态 ESM import()
函数通常在渲染器进程中具有优先权,并且如果没有上下文隔离,就无法知道 Node.js 是否在动态导入语句中可用。 如果你启用了上下文隔离,则来自渲染器隔离预加载上下文的 import()
语句可以路由到 Node.js 模块加载器。