跳转到主要内容

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.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 import 语句(静态导入不受影响)时,这一点尤其重要。例如,如果 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 Module 语法。

示例:@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 上下文。如果您需要使用外部模块,我们建议为您的预加载代码使用打包器。加载 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 语句中是否可用。如果您启用了上下文隔离,则渲染进程的隔离预加载上下文中的 import() 语句可以路由到 Node.js 模块加载器。