整合非 JavaScript 資源

瞭解如何從 JavaScript 匯入及組合不同類型的素材資源。

伊格瓦史坦尼恩
Ingvar Stepanyan

假設您使用的是網路應用程式。在這種情況下,您可能不僅可以處理 JavaScript 模組,還需要處理其他各種資源:網路工作程式 (也不是 JavaScript,但不屬於一般模組圖)、圖片、樣式表、字型、WebAssembly 模組等等。

您可以在 HTML 中直接加入這些資源的參照,但通常它們在邏輯上會與可重複使用的元件結合。例如,樣式表中用於自訂下拉式選單的 JavaScript 部分、與工具列元件綁定的圖示圖片,或是與 JavaScript 膠帶綁定的 WebAssembly 模組。在這種情況下,您可以直接從 JavaScript 模組參照資源,並在 (或載入) 對應元件時動態載入資源。

以視覺化方式呈現匯入 JS 的各種集體類型。

不過,大部分的大型專案均須建構系統,以執行額外的最佳化及重整內容,例如打包和壓縮。這類模型無法執行程式碼並預測執行結果,也無法掃遍 JavaScript 中每個可能的字串常值,並猜測其是否為資源網址。那麼該如何將 JavaScript 元件載入的動態素材資源設為「看到」,並將這類素材資源納入建構中?

套件中的自訂匯入項目

常見的做法是重複使用靜態匯入語法。在某些封裝程式中,檔案可能會依據副檔名自動偵測格式,其他套件則允許外掛程式使用自訂網址通訊協定,如以下範例所示:

// regular JavaScript import
import { loadImg } from './utils.js';

// special "URL imports" for assets
import imageUrl from 'asset-url:./image.png';
import wasmUrl from 'asset-url:./module.wasm';
import workerUrl from 'js-url:./worker.js';

loadImg(imageUrl);
WebAssembly.instantiateStreaming(fetch(wasmUrl));
new Worker(workerUrl);

當 Bundler 外掛程式找到包含其可識別的擴充功能或這類明確自訂配置 (上述範例中的 asset-url:js-url:) 的匯入項目時,就會將該參照的素材資源新增至最終目的地,然後執行適用於該素材資源類型的最佳化作業,並傳回要在執行階段中使用的最終到達網址。

這個方法的好處:重複使用 JavaScript 匯入語法可保證所有網址都是靜態的,且相對於目前檔案,讓建構系統更容易找到這類依附元件。

但有一項明顯的缺點:這類程式碼無法直接在瀏覽器中運作,因為瀏覽器不知道如何處理這些自訂匯入配置或擴充功能。這也許適合您掌控所有的程式碼,無論如何,仍仰賴 Bundler 進行開發,但為了減少轉換而直接在瀏覽器中直接使用 JavaScript 模組是越來越常見的做法。小型示範裝置的人甚至可能完全不需要組合器,即使是生產環境也不例外。

瀏覽器和套件組合的通用模式

如果您正在開發可重複使用的元件,而且會希望這個元件能在任一環境中運作,無論是直接在瀏覽器中使用,或是做為大型應用程式的預先建構元件。大部分新型的套件都允許在 JavaScript 模組中接受下列模式:

new URL('./relative-path', import.meta.url)

工具能夠以靜態方式偵測這種模式,幾乎像是特殊語法,但它也是能直接在瀏覽器中運作的有效 JavaScript 運算式。

使用這個模式時,可以將上述範例重新編寫為:

// regular JavaScript import
import { loadImg } from './utils.js';

loadImg(new URL('./image.png', import.meta.url));
WebAssembly.instantiateStreaming(
  fetch(new URL('./module.wasm', import.meta.url)),
  { /* … */ }
);
new Worker(new URL('./worker.js', import.meta.url));

How does it work? 讓我們分手一下。new URL(...) 建構函式會將相對網址視為第一個引數,並與其提供絕對網址做為第二個引數進行解析。在本範例中,第二個引數是 import.meta.url,可提供目前 JavaScript 模組的網址,因此第一個引數可以是任何相對的路徑。

此方法也與動態匯入類似。雖然您可以將 import(...)import(someUrl) 等任意運算式搭配使用,但組合器會以靜態網址 import('./some-static-url.js') 的模式進行特殊處理,藉此預先處理編譯期間已知的依附元件,但將其分割為動態載入的專屬區塊

同樣地,您可以將 new URL(...)new URL(relativeUrl, customAbsoluteBase) 等任意運算式搭配使用,但 new URL('...', import.meta.url) 模式是方便封裝器預先處理的明確信號,且除了主要 JavaScript 以外,也包括依附元件。

無法識別的相對網址

您可能會好奇,為何封裝器無法偵測其他常見的模式,例如在沒有 new URL 包裝函式的 fetch('./module.wasm') 的情況下?

原因是與匯入陳述式不同的是,任何動態要求都會相對解析文件本身,而非目前的 JavaScript 檔案。假設您的結構如下:

  • index.html
    html <script src="src/main.js" type="module"></script>
  • src/
    • main.js
    • module.wasm

如要從 main.js 載入 module.wasm,您可能會想使用 fetch('./module.wasm') 的相對路徑。

不過,fetch 不知道執行指令碼時所在的 JavaScript 檔案網址,而會相對解析該文件的網址。因此,fetch('./module.wasm') 最終會嘗試載入 http://example.com/module.wasm,而非預期的 http://example.com/src/module.wasm,並失敗 (更糟的是,以無訊息方式載入與您預期不同的資源)。

只要將相對網址納入 new URL('...', import.meta.url) 中,就能避免這個問題,並確保提供的網址一律與目前 JavaScript 模組的網址 (import.meta.url) 相關,然後才傳遞至任何載入器。

fetch('./module.wasm') 替換為 fetch(new URL('./module.wasm', import.meta.url)),它就能成功載入預期的 WebAssembly 模組,並讓整合工具能在建構期間找到這些相對路徑。

工具支援

組合器

以下組合器可支援 new URL 配置:

WebAssembly

使用 WebAssembly 時,您通常不會手動載入 Wasm 模組,而是匯入工具鍊發出的 JavaScript 黏合。下列工具鍊可為您發出所述 new URL(...) 模式。

透過 Emscripten 使用 C/C++

使用 Emscripten 時,您可以透過下列任一選項,要求 Inscripten 將 JavaScript glue 輸出為 ES6 模組,而非一般指令碼:

$ emcc input.cpp -o output.mjs
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6

使用這個選項時,輸出結果會使用 new URL(..., import.meta.url) 模式,讓套裝組合能自動尋找相關聯的 Wasm 檔案。

您也可以透過新增 -pthread 標記,搭配 WebAssembly 執行緒使用這個選項:

$ emcc input.cpp -o output.mjs -pthread
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6 -pthread

在這種情況下,產生的 Web Worker 將以相同方式納入其中,軟體開發者和瀏覽器也可加以搜尋。

透過 Wam-pack / wasm-bindgen 設計的 Rust

wasm-pack (WebAssembly 的主要 Rust 工具鍊) 也有數種輸出模式。

根據預設,它會發出依附於 WebAssembly ESM 整合提案的 JavaScript 模組。目前,這項提案仍處於實驗階段,輸出內容只有在與 Webpack 組合時才能運作。

您可以改為要求 wasm-pack 透過 --target web 產生與瀏覽器相容的 ES6 模組:

$ wasm-pack build --target web

輸出內容將使用描述的 new URL(..., import.meta.url) 模式,套件也會自動探索 Wasm 檔案。

如果您想搭配 Rust 使用 WebAssembly 執行緒,這部分會比較複雜。詳情請參閱指南中對應的章節

精簡版本是您無法使用任意執行緒 API,但如果您使用 Rayon,便可將其與 wasm-bindgen-rayon 轉接器結合,藉此產生網路上的工作站。wasm-bindgen-rayon 使用的 JavaScript 黏膠也包含 new URL(...) 模式,因此 worker 也可以找到並加入 worker。

日後推出的功能

import.meta.resolve

import.meta.resolve(...) 進行的專屬通話可能是未來改善項目。這允許以較直接的方式,以較直接的方式解析指定器,無需額外的參數:

new URL('...', import.meta.url)
await import.meta.resolve('...')

此外,匯入地圖和自訂解析器也會更好地整合,因為它會採用與 import 相同的模組解析系統。這也會是套件工具的重要信號,因為這個靜態語法不依附於 URL 等執行階段 API。

import.meta.resolve 已做為在 Node.js 中的實驗實作,但仍有一些未解決的問題,說明如何將其在網路上運作。

匯入斷言

匯入斷言是一項新功能,可匯入 ECMAScript 模組以外的類型。但目前只能採用 JSON:

foo.json:

{ "answer": 42 }

main.mjs:

import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42

這些套件也可能由組合器使用,並取代 new URL 模式目前涵蓋的用途,但匯入斷言類型會依個別情況新增。這些素材資源目前只支援 JSON (即將推出的 CSS 模組),但其他類型的素材資源仍需使用更通用的解決方案。

如要進一步瞭解這項功能,請參閱 v8.dev 功能說明

結語

如您所見,有許多方法可以在網路上納入非 JavaScript 資源,但這些缺點各有不同,而且無法在各種工具鍊中運作。未來提案或許可讓我們以特殊語法匯入這類素材資源,但目前尚未完全完成。

在此之前,new URL(..., import.meta.url) 模式是最有潛力的解決方案,已可在瀏覽器、各種套件和 WebAssembly 工具鍊中運作。