在 GraalJS 中使用 JavaScript 模块和包

GraalJS 与最新的 ECMAScript 标准兼容,可以在各种基于 Java 的嵌入式场景中运行。根据嵌入方式,JavaScript 包和模块的使用方式可能有所不同。

通过 Context API 嵌入 Java #

当嵌入到 Java 应用程序中(使用 Context API)时,GraalJS 可以执行 JavaScript 应用程序和模块,这些应用程序和模块依赖于 Node.js 的内置模块(例如 'fs''events''http')或 Node.js 特定的函数(例如 setTimeout()setInterval())。另一方面,依赖于此类 Node.js 内置模块的模块无法在 GraalVM 多语言环境 Context 中加载。

支持的 NPM 包可以使用以下方法之一在 JavaScript Context 中使用

  1. 使用包捆绑器。例如,将多个 NPM 包组合成一个 JavaScript 源文件
  2. 在本地文件系统上使用 ECMAScript (ES) 模块。可选地,可以使用自定义 Truffle 文件系统 来配置如何解析文件。

默认情况下,Java Context 不会使用 CommonJS require() 函数加载模块。这是因为 require() 是一个 Node.js 内置函数,不是 ECMAScript 规范的一部分。可以通过 js.commonjs-require 选项启用对 CommonJS 模块的实验性支持,如下所述。

ECMAScript 模块 (ESM) #

GraalJS 支持完整的 ES 模块规范,包括 import 语句、使用 import() 的动态模块导入以及 顶层 await 等高级功能。

只需评估模块源代码,就可以在 Context 中加载 ECMAScript 模块。GraalJS 根据其文件扩展名加载 ECMAScript 模块。因此,任何 ECMAScript 模块都应具有文件扩展名 .mjs。或者,模块 Source 应具有 MIME 类型 "application/javascript+module"

例如,假设您有一个名为 foo.mjs 的文件,其中包含以下简单的 ES 模块

export class Foo {

    square(x) {
        return x * x;
    }
}

此 ES 模块可以通过以下方式在多语言环境 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。当直接从 Java 使用 ES 模块时,这可能很方便

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 使用多语言环境 Context 的内置文件系统来加载和解析 ES 模块。可以使用 FileSystem 自定义 ES 模块加载过程。例如,可以使用自定义文件系统使用 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 时加载动态生成的 ES 模块。

可以在 此处找到加载 ES 模块的自定义 Truffle 文件系统的完整示例。

CommonJS 模块 #

默认情况下,Context API 不支持 CommonJS 模块,并且没有内置的 require() 函数。为了从 Java 的 Context 中加载和使用,CommonJS 模块需要捆绑到一个独立的 JavaScript 源文件。这可以使用许多流行的开源捆绑工具(例如 Parcel、Browserify 和 Webpack)来实现。可以通过 js.commonjs-require 选项启用对 CommonJS 模块的实验性支持,如下所述。

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");

正如代码所示,该选项指示 GraalJS 在应用程序尝试使用 require('buffer') 加载 Node.js buffer 内置模块时加载名为 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');

与我们联系