如何不基於構建工具優雅的實現模組匯入?

語言: CN / TW / HK

大家好,我是ConardLi。

ES Module 最開始作為一種新的 JavaScript 模組化方案在 ES6 中被引入的候,其實是通過在 import 語句中強制指定相對路徑或絕對路徑來實現的。

import dayjs from "https://cdn.skypack.dev/[email protected]"; // ES modules
console.log(dayjs("2022-08-12").format("YYYY-MM-DD"));

這和其他常見的模組化系統(例如 CommonJS )的工作方式略有不同,並且在使用像 webpack 這樣的模組打包工具的時候會使用更簡單的語法:

const dayjs = require('dayjs') // CommonJS

import dayjs from 'dayjs'; // webpack

在這些系統裡,模組匯入語句通過 Node.js 執行時或相關構建工具對映到特定(版本)的檔案。使用者只需要在 import 語句中直接編寫模組說明符(通常是包名),模組就可以自動處理。

由於開發人員已經熟悉了這種從 npm 匯入包的方式,因此必須要先經過一個的構建步驟才能確保以這種方式編寫的程式碼可以在瀏覽器中執行。

Import maps 就可以解決這個問題,它可以將模組說明符(包名)自動對映到它的相對或絕對路徑。從而讓我們不使用構建工具也能使用簡潔的模組匯入語法。

如何使用 Import maps

我們可以通過 HTML 中的 <script type="importmap"> 標籤來指定一個 Import maps

<script type="importmap">
{
"imports": {
"dayjs": "https://cdn.skypack.dev/[email protected]",
}
}
</script>

為了成功的在模組解析之前對其進行解析。這個 script 標籤必須放在文件中第一個 <script type="module"> 標籤之前(最好放在 <head> 中),另外,目前每個 HTML 只允許編寫一個 Import maps

<script type="module">
import dayjs from 'dayjs';

console.log(dayjs("2022-08-12").format("YYYY-MM-DD"));
</script>

script 標籤內,我們可以通過一個 JSON 物件來為文件中的指令碼所需匯入的模組指定所有必要的對映。一個典型的 importmap 結構如下所示:

<script type="importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js"
"square": "./modules/square.js",
"lodash": "/node_modules/lodash-es/lodash.js"
}
}
</script>

在上面的 import 物件中,每個屬性對應一個對映。對映的左側是匯入說明符的名稱(一般是包名),而右側是說明符需要對映到的相對或絕對路徑。在對映中指定相對路徑時,必須要確保它們始終以 /、../或 ./ 開頭。

另外, importmap 中宣告的包並不一定意味著它一定會被瀏覽器載入。頁面上的指令碼沒有使用到的任何模組都不會被瀏覽器載入,即便你在 importmap 中聲明瞭它。

編寫好 importmap 之後,你就可以在後面的指令碼中直接使用 ES Module 語法了。

<script type="module">
import { cloneDeep } from 'lodash';

const objects = [{ a: 1 }, { b: 2 }];

const deep = cloneDeep(objects);
console.log(deep[0] === objects[0]);
</script>

外部對映

你還可以在外部檔案中指定你的對映,然後使用 scriptsrc 屬性連結到這個檔案( Content-Type Header 必須要設定為 application/importmap+json 才能正常載入)。

<script type="importmap" src="importmap.json"></script>

不過儘量不要使用這種方式,因為它的效能比直接內聯編寫要差。

對映整個包

除了將一個說明符對映到模組之外,你還可以將一個說明符對映到包含多個模組的包:

<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('ConardLi'));
console.log(toLower('ConardLi'));
</script>

動態對映

你也可以基於一些條件在 script 中新增一個動態對映,比如,在下面的示例中我們通過判斷是否存在 IntersectionObserver API 來匯入不同檔案:

<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>

使用同一模組的不同版本

使用 importmap 我們可以將不同的版本的模組對映到不同的包名中:

    <script type="importmap">
{
"imports": {
"lodash@3/": "https://unpkg.com/[email protected]/",
"lodash@4/": "https://unpkg.com/[email protected]/"
}
}
</script>

另外你還可以通過 scopes 來實現同一個包不同模組的更細粒度的版本控制:

<script type="importmap">
{
"imports": {
"lodash/": "https://unpkg.com/[email protected]/"
},
"scopes": {
"/static/js": {
"lodash/": "https://unpkg.com/[email protected]/"
}
}
}
</script>

/static/js 下的模組會使用 3.10.1 版本,而其他模組會使用 4.17.21 版本。

相容性

這項技術目前在 ChromeEdge 瀏覽器 89 及更高版本提供了全面支援,但 Firefox、Safari 和一些移動瀏覽器還沒有支援。我們可以通過下面的程式碼來判斷瀏覽器的支援情況:

if (HTMLScriptElement.supports && HTMLScriptElement.supports('importmap')) {
// import maps is supported
}

對於沒有提供支援的瀏覽器,我們可以使用下面這個 polyfill :https://github.com/guybedford/es-module-shims

另外官方也推薦了一些其他 importmap 相關的 polyfill 和工具:

https://github.com/WICG/import-maps#community-polyfills-and-tooling

參考

  • https://github.com/WICG/import-maps

  • https://www.honeybadger.io/blog/import-maps/

-   E N D   -

3 6 0 W 3 C E C M A T C 3 9 L e a d e r 注和加