Type="Module" 你了解,但 Type="Importmap" 你知道嗎?
當(dāng)ES模塊第一次在ECMAScript 2015中被引入,作為在JavaScript中標(biāo)準(zhǔn)化模塊系統(tǒng)的一種方式時(shí),它是通過在import語句中指定相對(duì)或絕對(duì)路徑來實(shí)現(xiàn)的。
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]"));
這與模塊在其他通用模塊系統(tǒng)中的工作方式略有不同,例如CommonJS,以及在使用webpack這樣的模塊捆綁器時(shí),使用的是更簡(jiǎn)單的語法。
const dayjs = require('dayjs') // CommonJS
import dayjs from 'dayjs'; // webpack
在這些系統(tǒng)中,通過Node.js運(yùn)行時(shí)或相關(guān)的構(gòu)建工具,導(dǎo)入指定器被映射到一個(gè)特定(和版本)的文件。用戶只需要在導(dǎo)入語句中應(yīng)用裸露的模塊指定符(通常是包名),圍繞模塊解析的問題就會(huì)被自動(dòng)解決。
由于開發(fā)者已經(jīng)熟悉了這種從npm導(dǎo)入包的方式,所以需要一個(gè)構(gòu)建步驟來確保以這種方式編寫的代碼能夠在瀏覽器中運(yùn)行。這個(gè)問題由import maps解決了。從本質(zhì)上講,它允許將導(dǎo)入指定器映射到相對(duì)或絕對(duì)的URL上,這有助于控制模塊的解析,而不需要應(yīng)用構(gòu)建步驟。
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">標(biāo)簽指定的。這個(gè)script 標(biāo)簽必須放在 document 中的中第一個(gè) <script type="module">標(biāo)簽之前(最好是在<head>中),以便在進(jìn)行模塊解析之前對(duì)它進(jìn)行解析。此外,目前每個(gè) document 只允許有一個(gè) import map,未來可能會(huì)取消這一限制。
在 script 標(biāo)簽內(nèi),一個(gè)JSON對(duì)象被用來指定document中 script 所需的所有必要的模塊映射。一個(gè)典型的 import map 的結(jié)構(gòu)如下所示。
<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 對(duì)象中,每個(gè)屬性都對(duì)應(yīng)著一個(gè)映射。映射的左邊是 import 指定器的名稱,而右邊是指定器應(yīng)該映射到的相對(duì)或絕對(duì)URL。
當(dāng)在映射中指定相對(duì)URL時(shí),確保它們總是以/、./或./開頭。請(qǐng)注意,在 import map 中出現(xiàn)包并不意味著它一定會(huì)被瀏覽器加載。任何沒有被頁(yè)面上的 script 使用的模塊都不會(huì)被瀏覽器加載,即使它存在于import map中。
<script type="importmap" src="importmap.json"></script>
你也可以在一個(gè)外部文件中指定你的映射,然后使用src屬性鏈接到該文件(如上所示)。如果決定使用這種方法,請(qǐng)確保在發(fā)送文件時(shí)將其Content-Type標(biāo)頭設(shè)置為application/importmap+json。
注意,出于性能方面的考慮,推薦使用內(nèi)聯(lián)方式,本文的其余部分的事例,也會(huì)使用內(nèi)聯(lián)方式。
一旦指定了映射,就可以在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>
需要注意的是,導(dǎo)入映射中的映射不會(huì)影響諸如<script>標(biāo)簽的 src 屬性之類的位置。因此,如你的使用<script src="/app.js">之類的內(nèi)容,瀏覽器將試圖在該路徑上下載一個(gè)字面上的app.js文件,而不管 import map 中的內(nèi)容如何。
將指定者映射到整個(gè)包中
除了將一個(gè)指定器映射到一個(gè)模塊,你也可以將一個(gè)指定器映射到一個(gè)包含多個(gè)模塊的包。這是通過使用指定器鍵和以尾部斜線結(jié)尾的路徑來實(shí)現(xiàn)的。
<script type="importmap">
{
"imports": {
"lodash/": "/node_modules/lodash-es/"
}
}
</script>
這種方法允許我們導(dǎo)入指定路徑中的任何模塊,而不是整個(gè)主模塊,這會(huì)導(dǎo)致所有組件模塊由瀏覽器下載。
<script type="module">
import toUpper from 'lodash/toUpper.js';
import toLower from 'lodash/toLower.js';
console.log(toUpper('hello'));
console.log(toLower('HELLO'));
</script>
動(dòng)態(tài)地構(gòu)建 import map
映射也可以基于任意條件在 script 中動(dòng)態(tài)構(gòu)造,這種能力可以用來根據(jù)特征檢測(cè)有條件地導(dǎo)入模塊。下面的例子根據(jù)IntersectionObserver API是否被支持,在lazyload指定器下選擇正確的文件進(jìn)行導(dǎo)入。
<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>
如果你想使用這種方法,請(qǐng)確保在創(chuàng)建和插入 import map 腳本標(biāo)簽之前進(jìn)行(如上所述),因?yàn)樾薷囊粋€(gè)已經(jīng)存在的導(dǎo)入地圖對(duì)象不會(huì)有任何效果。
通過對(duì)哈希值的映射來提高腳本的可緩存性
實(shí)現(xiàn)靜態(tài)文件長(zhǎng)期緩存的常見技術(shù)是在文件名中使用文件內(nèi)容的哈希值,這樣文件就會(huì)一直在瀏覽器的緩存中,直到文件內(nèi)容發(fā)生變化。當(dāng)這種情況發(fā)生時(shí),文件將得到一個(gè)新的名字,以便最新的更新立即反映在應(yīng)用程序中。
在傳統(tǒng)的 bundling scripts,的方式下,如果一個(gè)被多個(gè)模塊依賴的依賴關(guān)系被更新,這種技術(shù)就會(huì)出現(xiàn)問題。這將導(dǎo)致所有依賴該依賴的文件被更新,迫使瀏覽器重新下載它們,即使只有一個(gè)字符的代碼被改變。
import map 為這個(gè)問題提供了一個(gè)解決方案,它允許通過重映射技術(shù)單獨(dú)更新每個(gè)依賴關(guān)系。假設(shè)你需要從一個(gè)名為post.bundle.8cb615d12a121f6693aa.js的文件中導(dǎo)入一個(gè)方法:
<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'
當(dāng)更新文件的時(shí)候,只有 import map 需要更新。由于對(duì)其導(dǎo)出的引用沒有更改,它們將保持在瀏覽器中的緩存,同時(shí)由于更新的哈希值,更新的腳本將再次被下載。
<script type="importmap">
{
"imports": {
"post.js": "./static/dist/post.bundle.6e2bf7368547b6a85160.js",
}
}
</script>
使用同一模塊的多個(gè)版本
在 import map 中很容易實(shí)現(xiàn)一個(gè)包對(duì)應(yīng)多個(gè)版本,所需要做的就是在映射中使用不同的導(dǎo)入指定符,如下圖所示:
<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>
通過使用作用域,也可以用同一個(gè)導(dǎo)入指定符來指代同一個(gè)包的不同版本。這允許我們?cè)谝粋€(gè)給定的作用域內(nèi)改變導(dǎo)入指定符的含義。
<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路徑下的任何模塊,在導(dǎo)入語句中引用lodash/指定器時(shí),將使用https://unpkg.com/lodash-es@3.10.1/,而其他模塊將使用https://unpkg.com/lodash-es@4.17.21/。
使用帶有 import map 的 NPM 包
正如在本文中所展示的,任何使用ES Modules的NPM包的生產(chǎn)版本都可以通過ESM、Unpkg和Skypack等CDN在 import map中使用。
即使NPM上的包不是為ES模塊系統(tǒng)和本地瀏覽器導(dǎo)入行為設(shè)計(jì)的,像Skypack和ESM這樣的服務(wù)也可以將它們轉(zhuǎn)化為可在導(dǎo)入地圖中使用的包。可以使用Skypack主頁(yè)上的搜索欄來尋找瀏覽器優(yōu)化的NPM包,這些包可以立即使用,而無需擺弄構(gòu)建步驟。
檢測(cè) import map支持
只要支持HTMLScriptElement.supports()方法,就可以在瀏覽器中檢測(cè) import map的支持:?
if (HTMLScriptElement.supports && HTMLScriptElement.supports('importmap')) {
// import maps is supported
}
支持舊的瀏覽器
Import map 使得在瀏覽器中使用裸模塊指定器成為可能,而無需依賴目前在JavaScript生態(tài)系統(tǒng)中普遍存在的復(fù)雜的構(gòu)建系統(tǒng),但目前網(wǎng)絡(luò)瀏覽器中并不廣泛支持它。
在整理本文時(shí),Chrome和Edge瀏覽器的89版及以后的版本提供了全面支持,但Firefox、Safari和一些移動(dòng)瀏覽器不支持這項(xiàng)技術(shù)。為了在這些瀏覽器中保留對(duì) import map的使用,必須采用一個(gè)合適的 polyfill 。
一個(gè)可以使用的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之后,可能會(huì)在你的控制臺(tái)中得到一個(gè)JavaScript TypeError。這個(gè)錯(cuò)誤可以被安全地忽略,因?yàn)樗粫?huì)產(chǎn)生任何面向用戶的后果。
總結(jié)
import map提供了一種更理智的方式來在瀏覽器中使用ES模塊,而不局限于從相對(duì)或絕對(duì)的URL中導(dǎo)入。這使得我們可以很容易地移動(dòng)代碼,而不需要調(diào)整 import語句,并使個(gè)別模塊的更新更加無縫,而不影響依賴這些模塊的腳本的緩存能力。總的來說,import map為ES模塊在服務(wù)器和瀏覽器中的使用方式帶來了平等性。