整合非 JavaScript 資源

瞭解如何從 JavaScript 匯入及捆綁各種類型的素材資源。

Ingvar Stepanyan
Ingvar Stepanyan

假設您正在開發網路應用程式,那麼您可能不僅要處理 JavaScript 模組,還要處理各種其他資源,包括網路工作站 (也是 JavaScript,但不是一般模組圖表的一部分)、圖片、樣式表、字型、WebAssembly 模組等。

您也可以在 HTML 中直接納入對部分資源的參照,但資源通常在邏輯上伴隨著可重複使用的元件。舉例來說,自訂下拉式選單的樣式表會與其 JavaScript 部分綁定,圖示圖片會與工具列元件綁定,而 WebAssembly 模組會與其 JavaScript 黏合劑綁定。在這種情況下,直接從 JavaScript 模組參照資源,並在載入相應元件時動態載入資源,會更方便。

圖表呈現匯入至 JS 的各種資產類型。

不過,大部分的大型專案都有建置系統,可對內容進行額外最佳化調整及重新編排,例如合併和壓縮。他們不能執行程式碼並預測執行結果,也無法掃遍 JavaScript 中所有可能的字串常值,並猜測是否為資源網址。那麼,您要如何讓這些元件「看到」由 JavaScript 元件載入的動態素材資源,並將這些素材資源納入建構?

在 Bundler 中使用自訂匯入

其中一種常見方法是重複使用靜態匯入語法。在部分整合程式中,檔案副檔名可能會自動偵測格式,其他套件則允許外掛程式使用自訂網址通訊協定,如以下範例所示:

// 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 匯入語法可保證所有網址都是靜態的,且與目前檔案相對,如此有助於建構系統輕鬆找到這類依附元件。

不過,這還有一個重大的缺點:這類程式碼無法直接在瀏覽器中運作,因為瀏覽器不知道如何處理這些自訂匯入配置或擴充功能。如果您控制所有程式碼,並且依賴套件組合進行開發,這麼做或許沒問題,但在瀏覽器中直接使用 JavaScript 模組 (至少在開發期間) 的做法越來越普遍,可減少摩擦。製作小型示範內容的開發人員可能根本不需要使用 Bundler,即使是正式版也是如此。

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

如果您使用的是可重複使用的元件,建議您讓這個元件直接在任一環境中運作,無論該元件是直接在瀏覽器中使用,還是預先建構成大型應用程式的一部分。大部分新型套件器都允許在 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 旁的依附元件。

模糊不清的相對網址

您可能會想知道,為何 Bundler 無法偵測其他常見模式,例如沒有 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 模組,並讓 bundler 在建構期間找到這些相對路徑。

工具支援

Bundler

以下是已支援 new URL 配置的套件組合器:

WebAssembly

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

透過 Emscripten 執行 C/C++

使用 Emscripten 時,您可以透過下列任一選項,要求 Emscripten 將 JavaScript 黏合劑以 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,並讓 bundler 和瀏覽器都能發現。

透過 wasm-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 檔案也會由 bundler 自動偵測。

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

簡單來說,您無法使用任意執行緒 API,但如果使用 Rayon,則可以將其與 wasm-bindgen-rayon 轉接器結合,以便在網路上產生 Worker。wasm-bindgen-rayon 使用的 JavaScript 黏合劑也包含幕後的 new URL(...) 模式,因此 Worker 可供 bundler 發現及納入。

未來功能

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) 模式是目前最有希望的解決方案,可在瀏覽器、各種 bundler 和 WebAssembly 工具鏈中運作。