跳至主要内容

Electron 中的 ES 模块 (ESM)

简介

ECMAScript 模块 (ESM) 格式是 加载 JavaScript 包的标准方式

Chromium 和 Node.js 都有自己实现的 ESM 规范,Electron 会根据上下文选择使用哪个模块加载器。

本文档旨在概述 ESM 在 Electron 中的限制,以及 Electron 中的 ESM 与 Node.js 和 Chromium 中的 ESM 之间的差异。

信息

此功能在 [email protected] 中添加。

摘要:ESM 支持矩阵

此表概述了 ESM 支持的位置以及使用哪个 ESM 加载器。

进程ESM 加载器预加载中的 ESM 加载器适用要求
主进程Node.jsN/A
渲染器(沙盒)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>

如果您希望通过 npm 直接将 JavaScript 包加载到渲染器进程中,我们建议您使用 webpack 或 Vite 等打包器来编译您的代码以供客户端使用。

预加载脚本

渲染器的预加载脚本将在可用时使用 Node.js ESM 加载器。ESM 的可用性将取决于其渲染器的 sandboxcontextIsolation 首选项的值,并且由于 ESM 加载的异步特性,还存在一些其他注意事项。

注意事项

ESM 预加载脚本必须具有 .mjs 扩展名

预加载脚本将忽略 "type": "module" 字段,因此您必须在 ESM 预加载脚本中使用 .mjs 文件扩展名。

沙盒预加载脚本无法使用 ESM 导入

沙盒预加载脚本作为纯 JavaScript 运行,没有 ESM 上下文。如果您需要使用外部模块,我们建议您为预加载代码使用打包器。仍然可以通过 require('electron') 加载 electron API。

有关沙盒的更多信息,请参阅 进程沙盒 文档。

非沙盒 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 模块加载器。