在 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 多語環境 Context 中載入。

支援的 NPM 套件可以使用以下方法之一在 JavaScript Context 中使用

  1. 使用套件捆綁器。例如,將多個 NPM 套件合併到單個 JavaScript Source 檔案中。
  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 模組命名空間匯出的物件公開給多語環境 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 模組。可以使用 檔案系統來自訂 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 ("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 時,會使用自訂檔案系統來載入動態產生的 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() 依賴於 檔案系統,因此需要在建立環境時使用 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');

與我們聯繫