瞭解如何從 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,即使是正式版也是如此。
瀏覽器和套件器的通用模式
如果您使用的是可重複使用的元件,建議您讓這個元件直接在任一環境中運作,無論該元件是直接在瀏覽器中使用,還是預先建構成大型應用程式的一部分。大部分新型套件器都允許在 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
配置的套件組合器:
- Webpack 第 5 版
- 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 自動偵測。
如果您想將 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 工具鏈中運作。