在 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 中使用

  1. 使用包打包器。例如,将多个 NPM 包组合到一个 JavaScript 源文件中。
  2. 在本地文件系统上使用 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');

联系我们