跳到主要内容

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不支持
渲染器(非沙盒化 & 上下文隔离)ChromiumNode.js
渲染器(非沙盒化 & 非上下文隔离)ChromiumNode.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 了。

index.mjs(主进程)
// 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 设置

@babel/plugin-transform-modules-commonjs
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 的可用性将取决于其渲染器的 sandboxcontextIsolation 偏好设置的值,并且由于 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() 文件。

preload.mjs
// ❌ these won't work without context isolation
const fs = await import('node:fs')
await import('./foo')

这是因为 Chromium 的动态 ESM import() 函数通常在渲染器进程中具有优先权,并且如果没有上下文隔离,就无法知道 Node.js 是否在动态导入语句中可用。 如果你启用了上下文隔离,则来自渲染器隔离预加载上下文的 import() 语句可以路由到 Node.js 模块加载器。