談談前端模塊化的演變歷程
隨著前端項目越來越大,代碼復雜性不斷增加,對于模塊化的需求越來越大。模塊化是工程化基礎,只有將代碼模塊化,拆分為合理單元,才具備調度整合的能力。下面就來看看模塊化的概念,以及不同模塊化方案的使用方式和優缺點。
1、模塊概述
由于代碼之間會發生大量交互,如果結構不合理,這些代碼就會變得難以維護、難以測試、難以調試。而使用模塊化就解決了這些問題,模塊化的特點如下:
可重用性: 當應用被組織成模塊時,可以方便的在其他地方重用這些模塊,避免編寫重復代碼,從而加快開發流程;
可讀性: 當應用變得越來越復雜時,如果在一個文件中編寫所有功能,代碼會變得難以閱讀。如果使用模塊設計應用,每個功能都分布在各自的模塊中,代碼就會更加清晰、易讀;
可維護性: 軟件的美妙之處在于進化,從長遠來看,我們需要不斷為應用增加新的功能。當應用被結構化為模塊時,可以輕松添加或刪除功能。除此之外,修復錯誤也是軟件維護的一部分,使用模塊就可以更快速地定位問題。
模塊化是一種將系統分離成獨立功能部分的方法,可以將系統分割成獨立的功能部分,嚴格定義模塊接口,模塊間具有透明性。通過將代碼進行模塊化分隔,每個文件彼此獨立,開發者更容易開發和維護代碼,模塊之間又能夠互相調用和通信,這就是現代化開發的基本模式。
(2)模式
JavaScript 模塊包含三個部分:
- 導入: 在使用模塊時,需要將所需模塊作為依賴項導入。例如,如果想要創建一個 React 組件,就需導入 react 模塊。要使用像 Lodash 這樣的工具庫,就需要安裝并導入它作為依賴項;
- 代碼: 模塊具體代碼;
- 導出: 模塊接口,從模塊中導出的內容可供導入模塊的任何地方使用。
(3)類型
模塊化的貫徹執行離不開相應的約定,即規范。這是能夠進行模塊化工作的重中之重。實現模塊化的規范有很多,比如:AMD、RequireJS、CMD、SeaJS、UMD、CommonJS、ES6 Module。除此之外,IIFE(立即執行函數)也是實現模塊化的一種方案。
本文將介紹其中的六個:
- IIFE: 立即調用函數表達式
- AMD: 異步模塊加載機制
- CMD: 通用模塊定義
- UMD: 統一模塊定義
- CommonJS: Node.js 采用該規范
- ES 模塊: JavaScript 內置模塊系統
2. IIFE
在 ECMAScript 6 之前,模塊并沒有被內置到 JavaScript 中,因為 JavaScript 最初是為小型瀏覽器腳本設計的。這種模塊化的缺乏,導致在代碼的不同部分使用了共享全局變量。
比如,對于以下代碼:
var name = 'JavaScript';
var age = 20;
當上面的代碼運行時,name 和 age 變量會被添加到全局對象中。因此,應用中的所有 JavaScript 腳本都可以訪問全局變量 name 和 age,這就很容易導致代碼錯誤,因為在其他不相關的單元中也可以訪問和修改這些全局變量。除此之外,向全局對象添加變量會使全局命名空間變得混亂并增加了命名沖突的機會。
所以,我們就需要一種封裝變量和函數的方法,并且只對外公開定義的接口。因此,為了實現模塊化并避免使用全局變量,可以使用如下方式來創建模塊:
(function () {
// 聲明私有變量和函數
return {
// 聲明公共變量和函數
}
})();
上面的代碼就是一個返回對象的閉包,這就是我們常說的IIFE(Immediately Invoked Function Expression),即立即調用函數表達式。在該函數中,就創建了一個局部范圍。這樣就避免了使用全局變量(IIFE 是匿名函數),并且代碼單元被封裝和隔離。
可以這樣來使用 IIFE 作為一個模塊:
var module = (function(){
var age = 20;
var name = 'JavaScript'
var fn1 = function(){
console.log(name, age)
};
var fn2 = function(a, b){
console.log(a + b)
};
return {
age,
fn1,
fn2,
};
})();
module.age; // 20
module.fn1(); // JavaScript 20
module.fn2(128, 64); // 192
在這段代碼中,module 就是我們定義的一個模塊,它里面定義了兩個私有變量 age 和 name,同時定義了兩個方法 fn1 和 fn2,其中 fn1 中使用 module 中定義的私有變量,fn2 接收外部傳入參數。最后,module 向外部暴露了age、fn1、fn2。這樣就形成了一個模塊。
當試圖在 module 外部直接調用fn1時,就會報錯:
fn1(); // Uncaught ReferenceError: fn1 is not defined
當試圖在 module 外部打印其內部的私有變量name時,得到的結果是 undefined:
module.name; // undefined
上面的 IIFE 的例子是遵循模塊模式的,具備其中的三部分,其中 age、name、fn1、fn2 就是模塊內部的代碼實現,返回的 age、fn1、fn2 就是導出的內容,即接口。調用 module 方法和變量就是導入使用。
3. CommonJS
(1)概念
① 定義
CommonJS 是社區提出的一種 JavaScript 模塊化規范,它是為瀏覽器之外的 JavaScript 運行環境提供的模塊規范,Node.js 就采用了這個規范。
注意:
- 瀏覽器不支持使用 CommonJS 規范;
- Node.js 不僅支持使用 CommonJS 來實現模塊,還支持最新的 ES 模塊。
CommonJS 規范加載模塊是同步的,只有加載完成才能繼續執行后面的操作。不過由于 Node.js 主要運行在服務端,而所需加載的模塊文件一般保存在本地硬盤,所以加載比較快,而無需考慮使用異步的方式。
② 語法
CommonJS 規范規定每個文件就是一個模塊,有獨立的作用域,對于其他模塊不可見,這樣就不會污染全局作用域。在 CommonJS 中,可以分別使用 export 和 require 來導出和導入模塊。在每個模塊內部,都有一個 module 對象,表示當前模塊。通過它來導出 API,它有以下屬性:
- exports:模塊導出值。
- filename:模塊文件名,使用絕對路徑;
- id:模塊識別符,通常是使用絕對路徑的模塊文件名;
- loaded:布爾值,表示模塊是否已經完成加載;
- parent:對象,表示調用該模塊的模塊;
- children:數組,表示該模塊要用到的其他模塊;
③ 特點
CommonJS 規范具有以下特點:
- 文件即模塊,文件內所有代碼都運行在獨立的作用域,因此不會污染全局空間;
- 模塊可以被多次引用、加載。第一次被加載時,會被緩存,之后都從緩存中直接讀取結果。
- 加載某個模塊,就是引入該模塊的 module.exports 屬性,該屬性輸出的是值拷貝,一旦這個值被輸出,模塊內再發生變化不會影響到輸出的值。
- 模塊加載順序按照代碼引入的順序。
④ 優缺點
CommonJS 的優點:
- 使用簡單
- 很多工具系統和包都是使用 CommonJS 構建的;
- 在 Node.js 中使用,Node.js 是流行的 JavaScript 運行時環境。
CommonJS 的缺點
- 可以在 JavaScript 文件中包含一個模塊;
- 如果想在 Web 瀏覽器中使用它,則需要額外的工具;
- 本質上是同步的,在某些情況下不適合在 Web 瀏覽器中使用。
(2)使用
在 CommonJS 中,可以通過 require 函數來導入模塊,它會讀取、執行 JavaScript 文件,并返回該模塊的 exports 對象,該對象只有在模塊腳本運行完才會生成。
① 模塊導出
可以通過以下兩種方式來導出模塊內容:
module.exports.TestModule = function() {
console.log('exports');
}
exports.TestModule = function() {
console.log('exports');
}
則合兩種方式的導出結果是一樣的,module.exports和exports的區別可以理解為:exports是module.exports的引用,如果在exports調用之前調用了exports=...,那么就無法再通過exports來導出模塊內容,除非通過exports=module.exports重新設置exports的引用指向。
當然,可以先定義函數,再導出:
function testModule() {
console.log('exports');
}
module.exports = testModule;
這是僅導出一個函數的情況,使用時就是這樣的:
testModule = require('./MyModule');
testModule();
如果是導出多個函數,就可以這樣:
function testModule1() {
console.log('exports1');
}
function testModule2() {
console.log('exports2');
}
導入多個函數并使用:
({testModule1, testModule2} = require('./MyModule'));
testModule1();
testModule2();
② 模塊導入
可以通過以下方式來導入模塊:
const module = require('./MyModule');
注意,如果 require 的路徑沒有后綴,會自動按照.js、.json和.node的順序進行補齊查找。
③ 加載過程
在 CommonJS 中,require 的加載過程如下:
- 優先從緩存中加載;
- 如果緩存中沒有,檢查是否是核心模塊,如果是直接加載;
- 如果不是核心模塊,檢查是否是文件模塊,解析路徑,根據解析出的路徑定位文件,然后執行并加載;
- 如果以上都不是,沿當前路徑向上逐級遞歸,直到根目錄的node_modules目錄。
(3)示例
下面來看一個購物車的例子,主要功能是將商品添加到購物車,并計算購物車商品總價格:
// cart.js
var items = [];
function addItem (name, price)
item.push({
name: name,
price: price
});
}
exports.total = function () {
return items.reduce(function (a, b) {
return a + b.price;
}, 0);
};
exports.addItem = addItem;
這里通過兩種方式在 exports 對象上定義了兩個方法:addItem 和 total,分別用來添加購物車和計算總價。
下面在控制臺測試一下上面定義的模塊:
let cart = require('./cart');
這里使用相對路徑來導入 cart 模塊,打印 cart 模塊,結果如下:
cart // { total: [Function], addItem: [Function: addItem] }
向購物車添加一些商品,并計算當前購物車商品的總價格:
cart.addItem('book', 60);
cart.total() // 60
cart.addItem('pen', 6);
cart.total() // 66
這就是創建模塊的基本方法,我們可以創建一些方法,并且只公開希望其他文件使用的部分代碼。該部分成為 API,即應用程序接口。
這里有一個問題,只有一個購物車,即只有一個模塊實例。下面來在控制臺執行以下代碼:
second_cart = require('./cart');
那這時會創建一個新的購物車嗎?事實并非如此,打印當前購物車的商品總金額,它仍然是66:
second_cart.total(); // 66
當我們?創建多個實例時,就需要再模塊內創建一個構造函數,下面來重寫 cart.js 文件:
// cart.js
function Cart () {
this.items = [];
}
Cart.prototype.addItem = function (name, price) {
this.items.push({
name: name,
price: price
});
}
Cart.prototype.total = function () {
return this.items.reduce(function(a, b) {
return a + b.price;
}, 0);
};
module.export = Cart;
現在,當需要使用此模塊時,返回的是 Cart 構造函數,而不是具有 cart 函數作為一個屬性的對象。下面來導入這個模塊,并創建兩個購物車實例:
Cart = require('./second_cart');
cart1 = new Cart();
cart2 = new Cart();
cart1.addItem('book', 50);
cart1.total(); // 50
cart2.total(); // 50
4. AMD
(1)概念
CommonJS 的缺點之一是它是同步的,AMD 旨在通過規范中定義的 API 異步加載模塊及其依賴項來解決這個問題。AMD 全稱為 Asynchronous Module Definition,即異步模塊加載機制。它規定了如何定義模塊,如何對外輸出,如何引入依賴。
AMD規范重要特性就是異步加載。所謂異步加載,就是指同時并發加載所依賴的模塊,當所有依賴模塊都加載完成之后,再執行當前模塊的回調函數。這種加載方式和瀏覽器環境的性能需求剛好吻合。
① 語法
AMD 規范定義了一個全局函數 define,通過它就可以定義和引用模塊,它有 3 個參數:
define(id?, dependencies?, factory);
其包含三個參數:
- id:可選,指模塊路徑。如果沒有提供該參數,模塊名稱默認為模塊加載器請求的指定腳本的路徑。
- dependencies:可選,指模塊數組。它定義了所依賴的模塊。依賴模塊必須根據模塊的工廠函數優先級執行,并且執行的結果應該按照依賴數組中的位置順序以參數的形式傳入工廠函數中。
- factory:為模塊初始化要執行的函數或對象。如果是函數,那么該函數是單例模式,只會被執行一次;如果是對象,此對象應該為模塊的輸出值。
除此之外,要想使用此模塊,就需要使用規范中定義的 require 函數:
require(dependencies?, callback);
其包含兩個參數:
- dependencies:依賴項數組;
- callback:加載模塊時執行的回調函數。
有關 AMD API 的更詳細說明,可以查看 GitHub 上的 AMD API 規范:https://github.com/amdjs/amdjs-api/blob/master/AMD.md。
② 兼容性
該規范的瀏覽器兼容性如下:
③ 優缺點
AMD 的優點:
- 異步加載導致更好的啟動時間;
- 能夠將模塊拆分為多個文件;
- 支持構造函數;
- 無需額外工具即可在瀏覽器中工作。
AMD 的缺點:
- 語法很復雜,學習成本高;
- 需要一個像 RequireJS 這樣的加載器庫來使用 AMD。
(2)使用
當然,上面只是 AMD 規范的理論,要想理解這個理論在代碼中是如何工作的,就需要來看看 AMD 的實際實現。RequireJS 就是 AMD 規范的一種實現,它被描述為“JavaScript 文件和模塊加載器”。下面就來看看 RequireJS 是如何使用的。
① 引入RequireJS
可以通過 npm 來安裝 RequireJS:
npm i requirejs
也可以在 html 文件引入 require.js 文件:
<script data-main="js/config" src="js/require.js"></script>
這里 script標簽有兩個屬性:
- data-main="js/config":這是 RequireJS 的入口,也是配置它的地方;
- src="js/require.js":加載腳本的正常方式,會加載 require.js 文件。
在 script 標簽下添加以下代碼來初始化 RequireJS:
<script>
require(['config'], function() {
//...
})
</script>
當頁面加載完配置文件之后, require() 中的代碼就會運行。這個 script 標簽是一個異步調用,這意味著當 RequireJS 通過 src="js/require.js 加載時,它將異步加載 data-main 屬性中指定的配置文件。因此,該標簽下的任何 JavaScript 代碼都可以在 RequireJS 獲取時執行配置文件。
那 AMD 中的 require() 和 CommonJS 中的 require() 有什么區別呢?
- AMD require() 接受一個依賴數組和一個回調函數,CommonJS require() 接受一個模塊 ID;
- AMD require() 是異步的,而 CommonJS require() 是同步的。
② 定義 AMD 模塊
下面是 AMD 中的一個基本模塊定義:
define(['dependency1', 'dependency2'], function() {
// 模塊內容
});
這個模塊定義清楚地顯示了其包含兩個依賴項和一個函數。
下面來定義一個名為addition.js的文件,其包含一個執行加法操作的函數,但是沒有依賴項:
// addition.js
define(function() {
return function(a, b) {
alert(a + b);
}
});
再來定義一個名為 calculator.js 的文件:
define(['addition'], function(addition) {
addition(7, 9);
});
當 RequireJS 看到上面的代碼塊時,它會去尋找依賴項,并通過將它們作為參數傳遞給函數來自動將其注入到模塊中。
RequireJS 會自動為 addition.js 和 calculator.js 文件創建一個 <script> 標簽,并將其放在HTML <head> 元素中,等待它們加載,然后運行函數,這類似于 require() 的行為。
下面來更新一下 index.html 文件:
// index.html
require(['config'], function() {
require(['calculator']);
});
當瀏覽器加載 index.html 文件時,RequireJS 會嘗試查找 calculator.js 模塊,但是沒有找到,所以瀏覽器也不會有任何反應。那該如何解決這個問題呢?我們必須提供配置文件來告訴 RequireJS 在哪里可以找到 calculator.js(和其他模塊),因為它是引用的入口。
下面是配置文件的基本結構:
requirejs.config({
baseURL: "string",
paths: {},
shim: {},
});
這里有三個屬性值:
- baseURL:告訴 RequireJS 在哪里可以找到模塊;
- path:這些是與 define() 一起使用的模塊的名稱。 在路徑中,可以使用文件的 CDN,這時 RequireJS 將嘗試在本地可用的模塊之前加載模塊的 CDN 版本;
- shim:允許加載未編寫為 AMD 模塊的庫,并允許以正確的順序加載它們
我們的配置文件如下:
requirejs.config({
baseURL: "js",
paths: {
// 這種情況下,模塊位于 customScripts 文件中
addition: "customScripts/addition",
calculator: "customScripts/calculator",
},
});
配置完成之后,重新加載瀏覽器,就會收到瀏覽器的彈窗:
這就是在 AMD 中使用 RequireJS 定義模塊的方法之一。我們還可以通過指定其路徑名來定義模塊,該路徑名是模塊文件在項目目錄中的位置。 下面給出一個例子:
define("path/to/module", function() {
// 模塊內容
})
當然,RequireJS 并不鼓勵這種方法,因為當我們將模塊移動到項目中的另一個位置時,就需要手動更改模塊中的路徑名。
在使用 AMD 定義模塊時需要注意:
- 在依賴項數組中列出的任何內容都必須與工廠函數中的分配相匹配;
- 盡量不要將異步代碼與同步代碼混用。當在 index.html 上編寫其他 JavaScript 代碼時就是這種情況。
5. CMD
CMD 全稱為 Common Module Definition,即通用模塊定義。CMD 規范整合了 CommonJS 和 AMD 規范的特點。sea.js 是 CMD 規范的一個實現 。
CMD 定義模塊也是通過一個全局函數 define 來實現的,但只有一個參數,該參數既可以是函數也可以是對象:
define(factory);
如果這個參數是對象,那么模塊導出的就是對象;如果這個參數為函數,那么這個函數會被傳入 3 個參數:
define(function(require, exports, module) {
//...
});
這三個參數分別如下: (1)require:一個函數,通過調用它可以引用其他模塊,也可以調用 require.async 函數來異步調用模塊; (2)exports:一個對象,當定義模塊的時候,需要通過向參數 exports 添加屬性來導出模塊 API; (3)module 是一個對象,它包含 3 個屬性:
- uri:模塊完整的 URI 路徑;
- dependencies:模塊依賴;
- exports:模塊需要被導出的 API,作用同第二個參數 exports。
下面來看一個例子,定義一個 increment 模塊,引用 math 模塊的 add 函數,經過封裝后導出成 increment 函數:
define(function(require, exports, module) {
var add = require('math').add;
exports.increment = function(val) {
return add(val, 1);
};
module.id = "increment";
});
CMD 最大的特點就是懶加載,不需要在定義模塊的時候聲明依賴,可以在模塊執行時動態加載依賴。除此之外,CMD 同時支持同步加載模塊和異步加載模塊。
AMD 和 CMD 的兩個主要區別如下:
- AMD 需要異步加載模塊,而 CMD 在加載模塊時,可以同步加載(require),也可以異步加載(require.async)。
- CMD 遵循依賴就近原則,AMD 遵循依賴前置原則。也就是說,在 AMD 中,需要把模塊所需要的依賴都提前在依賴數組中聲明。而在 CMD 中,只需要在具體代碼邏輯內,使用依賴前,把依賴的模塊 require 進來。
6. UMD
UMD 全程為 Universal Module Definition,即統一模塊定義。其實 UMD 并不是一個模塊管理規范,而是帶有前后端同構思想的模塊封裝工具。
UMD 是一組同時支持 AMD 和 CommonJS 的模式,它旨在使代碼無論執行代碼的環境如何都能正常工作,通過 UMD 可以在合適的環境選擇對應的模塊規范。比如在 Node.js 環境中采用 CommonJS 模塊管理,在瀏覽器環境且支持 AMD 的情況下采用 AMD 模塊,否則導出為全局函數。
一個UMD模塊由兩部分組成:
- **立即調用函數表達式 (IIFE)**:它會檢查使用模塊的環境。其有兩個參數:root 和 factory。 root 是對全局范圍的 this 引用,而 factory 是定義模塊的函數。
- 匿名函數: 創建模塊,此匿名函數被傳遞任意數量的參數以指定模塊的依賴關系。
UMD 的代碼實現如下:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
module.exports,
module.exports = factory();
} else {
root.returnExports = factory();
}
}(this, function () {
// 模塊內容定義
return {};
}));
它的執行過程如下:
- 先判斷是否支持 Node.js 模塊格式(exports 是否存在),存在則使用 Node.js 模塊格式;
- 再判斷是否支持 AMD(define 是否存在),存在則使用 AMD 方式加載模塊;
- 若兩個都不存在,則將模塊公開到全局(Window 或 Global)。
UMD的特點如下:① UMD 的優點:
- 小而簡潔;
- 適用于服務器端和客戶端。
② UMD 的缺點:
- 不容易正確配置。
7. ES 模塊
(1)概念
通過上面的例子,你可能會發現,使用 UMD、AMD、CMD 的代碼會變得難以編寫和理解。于是在 2015 年,負責 ECMAScript 規范的 TC39 委員會將模塊添加為 JavaScript 的內置功能,這些模塊稱為 ECMAScript模塊,簡稱 ES 模塊。
模塊和經典 JavaScript 腳本略有不同:
- 模塊默認啟用嚴格模式,比如分配給未聲明的變量會報錯:
<script type="module">
a = 5;
</script>
- 模塊有一個詞法頂級作用域。 這意味著,例如,運行 var foo = 42; 在模塊內不會創建名為 foo 的全局變量,可通過瀏覽器中的 window.foo 訪問,盡管在經典JavaScript腳本中會出現這種情況;
<script type="module">
let person = "Alok";
</script>
<script type="module">
alert(person);{/* Error: person is not defined */}
</script>
- 模塊中的 this 并不引用全局 this,而是 undefined。 (如果需要訪問全局 this,可以使用 globalThis);
<script>
alert(this); {/* 全局對象 */}
</script>
<script type="module">
alert(this); {/* undefined */}
</script>
- 新的靜態導入和導出語法僅在模塊中可用,并不適用于經典腳本。
- 頂層 await 在模塊中可用,但在經典 JavaScript 腳本中不可用;
- await 不能在模塊中的任何地方用作變量名,經典腳本中的變量可以在異步函數之外命名為 await;
- JavaScript 會提升 import 語句。因此,可以在模塊中的任何位置定義它們。
CommonJS 和 AMD 都是在運行時確定依賴關系,即運行時加載,CommonJS 加載的是拷貝。而 ES 模塊是在編譯時就確定依賴關系,所有加載的其實都是引用,這樣做的好處是可以執行靜態分析和類型檢查。
(2)語法
① 導出
當導出模塊代碼時,需要在其前面添加 export 關鍵詞。導出內容可以是變量、函數或類。任何未導出的代碼都是模塊私有的,無法在該模塊之被外訪問。ES 模塊支持兩種類型的導出:
- 命名導出:
export const first = 'JavaScript';
export function func() {
return true;
}
當然,我們也可以先定義需要導出的變量/函數,最后統一導出這些變量/函數:
const first = 'JavaScript';
const second = 'TypeScript';
function func() {
return true;
}
export {first, second, func};
- 默認導出:
function func() {
return true;
}
export default func;
當然,也可以直接默認導出:
export default function func() {
return true;
}
默認導出可以省略變量/函數/類名,在導入時可以為其指定任意名稱:
// 導出
export default function () {
console.log('foo');
}
// 導入
import customName from './module';
注意: 導入默認模塊時不需要大括號,導出默認的變量或方法可以有名字,但是對外是無效的。export default 在一個模塊文件中只能使用一次。
可以使用 as 關鍵字來重命名需要暴露出的變量或方法,經過重命名后同一變量可以多次暴露出去:
const first = 'test';
export {first as second};
② 導入
使用命名導出的模塊,可以通過以下方式來導入:
import {first, second, func} from './module';
使用默認導出的模塊,可以通過以下方式來引入,導入名稱可以自定義,無論導出的名稱是什么:
import customName from './module.js';
導入模塊位置可以是相對路徑也可以是絕對路徑,.js擴展名是可以省略的,如果不帶路徑而只是模塊名,則需要通過配置文件告訴引擎查找的位置:
import {firstName, lastName} from './module';
可以使用 as 關鍵字來將導入的變量/函數重命名:
import { fn as fn1 } from './profile';
在 ES 模塊中,默認導入和命名導入是可以同時使用的,比如在 React 組件中:
import React, {usestate, useEffect} from 'react';
const Comp = () => {
return <React.Fragment>...</React.Fragment>
}
export default Comp;
可以使用 as 關鍵字來加載整個模塊,用于從另一個模塊中導入所有命名導出,會忽略默認導出:
import * as circle from './circle';
console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));
③ 動態導入
上面我們介紹的都是靜態導入,使用靜態 import 時,整個模塊需要先下載并執行,然后主代碼才能執行。有時我們不想預先加載模塊,而是按需加載,僅在需要時才加載。這可以提高初始加載時的性能,動態 import 使這成為可能:
<script type="module">
(async () => {
const moduleSpecifier = './lib.mjs';
const {repeat, shout} = await import(moduleSpecifier);
repeat('hello');
// → 'hello hello'
shout('Dynamic import in action');
// → 'DYNAMIC IMPORT IN ACTION!'
})();
</script>
與靜態導入不同,動態導入可以在常規腳本中使用。
④ 其他用法
可以使用以下方式來先導入后導出模塊內容:
export { foo, bar } from './module';
上面的代碼就等同于:
import { foo, bar } from './module';
export { foo, boo};
另一個與模塊相關的新功能是import.meta,它是一個給 JavaScript 模塊暴露特定上下文的元數據屬性的對象。它包含了這個模塊的信息,比如說這個模塊的 URL。
默認情況下,圖像是相對于 HTML 文檔中的當前 URL 加載的。import.meta.url可以改為加載相對于當前模塊的圖像:
function loadThumbnail(relativePath) {
const url = new URL(relativePath, import.meta.url);
const image = new Image();
image.src = url;
return image;
}
const thumbnail = loadThumbnail('../img/thumbnail.png');
container.append(thumbnail);
(3)在瀏覽器使用
目前主流瀏覽器都支持 ES 模塊:
如果想在瀏覽器中使用原生 ES 模塊方案,只需要在 script 標簽上添加 type="module" 屬性。通過該屬性,瀏覽器知道這個文件是以模塊化的方式運行的。而對于不支持的瀏覽器,需要通過 nomodule 屬性來指定某腳本為 fallback 方案:
<script type="module">
import module1 from './module1'
</script>
<script nomodule src="fallback.js"></script>
支持 type="module" 的瀏覽器會忽略帶有 nomodule 屬性的腳本。使用 type="module" 的另一個作用就是進行 ES Next 兼容性的嗅探。因為支持 ES 模塊化的瀏覽器,都支持 ES Promise 等特性。
由于默認情況下模塊是延遲的,因此可能還希望以延遲方式加載 nomodule 腳本:
<script nomodule defer src="fallback.js"></script>
(4)在 Node.js 使用
上面提到,Node.js 使用的是 CommonJS 模塊規范,它也是支持 ES 模塊的。在 Node.js 13 之前,ES 模塊是一項實驗性技術,因此,可以通過使用 .mjs 擴展名保存模塊并通過標志訪問它來使用模塊。
從 Node.js 13 開始,可以通過以下兩種方式使用模塊:
- 使用 .mjs 擴展名保存模塊;
- 在最近的文件夾中創建一個 type="module" 的 package.json 文件。
那如何在小于等于 12 版本的 Node.js 中使用 ES 模塊呢?可以在執行腳本啟動時加上 --experimental-modules,不過這一用法要求相應的文件后綴名必須為 .mjs:
node --experimental-modules module1.mjs
import module1 from './module1.mjs'
module1