Electron 中的 ES 模块 (ESM)
简介
ECMAScript 模块 (ESM) 格式是加载 JavaScript 包的标准方式。
Chromium 和 Node.js 都有自己的 ESM 规范实现,Electron 根据上下文选择要使用的模块加载器。
本文档旨在概述 ESM 在 Electron 中的限制,以及 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
,请确保在 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>
如果您希望通过 npm 直接将 JavaScript 包加载到渲染进程中,我们建议使用 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 模块加载器。