进阶 electronjs.org 2026-10-07 23:51:43 · 8 阅读

第23章 Electron 中的 ES Modules(ESM)

# 第23章 Electron 中的 ES Modules (ESM) ## 简介 ECMAScript module(ESM)格式是加载 JavaScript 包的标准方式。Chromium 和 Node.js 各自有 ESM 规范的实现,Electron 会根据上下文选择使用哪个模块加载器。本文将概述 Electron 中 ESM 的限制,以及它与 Node.js 和 Chromium 中 ESM 的差异。 > 信息:此功能在 electron@28.0.0 中加入。 ## 总结:ESM 支持情况 下表概览了 ESM 在哪些场景下受支持,以及使用哪个 ESM 加载器。 | 进程 | ESM 加载器 | Preload 中的 ESM 加载器 | 相关限制 | |---|---|---|---| | Main | Node.js | 不适用 | 必须在 app 的 ready 事件前大量使用 await | | Renderer(沙箱化) | Chromium | 不支持 | 沙箱化的 preload 脚本无法使用 ESM import | | Renderer(非沙箱 & 上下文隔离) | Chromium | Node.js | 非沙箱的 ESM preload 脚本会在没有内容的页面加载完成后才运行;ESM preload 脚本必须使用 .mjs 扩展名 | | Renderer(非沙箱 & 非上下文隔离) | Chromium | Node.js | 非沙箱的 ESM preload 脚本会在没有内容的页面加载完成后才运行;ESM preload 脚本必须使用 .mjs 扩展名 | ## 主进程 Electron 的主进程运行在 Node.js 环境中,使用 Node.js 的 ESM 加载器,用法遵循 Node 的 ESM 文档。要让主进程中的文件启用 ESM,需满足以下条件之一: - 文件以 .mjs 扩展名结尾 - 最近的父级 package.json 中设置了 "type": "module" 详情请参阅 Node 的 Determining Module System 文档。 ## 注意事项 ### 必须在 app 的 ready 事件前大量使用 await ES Modules 是异步加载的。这意味着在 ready 事件触发前,只会执行主进程入口文件 import 语句带来的副作用。这一点很重要,因为某些 Electron API(如 app.setPath)必须在 ready 事件触发前调用。 Node.js 的 ESM 支持顶层 await,因此请确保对所有需要在 ready 事件前执行完的 Promise 使用 await。否则,app 可能会在你的代码执行完毕前就进入 ready 状态。 使用动态 import 语句时尤其要注意这一点(静态 import 不受影响)。例如,如果 index.mjs 在顶层调用 import('./set-up-paths.mjs'),那么等这个动态 import 完成时,app 很可能已经 ready 了。 index.mjs(主进程) ```js // 在这里加一个 await 调用,确保路径设置能在 `ready` 之前完成 ```

import('./set-up-paths.mjs')

app.whenReady().then(() => {
console.log('This code may execute before the above import')
})

转译器对 ESM 的支持

在 Node.js 原生支持 ESM 之前,JavaScript 转译器(如 Babel、TypeScript)早就支持 ES Module 语法。它们通过将 ESM 导入转换为 CommonJS 的 require 调用来实现兼容。

例如:@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;

// 当 "importInterop: node" 时,编译为 ...

"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 包

如果希望在渲染进程中通过 npm 直接加载 JavaScript 包,我们建议使用 webpack 或 Vite 这类打包工具来编译代码,供客户端使用。

Preload scripts

在可用情况下,渲染进程的 preload script 会使用 Node.js ESM 加载器。ESM 的可用性取决于该渲染进程 sandbox 和 contextIsolation 选项的值,且由于 ESM 加载是异步的,还存在一些其他注意事项。

Caveats

ESM preload scripts 必须使用 .mjs 扩展名
Preload scripts 会忽略 "type": "module" 字段,因此 ESM preload scripts 必须使用 .mjs 文件扩展名。

沙箱化的 preload scripts 无法使用 ESM import
沙箱化的 preload scripts 在没有 ESM 上下文的环境中以纯 JavaScript 方式运行。如果需要引入外部模块,我们建议使用打包工具处理 preload 代码。加载 electron API 仍需通过 require('electron') 完成。
更多沙箱化信息请参阅 Process Sandboxing 文档。

对于没有内容的页面,非沙箱化的 ESM preload scripts 会在页面加载完成后才运行
如果渲染进程所加载页面的响应体完全为空(即 Content-Length: 0),其 preload script 不会阻塞页面加载,这可能导致竞态条件。
如果这对你造成影响,请在响应体中加入一些内容(例如空的 html 标签 <html></html>),或换回使用 CommonJS preload script(.js 或 .cjs),后者会阻塞页面加载。

ESM preload scripts 必须启用上下文隔离才能使用动态 Node.js ESM import
如果你的非沙箱化渲染进程没有启用 contextIsolation 标志,则无法通过 Node.js ESM 加载器动态 import() 文件。

preload.mjs// ❌ 在没有上下文隔离的情况下,以下代码无法正常工作
const fs = await import('node:fs')
await import('./foo')

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

评论 (0)