- 适用于 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 ("http://localhost/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 'http://localhost/foo';" +
"const foo = new Foo();" +
"console.log(foo.square(42));";
cx.eval(Source.newBuilder("js", src, "test.mjs").build());
在这个简单的例子中,当应用程序尝试导入 http://localhost/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');