打包非 JavaScript 靜態(tài)資源詳情
本文翻譯自 https://web.dev/bundling-non-js-resources/,原文未做修改
假設(shè)你正在開發(fā)一個網(wǎng)絡(luò)應(yīng)用程序。在這種情況下,你很可能不僅要處理 JavaScript 模塊,還要處理各種其他資源--Web Workers
(它也是 JavaScript ,但它擁有一套獨(dú)立的構(gòu)建依賴圖)、圖片、CSS、字體、WebAssembly
模塊等等。
一種可行的加載靜態(tài)資源的辦法是在 HTML 中直接引用它們,但通常它們在邏輯上是與其他可重用的組件耦合的。例如,自定義下拉菜單的 CSS
與它的 JavaScript
部分相聯(lián)系,圖標(biāo)圖像與工具欄組件相關(guān),而 WebAssembly
模塊與它的 JavaScript
膠水相依賴。在這些情況下,有種更加方便快捷的辦法是直接從它們的 JavaScript
模塊中引用資源,并在加載相應(yīng)的組件時動態(tài)地加載它們。
然而,大多數(shù)大型項(xiàng)目的構(gòu)建系統(tǒng)都會對內(nèi)容進(jìn)行額外的優(yōu)化和重組--例如打包和最小化(minimize
)。構(gòu)建系統(tǒng)不能執(zhí)行代碼并預(yù)測執(zhí)行的結(jié)果是什么,也沒理由去遍歷判斷 JavaScript
中每一個可能的字符串是否是一個資源 URL。那么,如何才能讓它們 "看到 "那些由 JavaScript
組件加載的動態(tài)資源,并將它們包含在構(gòu)建產(chǎn)物中呢?
1、打包工具中的自定義導(dǎo)入
一種常見的方法是利用已有的靜態(tài)導(dǎo)入語法。有些打包工具可能會通過文件擴(kuò)展名來自動檢測格式,而有些其他打包工具則允許插件使用自定義的 URL Scheme
,比如下面的例子:
// 普通 JavaScript 導(dǎo)入 import { loadImg } from './utils.js'; // 特殊 "URL 導(dǎo)入" 的靜態(tài)資源 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);
當(dāng)一個打包工具插件發(fā)現(xiàn)一個導(dǎo)入項(xiàng)帶有它所識別的擴(kuò)展名或 URL Scheme
(上面的例子中的 asset-url: 和 js-url: )時,它會將引用的資源添加到構(gòu)建圖中,將其復(fù)制到最終目的地,執(zhí)行適用于資源類型的優(yōu)化,并返回最終的 URL,以便在運(yùn)行時使用。
這種方法的好處是:重用 JavaScript
導(dǎo)入語法,保證所有的 URL 都是靜態(tài)的相對路徑,這使得構(gòu)建系統(tǒng)很容易定位這種依賴關(guān)系。
然而,它有一個明顯的缺點(diǎn):這種代碼不能直接在瀏覽器中工作,因?yàn)闉g覽器不知道如何處理那些自定義的導(dǎo)入方案或擴(kuò)展名。當(dāng)然,如果你可以控制所有的代碼,并且本來就要依靠打包工具進(jìn)行開發(fā),這聽起來還不錯。然而為了減少麻煩,直接在瀏覽器中使用 JavaScript
模塊的情況越來越普遍(至少在開發(fā)過程中是這樣)。一個小 demo
可能根本就不需要打包工具,即使在生產(chǎn)中也不需要。
2、瀏覽器和打包工具中通用的導(dǎo)入語法
如果你正在開發(fā)一個可重用的組件,你會希望它在任何環(huán)境下都能發(fā)揮作用,無論它是直接在瀏覽器中使用還是作為一個更大的應(yīng)用程序的一部分預(yù)先構(gòu)建。大多數(shù)現(xiàn)代的打包工具都接受下面這個JavaScript 模塊導(dǎo)入語法:
new URL('./relative-path', import.meta.url)
它看著像是一種特殊的語法,然而它確實(shí)是一種有效的 JavaScript
表達(dá)式,可以直接在瀏覽器中使用,也可以被打包工具靜態(tài)地檢測出來并加以處理。
使用這個語法,前面的例子可以改寫為:
// 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));
讓我們分析一下它是什么原理: new URL(...)
構(gòu)造函數(shù)會基于第二個參數(shù)里的絕對URL,解析出第一個參數(shù)中相對 URL 所對應(yīng)的 URL。在我們的例子中,第二個參數(shù)是 import.meta.url [1]
,它是當(dāng)前 JavaScript
模塊的 URL ,所以第一個參數(shù)可以是相對于它的任何路徑。
它的優(yōu)點(diǎn)和劣勢都類似于 動態(tài)導(dǎo)入。雖然可以使用 import(...) 導(dǎo)入內(nèi)容,如 import
(someUrl) ,但打包工具會特殊處理帶有靜態(tài) URL import
('./some-static-url.js') 的導(dǎo)入方式:把它作為一種在編譯時預(yù)處理已知依賴關(guān)系的導(dǎo)入方式,把 代碼分塊 并動態(tài)加載。
同樣,你可以使用 new URL
(...) ,如 new URL
(relativeUrl, customAbsoluteBase) ,然而 new URL
('...', import.meta.url) 語法可以明確地告訴打包工具預(yù)處理依賴,并將其與主 JavaScript 資源打包在一起。
3、模棱兩可的相對URL
你可能會想,為什么打包工具不能檢測到其他常見的語法--例如,沒有 new URL
包裝的 fetch
('./module.wasm') ?
原因是,與 import
關(guān)鍵字不同,任何動態(tài)請求都是相對于文檔本身的,而不是相對于當(dāng)前的JavaScript文件進(jìn)行解析。比方說,我們有以下結(jié)構(gòu):
index.html:
<script src="src/main.js" type="module"></script> src/ main.js module.wasm
如果你想從 main.js
中加載 module.wasm
,你的第一反應(yīng)可能是使用 fetch('./module.wasm') 這樣的相對路徑引用。
然而,fetch
不知道它所執(zhí)行的 JavaScript
文件的 URL,相反,它是相對于文檔來解析 URL 的。因此, fetch
('./module.wasm') 最終會試圖加載 http://example.com/module.wasm
,而不是預(yù)期的 http://example.com/src/module.wasm
,從而造成失?。ㄟ\(yùn)氣更不好的情況下,還可能默默地加載一個與你預(yù)期不同的資源)。
通過將相對的URL包裝成 new URL
('...', import.meta.url) ,你可以避免這個問題,并保證任何提供的URL在傳遞給任何loader之前都是相對于 當(dāng)前 JavaScript 模塊的 URL(import.meta.url) 解析的。
只要用 fetch
(new URL('./module.wasm', import.meta.url)) 代替 fetch
('./module.wasm') ,就可以成功地加載預(yù)期的 WebAssembly
模塊,同時給打包工具一個在構(gòu)建時找到這些相對路徑的可靠方法。
4、工具鏈中的支持
4.1 打包工具
下面這些打包工具已經(jīng)支持 new URL 語法:
Webpack v5
Rollup
(通過插件支持: @web/rollup-plugin-import-meta-assets 支持通用資源,而 @surma/rollup-plugin-off-main-thread
支持 Workers.)Parcel v2 (beta)
(譯者注:在本譯文發(fā)布時,Parcel V2已經(jīng)正式發(fā)布:https://parceljs.org/blog/v2)Vite
5、 WebAssembly
當(dāng)使用 WebAssembly
時,你通常不會手動加載 Wasm
模塊,而是導(dǎo)入由工具鏈發(fā)出的 JavaScript
膠水代碼。下面的工具鏈可以替你生成 new URL(...) 語法:
5.1 通過Emscripten編譯的C/C++
當(dāng)使用 Emscripten
工具鏈時,我們可以通過以下選項(xiàng)要求它輸出 ES6 模塊膠水代碼,而非普通 JS 代碼:
$ emcc input.cpp -o output.mjs ## 如果你不想用mjs擴(kuò)展名: $ emcc input.cpp -o output.js -s EXPORT_ES6
當(dāng)使用這個選項(xiàng)時,輸出的膠水代碼將使用new URL
(..., import.meta.url) 語法,這樣打包工具可以自動找到相關(guān)的 Wasm 文件。
通過添加 -pthread
參數(shù),這個語法也可以支持 WebAssembly
線程的編譯
$ emcc input.cpp -o output.mjs -pthread ## 如果你不想用mjs擴(kuò)展名: $ emcc input.cpp -o output.js -s EXPORT_ES6 -pthread
在這種情況下,生成的Web Worker
將以同樣的方式被引用,并且也能被打包工具和瀏覽器正確加載。
5.2 通過 wasm-pack / wasm-bindgen 編譯的 Rust
wasm-pack --WebAssembly
的主要 Rust 工具鏈,也有幾種輸出模式。
默認(rèn)情況下,它將輸出一個依賴于 WebAssembly ESM
集成提議 的 JavaScript
模塊。在寫這篇文章的時候,這個提議仍然是實(shí)驗(yàn)性的,只有在使用 Webpack
打包時,輸出才會有效。
或者,我們可以通過 -target web 參數(shù)要求 wasm-pack 通過輸出一個與瀏覽器兼容的 ES6 模塊:
$ wasm-pack build --target web
輸出將使用前面所說的 new URL(..., import.meta.url) 語法,而且 Wasm 文件也會被打包工具自動發(fā)現(xiàn)。
如果你想通過 Rust
使用 WebAssembly
線程,這就有點(diǎn)復(fù)雜了。請查看指南的 相應(yīng)部分 [13] 以了解更多。
簡而言之,你不能使用任意的線程 API,但如果你使用 Rayon [14] ,你可以試試 wasm-bingen-rayon [15] 適配器,這樣它就可以生成 Web 上可以運(yùn)行的 Worker 。 wasm-bindgen-rayon
使用的 JavaScript 膠水 也包括 [16] new URL (...)語法,因此 Workers
也能被打包工具發(fā)現(xiàn)和引入。
6、未來的導(dǎo)入方式
6.1 import.meta.resolve
有一個潛在的未來改進(jìn)是專門的 import.meta.resolve(...)
語法。它將允許以一種更直接的方式解析相對于當(dāng)前模塊的內(nèi)容,而不需要額外的參數(shù)。
// 現(xiàn)在的語法 new URL('...', import.meta.url) // 未來的語法 await import.meta.resolve('...')
它還能與導(dǎo)入依賴圖(import maps
)還有自定義解析器更好地整合,因?yàn)樗?import
語法通過同一個模塊解析系統(tǒng)處理。這對打包工具來說也是一個更可靠的信號,因?yàn)樗且粋€靜態(tài)語法,不依賴于像 URL 這樣的運(yùn)行時 API 。
import.meta.resolve
已經(jīng)作為一個 實(shí)驗(yàn)性功能 在 Node.js 中實(shí)現(xiàn)了,但是關(guān)于它在 Web 上應(yīng)該如何工作 還有一些問題沒有定論。
6.2 導(dǎo)入斷言
導(dǎo)入斷言(import assertions)是一項(xiàng)新功能,允許導(dǎo)入 ECMAScript 模塊以外的類型,不過現(xiàn)在只支持JSON 類型。
foo.json
{ "answer": 42 }
main.mjs
import json from './foo.json' assert { type: 'json' }; console.log(json.answer); // 42
(譯者注:關(guān)于這個不太符合直覺的語法選擇也有點(diǎn)意思 https://github.com/tc39/proposal-import-assertions/issues/12)
它們也可能被打包工具使用,并取代目前由new URL語法所支持的場景,但導(dǎo)入斷言中的類型需要一個一個被支持,目前被支持的只有 JSON
,CSS
模塊即將被支持,但其他類型的資源導(dǎo)入仍然需要一個更通用的解決方案。
要想了解更多關(guān)于這個功能的信息,請查看 v8.dev上的功能解釋 [19] 。
7、小結(jié)
正如你所看到的,有各種方法可以在網(wǎng)絡(luò)上包含非 JavaScript
資源,但它們有各自的優(yōu)缺點(diǎn),而且都不能同時在所有工具鏈中工作。一些未來的提議可能會讓我們用專門的語法來導(dǎo)入這些資源,但我們還沒有走到這一步。
在那一天到來之前, new URL
(..., import.meta.url) 語法是最有希望的解決方案,并且今天已經(jīng)可以在瀏覽器、各種捆綁器和 WebAssembly
工具鏈中工作。
到此這篇關(guān)于打包非 JavaScript 靜態(tài)資源詳情的文章就介紹到這了,更多相關(guān)打包非 JavaScript 靜態(tài)資源內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
8、參考資料
[1];import.meta.url: https://v8.dev/features/modules#import-meta
[2];動態(tài)導(dǎo)入: https://v8.dev/features/dynamic-import
[3]:代碼分塊: https://web.dev/reduce-javascript-payloads-with-code-splitting/
[4]:Webpack v5: https://webpack.js.org/guides/asset-modules/#url-assets
[5]:Rollup: https://rollupjs.org/
[6]:@web/rollup-plugin-import-meta-assets: https://modern-web.dev/docs/building/rollup-plugin-import-meta-assets/
[7]:@surma/rollup-plugin-off-main-thread: https://github.com/surma/rollup-plugin-off-main-thread
[8]:Parcel v2 (beta): https://v2.parceljs.org/languages/javascript/#url-dependencies
[9]:Vite: https://vitejs.dev/guide/assets.html#new-url-url-import-meta-url
[10]:WebAssembly: https://web.dev/webassembly-threads/#c
[11]:wasm-pack: https://github.com/rustwasm/wasm-pack
[12]:WebAssembly ESM 集成提議: https://github.com/WebAssembly/esm-integration
[13]:相應(yīng)部分: https://web.dev/webassembly-threads/#rust
[14]:Rayon: https://github.com/rayon-rs/rayon
[15]:wasm-bingen-rayon: https://github.com/GoogleChromeLabs/wasm-bindgen-rayon
[16]:也包括: https://github.com/GoogleChromeLabs/wasm-bindgen-rayon/blob/4cd0666d2089886d6e8731de2371e7210f848c5d/demo/index.js#L26
[17]:實(shí)驗(yàn)性功能: https://nodejs.org/api/esm.html#esm_import_meta_resolve_specifier_parent
[18]:還有一些問題沒有定論: https://github.com/WICG/import-maps/issues/79
[19]:v8.dev上的功能解釋: https://v8.dev/features/import-assertions
相關(guān)文章
微信小程序 實(shí)戰(zhàn)實(shí)例開發(fā)流程詳細(xì)介紹
這篇文章主要介紹了微信小程序 實(shí)戰(zhàn)實(shí)例開發(fā)流程詳細(xì)介紹的相關(guān)資料,這里主要介紹微信小程序的開發(fā)流程和簡單實(shí)例,需要的朋友可以參考下2017-01-01document 和 document.all 分別什么時候用
document 和 document.all 分別什么時候用...2006-06-06Servlet3.0與純javascript通過Ajax交互的實(shí)例詳解
Servlet與純javascript通過Ajax交互,對于很多人來說應(yīng)該很簡單。不過還是寫寫,方便Ajax學(xué)習(xí)的后來者2018-03-03微信小程序 動態(tài)綁定事件并實(shí)現(xiàn)事件修改樣式
這篇文章主要介紹了微信小程序 動態(tài)綁定事件并實(shí)現(xiàn)事件修改樣式的相關(guān)資料,需要的朋友可以參考下2017-04-04微信小程序 WXDropDownMenu組件詳解及實(shí)例代碼
這篇文章主要介紹了微信小程序 WXDropDownMenu組件詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-10-10微信小程序getPhoneNumber獲取用戶手機(jī)號
這篇文章主要介紹了 微信小程序getPhoneNumber獲取用戶手機(jī)號的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-09-09javascript中的箭頭函數(shù)基礎(chǔ)語法及使用場景示例
這篇文章主要為大家介紹了?javascript中的箭頭函數(shù)基礎(chǔ)語法及使用場景示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07JavaScript面試數(shù)組index和對象key問題詳解
這篇文章主要為大家介紹了JavaScript面試數(shù)組index和對象key問題詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12