esm是什麼?
esm 是將 javascript 程序拆分成多個單獨模塊,並能按需導入的標準。和webpack,babel不同的是,esm 是 javascript 的標準功能,在瀏覽器端和 nodejs 中都已得到實現。使用 esm 的好處是瀏覽器可以最優化加載模塊,比使用庫更有效率。
esm 標準通過import, export語法實現模塊變量的導入和導出。
esm 模塊的特點
- 存在模塊作用域,頂層變量都定義在該作用域,外部不可見;
- 模塊腳本自動採用嚴格模式;
- 模塊頂層的this關鍵字返回undefined;
- esm 是編譯時加載,也就是隻有所有import的模塊都加載完成,才會開始執行;
- 同一個模塊如果加載多次,只會執行一次。
export
export語句用來導出模塊中的變量。
// 導出變量
export let count = 1;
export const CONST_VAR = 'CONST_VAR';
// 導出函數
export function incCount() {
count += 1;
}
// 導出類
export class Demo {
}
function add(x) {
return x + count;
}
// 使用export導出一組變量
export {
count,
add,
// 使用as重命名導出的變量
add as addCount,
}
// 導出default
export default add
// 合併導出其他模塊的變量
export { name } from './esm_module2.js'
export * from './esm_module2.js'
import
import語句用來導入其他模塊的變量
// 導入變量
import { count, incCount, CONST_VAR } from './esm_module1.js';
// 通過as重命名導入的變量
import { addCount as renamedAddCount } from './esm_module1.js';
// 導入默認
import { default as defaultAdd } from './esm_module1.js';
import add from './esm_module1.js';
// 創建模塊對象
import * as module1 from './esm_module1.js';
export 導出的是值引用
esm 模塊和 commonjs 模塊的一個顯著差異是,cjs 導出的是值得拷貝,esm 導出的是值的引用。當模塊內部的值被修改時,cjs 獲取不到被修改後的值,esm 可以獲取到被修改後的值。
cjs 例子
// cjs_module1.js
var count = 1;
function incCount() {
count += 1;
}
module.exports = {
count: count,
incCount: incCount,
}
// cjs_demo.js
var { count, incCount } = require('./cjs_module1.js');
console.log(count); // 1
incCount();
console.log(count); // 1
esm 例子
// esm_module1.js
let count = 1;
function incCount() {
count += 1;
}
export {
count,
incCount,
}
// esm_demo.js
import { count, incCount } from './esm_module1.js';
console.log(count); // 1
incCount();
console.log(count); // 2
從實現原理上來看,cjs 的 module.exports是一個對象,在運行期注入模塊。在導出語句module.exports.count = count執行時,是給這個對象分配一個count的鍵,並賦值為1。 這之後模塊中的count變量再怎麼變化,都不會干擾到module.exports.count
esm 中的export { count }是導出了count變量的一個只讀引用,等於説使用者讀取count時,值的指向還是模塊中count變量的值。
可以看阮一峯的這篇文章:ES6入門教程
在 html 中使用 esm
使用script標籤引入 esm 文件,同時設置type=module,標識這個模塊為頂級模塊。瀏覽器將 esm 文件視為模塊文件,識別模塊的import語句並加載。
<script src="./esm_main.js" type="module"></script>
如果不設置type=module,瀏覽器認為該文件為普通腳本。檢查到文件中存在import語句時,會報如下錯誤:
esm的加載機制
esm 標準沒有規定模塊的加載細節,將這些留給具體環境實現。大致上分為下面四個步驟:
解析:實現讀取模塊的源代碼並檢查語法錯誤;
加載:遞歸加載所有import的模塊;
鏈接:對每個加載的模塊,都生成一個模塊作用域,該模塊下的所有全局聲明都綁定到該作用域上,包括從其他模塊導入的內容;
運行時:完成所有import的加載和鏈接,腳本運行每個已經加載的模塊中的語句。當運行到全局聲明時,什麼也不會做(在鏈接階段已經將聲明綁定到模塊作用域)。
可以看下 mdn 上的這篇深入 esm 的文章:ES6 In Depth: Modules
動態加載模塊
esm 的一個重要特性是編譯時加載,這有利於引擎的靜態分析。加載的過程會先於代碼的執行。卻也導致import導入語句不能在函數或者if語句中執行:
// 報語法錯誤
if (true) {
import add from './esm_module1.js';
}
es2020 提案引入import()函數,用來動態加載模塊,並且可以用在函數和if語句中。
import('./esm_module1.js')
.then(module => {
console.log(module);
})
import()函數接受要加載的模塊相對路徑,返回一個Promise對象,內容是要加載的模塊對象。
使用import()函數還可以實現根據變量動態加載模塊
async function getTemplate(templateName) {
let template = await import(`./templates/${templateName}`);
console.log(template);
}
getTemplate("foo");
getTemplate("bar");
getTemplate("baz");