Skip to main content

Electron 中的 ES 模块 (ESM)

¥ES Modules (ESM) in Electron

介绍

¥Introduction

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

¥The ECMAScript module (ESM) format is the standard way of loading JavaScript packages.

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

¥Chromium and Node.js have their own implementations of the ESM specification, and Electron chooses which module loader to use depending on the context.

本文档概述了 Electron 中 ESM 的局限性以及 Electron 中 ESM 与 Node.js 和 Chromium 中 ESM 之间的差异。

¥This document serves to outline the limitations of ESM in Electron and the differences between ESM in Electron and ESM in Node.js and Chromium.

信息

该功能是在 electron@28.0.0 年添加的。

¥This feature was added in electron@28.0.0.

概括:ESM 支持矩阵

¥Summary: ESM support matrix

此表概述了支持 ESM 的位置以及使用的 ESM 加载程序。

¥This table gives a general overview of where ESM is supported and which ESM loader is used.

过程ESM 加载器预加载中的 ESM 加载器适用要求
主进程Node.js不适用
渲染器(沙盒)Chromium不支持
渲染器(未沙盒化且上下文隔离)ChromiumNode.js
渲染器(未沙盒化且非上下文隔离)ChromiumNode.js

主进程

¥Main process

Electron 的主进程在 Node.js 上下文中运行并使用其 ESM 加载器。使用时应遵循 Node 的 ESM 文档。要在主进程的文件中启用 ESM,必须满足以下条件之一:

¥Electron's main process runs in a Node.js context and uses its ESM loader. Usage should follow Node's ESM documentation. To enable ESM in a file in the main process, one of the following conditions must be met:

  • 文件以 .mjs 扩展名结尾

    ¥The file ends with the .mjs extension

  • 最近的父 package.json 设置了 "type": "module"

    ¥The nearest parent package.json has "type": "module" set

有关更多详细信息,请参阅 Node 的 确定模块系统 文档。

¥See Node's Determining Module System doc for more details.

注意事项

¥Caveats

你必须在应用的 ready 事件之前慷慨地使用 await

¥You must use await generously before the app's ready event

ES 模块是异步加载的。这意味着只有来自主进程入口点导入的副作用才会在 ready 事件之前执行。

¥ES Modules are loaded asynchronously. This means that only side effects from the main process entry point's imports will execute before the ready event.

这很重要,因为在发出应用的 ready 事件之前需要调用某些 Electron API(例如 app.setPath)。

¥This is important because certain Electron APIs (e.g. app.setPath) need to be called before the app's ready event is emitted.

利用 Node.js ESM 中可用的顶层 await,确保在 ready 事件之前对需要执行的每个 Promise 进行 await。否则,你的应用在代码执行之前可能是 ready

¥With top-level await available in Node.js ESM, make sure to await every Promise that you need to execute before the ready event. Otherwise, your app may be ready before your code executes.

对于动态 ESM 导入语句,这一点尤为重要(静态导入不受影响)。例如,如果 index.mjs 在顶层调用 import('./set-up-paths.mjs'),则在动态导入解析时应用可能已经是 ready

¥This is particularly important to keep in mind for dynamic ESM import statements (static imports are unaffected). For example, if index.mjs calls import('./set-up-paths.mjs') at the top level, the app will likely already be ready by the time that dynamic import resolves.

index.mjs (Main Process)
// 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')
})
转译器翻译

在 Node.js 通过将这些调用转换为 CommonJS require 调用来支持 ESM 导入之前,JavaScript 转译器(例如 Babel、TypeScript)历来支持 ES 模块语法。

¥JavaScript transpilers (e.g. Babel, TypeScript) have historically supported ES Module syntax before Node.js supported ESM imports by turning these calls to CommonJS require calls.

Example: @babel/plugin-transform-modules-commonjs

@babel/plugin-transform-modules-commonjs 插件会将 ESM 导入转换为 require 调用。确切的语法取决于 importInterop 设置

¥The @babel/plugin-transform-modules-commonjs plugin will transform ESM imports down to require calls. The exact syntax will depend on the importInterop setting.

@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 之间的时间差异。

¥These CommonJS calls load module code synchronously. If you are migrating transpiled CJS code to native ESM, be careful about the timing differences between CJS and ESM.

渲染器进程

¥Renderer process

Electron 的渲染器进程在 Chromium 上下文中运行,并将使用 Chromium 的 ESM 加载器。实际上,这意味着 import 语句:

¥Electron's renderer processes run in a Chromium context and will use Chromium's ESM loader. In practice, this means that import statements:

  • 将无法访问 Node.js 内置模块

    ¥will not have access to Node.js built-in modules

  • 将无法从 node_modules 加载 npm 包

    ¥will not be able to load npm packages from node_modules

<script type="module">
import { exists } from 'node:fs' // ❌ will not work!
</script>

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

¥If you wish to load JavaScript packages via npm directly into the renderer process, we recommend using a bundler such as webpack or Vite to compile your code for client-side consumption.

预加载脚本

¥Preload scripts

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

¥A renderer's preload script will use the Node.js ESM loader when available. ESM availability will depend on the values of its renderer's sandbox and contextIsolation preferences, and comes with a few other caveats due to the asynchronous nature of ESM loading.

注意事项

¥Caveats

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

¥ESM preload scripts must have the .mjs extension

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

¥Preload scripts will ignore "type": "module" fields, so you must use the .mjs file extension in your ESM preload scripts.

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

¥Sandboxed preload scripts can't use ESM imports

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

¥Sandboxed preload scripts are run as plain JavaScript without an ESM context. If you need to use external modules, we recommend using a bundler for your preload code. Loading the electron API is still done via require('electron').

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

¥For more information on sandboxing, see the Process Sandboxing docs.

未沙盒的 ESM 预加载脚本将在页面加载后在没有内容的页面上运行

¥Unsandboxed ESM preload scripts will run after page load on pages with no content

如果渲染器加载页面的响应正文完全为空(即 Content-Length: 0),则其预加载脚本将不会阻止页面加载,这可能会导致竞争条件。

¥If the response body for a renderer's loaded page is completely empty (i.e. Content-Length: 0), its preload script will not block the page load, which may result in race conditions.

如果这对你产生影响,请更改你的响应正文以在其中包含某些内容(例如空的 html 标记 (<html></html>))或换回使用 CommonJS 预加载脚本(.js.cjs),这将阻止页面加载。

¥If this impacts you, change your response body to have something in it (e.g. an empty html tag (<html></html>)) or swap back to using a CommonJS preload script (.js or .cjs), which will block the page load.

ESM 预加载脚本必须进行上下文隔离才能使用动态 Node.js ESM 导入

¥ESM preload scripts must be context isolated to use dynamic Node.js ESM imports

如果你的未沙盒渲染器进程未启用 contextIsolation 标志,则你无法通过 Node 的 ESM 加载器动态加载 import() 文件。

¥If your unsandboxed renderer process does not have the contextIsolation flag enabled, you cannot dynamically import() files via Node's ESM loader.

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

¥This is because Chromium's dynamic ESM import() function usually takes precedence in the renderer process and without context isolation, there is no way of knowing if Node.js is available in a dynamic import statement. If you enable context isolation, import() statements from the renderer's isolated preload context can be routed to the Node.js module loader.