- 适用于 JDK 24 的 GraalVM(最新)
- 适用于 JDK 25 的 GraalVM(早期访问)
- 适用于 JDK 21 的 GraalVM
- 适用于 JDK 17 的 GraalVM
- 存档
- 开发构建
在 GraalJS 中使用 JavaScript 模块和包
GraalJS 兼容最新的 ECMAScript 标准,可以在各种基于 Java 的嵌入式场景中运行。根据嵌入方式的不同,JavaScript 包和模块的使用方式也可能不同。
通过 Context API 进行 Java 嵌入 #
当嵌入到 Java 应用程序中(使用 Context API)时,GraalJS 可以执行 *不* 依赖于 Node.js 内置模块(如 'fs'、'events' 或 'http')或 Node.js 特定函数(如 setTimeout() 或 setInterval())的 JavaScript 应用程序和模块。另一方面,依赖于此类 Node.js 内置模块的模块无法在 GraalVM polyglot Context 中加载。
支持的 NPM 包可以通过以下方法之一在 JavaScript Context 中使用
- 使用包打包器。例如,将多个 NPM 包组合到一个 JavaScript 源文件中。
- 在本地文件系统上使用 ECMAScript (ES) 模块。或者,可以使用自定义的 Truffle FileSystem 来配置文件的解析方式。
默认情况下,Java Context 不支持使用 CommonJS require() 函数加载模块。这是因为 require() 是 Node.js 的内置函数,不属于 ECMAScript 规范。CommonJS 模块的实验性支持可以通过 js.commonjs-require 选项启用,具体如下所述。
ECMAScript 模块 (ESM) #
GraalJS 支持完整的 ES 模块规范,包括 import 语句、使用 import() 动态导入模块,以及 顶级 await 等高级特性。
ECMAScript 模块可以通过简单地评估模块源在 Context 中加载。GraalJS 根据文件扩展名加载 ECMAScript 模块。因此,任何 ECMAScript 模块的文件名应以 .mjs 结尾。或者,模块 Source 应具有 MIME 类型 "application/javascript+module"。
例如,假设您有一个名为 foo.mjs 的文件,其中包含以下简单的 ES 模块
export class Foo {
square(x) {
return x * x;
}
}
这个 ES 模块可以按以下方式加载到 polyglot Context 中
public static void main(String[] args) throws IOException {
String src = "import {Foo} from '/path/to/foo.mjs';" +
"const foo = new Foo();" +
"console.log(foo.square(42));";
Context cx = Context.newBuilder("js")
.allowIO(true)
.build();
cx.eval(Source.newBuilder("js", src, "test.mjs").build());
}
请注意,ES 模块文件具有 .mjs 扩展名。另请注意,提供了 allowIO() 选项以启用 IO 访问。更多 ES 模块使用示例可在此处找到。
模块命名空间导出
可以使用 --js.esm-eval-returns-exports 选项(默认为 false)将 ES 模块命名空间导出的对象暴露给 Polyglot Context。当 ES 模块直接从 Java 中使用时,这会非常方便。
public static void main(String[] args) throws IOException {
String code = "export const foo = 42;";
Context cx = Context.newBuilder("js")
.allowIO(true)
.option("js.esm-eval-returns-exports", "true")
.build();
Source source = Source.newBuilder("js", code)
.mimeType("application/javascript+module")
.build();
Value exports = cx.eval(source);
// now the `exports` object contains the ES module exported symbols.
System.out.println(exports.getMember("foo").toString()); // prints `42`
}
Truffle 文件系统 #
默认情况下,GraalJS 使用 polyglot Context 的内置 FileSystem 来加载和解析 ES 模块。可以使用 FileSystem 来自定义 ES 模块加载过程。例如,自定义 FileSystem 可用于通过 URL 解析 ES 模块
Context cx = Context.newBuilder("js").fileSystem(new FileSystem() {
private final Path TMP = Paths.get("/some/tmp/path");
@Override
public Path parsePath(URI uri) {
// If the URL matches, return a custom (internal) Path
if ("https:///foo".equals(uri.toString())) {
return TMP;
} else {
return Paths.get(uri);
}
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
if (TMP.equals(path)) {
String moduleBody = "export class Foo {" +
" square(x) {" +
" return x * x;" +
" }" +
" }";
// Return a dynamically-generated file for the ES module.
return createByteChannelFrom(moduleBody);
}
}
/* Other FileSystem methods not shown */
}).allowIO(true).build();
String src = "import {Foo} from 'https:///foo';" +
"const foo = new Foo();" +
"console.log(foo.square(42));";
cx.eval(Source.newBuilder("js", src, "test.mjs").build());
在这个简单的例子中,当应用程序尝试导入 https:///foo URL 时,自定义 FileSystem 用于加载动态生成的 ES 模块。
加载 ES 模块的自定义 Truffle FileSystem 的完整示例可在此处找到。
CommonJS 模块 #
默认情况下,Context API 不支持 CommonJS 模块,也没有内置的 require() 函数。为了从 Java 中的 Context 加载和使用 CommonJS 模块,需要将其打包成一个自包含的 JavaScript 源文件。这可以通过许多流行的开源打包工具(如 Parcel、Browserify 和 Webpack)来实现。CommonJS 模块的实验性支持可以通过 js.commonjs-require 选项启用,具体如下所述。
Context API 中对 CommonJS NPM 模块的实验性支持
js.commonjs-require 选项提供了一个内置的 require() 函数,可用于在 JavaScript Context 中加载与 NPM 兼容的 CommonJS 模块。目前,这是一个实验性功能,不适用于生产环境。
要启用 CommonJS 支持,可以按以下方式创建 JavaScript 上下文
Map<String, String> options = new HashMap<>();
// Enable CommonJS experimental support.
options.put("js.commonjs-require", "true");
// (optional) directory where the NPM modules to be loaded are located.
options.put("js.commonjs-require-cwd", "/path/to/root/directory");
// (optional) Node.js built-in replacements as a comma separated list.
options.put("js.commonjs-core-modules-replacements",
"buffer:buffer/," +
"path:path-browserify");
// Create context with IO support and experimental options.
Context cx = Context.newBuilder("js")
.allowExperimentalOptions(true)
.allowIO(true)
.options(options)
.build();
// Require a module
Value module = cx.eval("js", "require('some-module');");
"js.commonjs-require-cwd" 选项可用于指定 NPM 包安装的主文件夹。例如,这可以是执行 npm install 命令的目录,或包含主 node_modules/ 目录的目录。任何 NPM 模块都将相对于该目录解析,包括使用 "js.commonjs-core-modules-replacements" 指定的任何内置替换。
与 Node.js 内置 require() 函数的区别
Context 内置的 require() 函数可以加载用 JavaScript 实现的常规 NPM 模块,但不能加载原生 NPM 模块。内置的 require() 依赖于 FileSystem,因此在上下文创建时需要使用 allowIO 选项启用 I/O 访问。内置的 require() 旨在与 Node.js 大致兼容,我们期望它能与任何在浏览器中(例如,使用包打包器创建的)可以工作的 NPM 模块一起使用。
安装用于 Context API 的 NPM 模块
为了在 JavaScript Context 中使用,NPM 模块需要安装到本地目录,例如,通过运行 npm install 命令。在运行时,可以使用 js.commonjs-require-cwd 选项指定 NPM 包的主安装目录。require() 内置函数会根据 Node.js 的默认包解析协议,从通过 js.commonjs-require-cwd 指定的目录开始解析包。如果未提供目录选项,则将使用应用程序的当前工作目录。
Node.js 核心模块模拟
某些 JavaScript 应用程序或 NPM 模块可能需要 Node.js 内置模块中可用的功能(例如 'fs' 和 'buffer')。这些模块在 Context API 中不可用。幸好,Node.js 社区已经为许多 Node.js 核心模块开发了高质量的 JavaScript 实现(例如,用于浏览器的 “buffer” 模块)。可以使用 js.commonjs-core-modules-replacements 选项将此类替代模块实现暴露给 JavaScript Context,具体如下所示
options.put("js.commonjs-core-modules-replacements", "buffer:my-buffer-implementation");
如代码所示,当应用程序尝试使用 require('buffer') 加载 Node.js buffer 内置模块时,该选项会指示 GraalJS 加载名为 my-buffer-implementation 的模块。
全局符号预初始化
NPM 模块或 JavaScript 应用程序可能期望在全局作用域中定义某些全局属性。例如,应用程序或模块可能期望在 JavaScript 全局对象中定义 Buffer 全局符号。为此,应用程序用户代码可以使用 globalThis 来修补应用程序的全局作用域
// define an empty object called 'process'
globalThis.process = {};
// define the 'Buffer' global symbol
globalThis.Buffer = require('some-buffer-implementation').Buffer;
// import another module that might use 'Buffer'
require('another-module');