Type="Module" 你了解,但 Type="Importmap" 你知道嗎?
當ES模塊第一次在ECMAScript 2015中被引入,作為在JavaScript中標準化模塊系統的一種方式時,它是通過在import語句中指定相對或絕對路徑來實現的。
import dayjs from "https://cdn.skypack.dev/dayjs@1.10.7"; // ES modules
console.log(dayjs("2019-01-25").format("YYYY-MM-DDTHH:mm:ssZ[Z]"));
這與模塊在其他通用模塊系統中的工作方式略有不同,例如CommonJS,以及在使用webpack這樣的模塊捆綁器時,使用的是更簡單的語法。
const dayjs = require('dayjs') // CommonJS
import dayjs from 'dayjs'; // webpack
在這些系統中,通過Node.js運行時或相關的構建工具,導入指定器被映射到一個特定(和版本)的文件。用戶只需要在導入語句中應用裸露的模塊指定符(通常是包名),圍繞模塊解析的問題就會被自動解決。
由于開發者已經熟悉了這種從npm導入包的方式,所以需要一個構建步驟來確保以這種方式編寫的代碼能夠在瀏覽器中運行。這個問題由import maps解決了。從本質上講,它允許將導入指定器映射到相對或絕對的URL上,這有助于控制模塊的解析,而不需要應用構建步驟。
import maps 是怎么工作的
<script type="importmap">
{
"imports": {
"dayjs": "https://cdn.skypack.dev/dayjs@1.10.7",
}
}
</script>
<script type="module">
import dayjs from 'dayjs';
console.log(dayjs('2019-01-25').format('YYYY-MM-DDTHH:mm:ssZ[Z]'));
</script>
import map 是通過HTML document中的 <script type="importmap">標簽指定的。這個script 標簽必須放在 document 中的中第一個 <script type="module">標簽之前(最好是在<head>中),以便在進行模塊解析之前對它進行解析。此外,目前每個 document 只允許有一個 import map,未來可能會取消這一限制。
在 script 標簽內,一個JSON對象被用來指定document中 script 所需的所有必要的模塊映射。一個典型的 import map 的結構如下所示。
<script type="importmap">
{
"imports": {
"react": "https://cdn.skypack.dev/react@17.0.1",
"react-dom": "https://cdn.skypack.dev/react-dom",
"square": "./modules/square.js",
"lodash": "/node_modules/lodash-es/lodash.js"
}
}
</script>
在上面的 imports 對象中,每個屬性都對應著一個映射。映射的左邊是 import 指定器的名稱,而右邊是指定器應該映射到的相對或絕對URL。
當在映射中指定相對URL時,確保它們總是以/、./或./開頭。請注意,在 import map 中出現包并不意味著它一定會被瀏覽器加載。任何沒有被頁面上的 script 使用的模塊都不會被瀏覽器加載,即使它存在于import map中。
<script type="importmap" src="importmap.json"></script>
你也可以在一個外部文件中指定你的映射,然后使用src屬性鏈接到該文件(如上所示)。如果決定使用這種方法,請確保在發送文件時將其Content-Type標頭設置為application/importmap+json。
注意,出于性能方面的考慮,推薦使用內聯方式,本文的其余部分的事例,也會使用內聯方式。
一旦指定了映射,就可以在import語句中使用import說明符,如下所示:
<script type="module">
import { cloneDeep } from 'lodash';
const objects = [{ a: 1 }, { b: 2 }];
const deep = cloneDeep(objects);
console.log(deep[0] === objects[0]);
</script>
需要注意的是,導入映射中的映射不會影響諸如<script>標簽的 src 屬性之類的位置。因此,如你的使用<script src="/app.js">之類的內容,瀏覽器將試圖在該路徑上下載一個字面上的app.js文件,而不管 import map 中的內容如何。
將指定者映射到整個包中
除了將一個指定器映射到一個模塊,你也可以將一個指定器映射到一個包含多個模塊的包。這是通過使用指定器鍵和以尾部斜線結尾的路徑來實現的。
<script type="importmap">
{
"imports": {
"lodash/": "/node_modules/lodash-es/"
}
}
</script>
這種方法允許我們導入指定路徑中的任何模塊,而不是整個主模塊,這會導致所有組件模塊由瀏覽器下載。
<script type="module">
import toUpper from 'lodash/toUpper.js';
import toLower from 'lodash/toLower.js';
console.log(toUpper('hello'));
console.log(toLower('HELLO'));
</script>
動態地構建 import map
映射也可以基于任意條件在 script 中動態構造,這種能力可以用來根據特征檢測有條件地導入模塊。下面的例子根據IntersectionObserver API是否被支持,在lazyload指定器下選擇正確的文件進行導入。
<script>
const importMap = {
imports: {
lazyload: 'IntersectionObserver' in window
? './lazyload.js'
: './lazyload-fallback.js',
},
};
const im = document.createElement('script');
im.type = 'importmap';
im.textContent = JSON.stringify(importMap);
document.currentScript.after(im);
</script>
如果你想使用這種方法,請確保在創建和插入 import map 腳本標簽之前進行(如上所述),因為修改一個已經存在的導入地圖對象不會有任何效果。
通過對哈希值的映射來提高腳本的可緩存性
實現靜態文件長期緩存的常見技術是在文件名中使用文件內容的哈希值,這樣文件就會一直在瀏覽器的緩存中,直到文件內容發生變化。當這種情況發生時,文件將得到一個新的名字,以便最新的更新立即反映在應用程序中。
在傳統的 bundling scripts,的方式下,如果一個被多個模塊依賴的依賴關系被更新,這種技術就會出現問題。這將導致所有依賴該依賴的文件被更新,迫使瀏覽器重新下載它們,即使只有一個字符的代碼被改變。
import map 為這個問題提供了一個解決方案,它允許通過重映射技術單獨更新每個依賴關系。假設你需要從一個名為post.bundle.8cb615d12a121f6693aa.js的文件中導入一個方法:
<script type="importmap">
{
"imports": {
"post.js": "./static/dist/post.bundle.8cb615d12a121f6693aa.js",
}
}
</script>
而不是這樣寫:
import { something } from './static/dist/post.bundle.8cb615d12a121f6693aa.js'
可以這么寫:
import { something } from 'post.js'
當更新文件的時候,只有 import map 需要更新。由于對其導出的引用沒有更改,它們將保持在瀏覽器中的緩存,同時由于更新的哈希值,更新的腳本將再次被下載。
<script type="importmap">
{
"imports": {
"post.js": "./static/dist/post.bundle.6e2bf7368547b6a85160.js",
}
}
</script>
使用同一模塊的多個版本
在 import map 中很容易實現一個包對應多個版本,所需要做的就是在映射中使用不同的導入指定符,如下圖所示:
<script type="importmap">
{
"imports": {
"lodash@3/": "https://unpkg.com/lodash-es@3.10.1/",
"lodash@4/": "https://unpkg.com/lodash-es@4.17.21/"
}
}
</script>
通過使用作用域,也可以用同一個導入指定符來指代同一個包的不同版本。這允許我們在一個給定的作用域內改變導入指定符的含義。
<script type="importmap">
{
"imports": {
"lodash/": "https://unpkg.com/lodash-es@4.17.21/"
},
"scopes": {
"/static/js": {
"lodash/": "https://unpkg.com/lodash-es@3.10.1/"
}
}
}
</script>
有了這種映射,在/static/js路徑下的任何模塊,在導入語句中引用lodash/指定器時,將使用https://unpkg.com/lodash-es@3.10.1/,而其他模塊將使用https://unpkg.com/lodash-es@4.17.21/。
使用帶有 import map 的 NPM 包
正如在本文中所展示的,任何使用ES Modules的NPM包的生產版本都可以通過ESM、Unpkg和Skypack等CDN在 import map中使用。
即使NPM上的包不是為ES模塊系統和本地瀏覽器導入行為設計的,像Skypack和ESM這樣的服務也可以將它們轉化為可在導入地圖中使用的包。可以使用Skypack主頁上的搜索欄來尋找瀏覽器優化的NPM包,這些包可以立即使用,而無需擺弄構建步驟。
檢測 import map支持
只要支持HTMLScriptElement.supports()方法,就可以在瀏覽器中檢測 import map的支持:?
if (HTMLScriptElement.supports && HTMLScriptElement.supports('importmap')) {
// import maps is supported
}
支持舊的瀏覽器
Import map 使得在瀏覽器中使用裸模塊指定器成為可能,而無需依賴目前在JavaScript生態系統中普遍存在的復雜的構建系統,但目前網絡瀏覽器中并不廣泛支持它。
在整理本文時,Chrome和Edge瀏覽器的89版及以后的版本提供了全面支持,但Firefox、Safari和一些移動瀏覽器不支持這項技術。為了在這些瀏覽器中保留對 import map的使用,必須采用一個合適的 polyfill 。
一個可以使用的polyfill的例子是ES Module Shims polyfill,它為任何支持ES模塊基線的瀏覽器(約94%的瀏覽器)添加了 import map 和其他新模塊特性的支持。我們所需要做的就是在 import map 腳本之前在HTML文件中包含es-module-shim腳本。
<script async src="https://unpkg.com/es-module-shims@1.3.0/dist/es-module-shims.js"></script>
在包括polyfill之后,可能會在你的控制臺中得到一個JavaScript TypeError。這個錯誤可以被安全地忽略,因為它不會產生任何面向用戶的后果。
總結
import map提供了一種更理智的方式來在瀏覽器中使用ES模塊,而不局限于從相對或絕對的URL中導入。這使得我們可以很容易地移動代碼,而不需要調整 import語句,并使個別模塊的更新更加無縫,而不影響依賴這些模塊的腳本的緩存能力。總的來說,import map為ES模塊在服務器和瀏覽器中的使用方式帶來了平等性。