瞭解如何從 JavaScript 匯入及捆綁各種類型的素材資源。
假設您正在開發網路應用程式,那麼您可能不僅要處理 JavaScript 模組,還要處理各種其他資源,包括網路工作站 (也是 JavaScript,但不是一般模組圖表的一部分)、圖片、樣式表、字型、WebAssembly 模組等。
您可以直接在 HTML 中加入部分資源的參照,但這些資源通常會在邏輯上與可重複使用的元件耦合。舉例來說,自訂下拉式選單的樣式表與其 JavaScript 部分相連、圖示圖片與工具列元件相連,或是 WebAssembly 模組與其 JavaScript 黏合劑相連。在這種情況下,直接從 JavaScript 模組參照資源,並在載入相應元件時動態載入資源,會更方便。
不過,大多數大型專案都會使用建構系統,對內容進行額外最佳化和重組,例如將內容整合和縮減。他們無法執行程式碼,也無法預測執行結果,也無法在 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,即使是在正式版也是如此。
適用於瀏覽器和 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)
等任意運算式搭配使用,但 Bundler 會為含有靜態網址 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 模組,並讓 bundler 在建構期間找到這些相對路徑。
工具支援
Bundler
以下是已支援 new URL
配置的套件組合器:
- Webpack v5
- Rollup (透過外掛程式達成,一般資產使用 @web/rollup-plugin-import-meta-assets,而 Worker 則使用 @surma/rollup-plugin-off-main-thread)。
- Parcel v2 (Beta 版)
- Vite
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 偵測。
如果您想在 Rust 中使用 WebAssembly 執行緒,情況會稍微複雜一些。詳情請參閱指南的對應部分。
簡單來說,您無法使用任意執行緒 API,但如果使用 Rayon,則可以將其與 wasm-bindgen-rayon 轉接器結合,以便在網路上產生 Worker。wasm-bindgen-rayon 使用的 JavaScript 黏合劑也包含 幕後的 new URL(...)
模式,因此 bundler 也能發現並納入 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)
模式是目前在瀏覽器、各種 bundler 和 WebAssembly 工具鍊中運作最有希望的解決方案。