徹底掌握前端模塊化
一. 什麼是模塊化開發
1.1. JavaScript 設計缺陷
那麼,到底什麼是模塊化開發呢?
-
事實上模塊化開發最終的目的是將程序劃分成一個個小的結構;
-
這個結構中編寫屬於自己的邏輯代碼,有自己的作用域,不會影響到其他的結構;
-
這個結構可以將自己希望暴露的變量、函數、對象等導出給其結構使用;
-
也可以通過某種方式,導入另外結構中的變量、函數、對象等;
上面說提到的結構,就是模塊;
按照這種結構劃分開發程序的過程,就是模塊化開發的過程;
無論你多麼喜歡 JavaScript,以及它現在發展的有多好,我們都需要承認在 Brendan Eich 用了 10 天寫出 JavaScript 的時候,它都有很多的缺陷:
-
比如 var 定義的變量作用域問題;
-
比如 JavaScript 的面向對象並不能像常規面嚮對象語言一樣使用 class;
-
比如 JavaScript 沒有模塊化的問題;
Brendan Eich 本人也多次承認過 JavaScript 設計之初的缺陷,但是隨着 JavaScript 的發展以及標準化,存在的缺陷問題基本都得到了完善。
- JavaScript 目前已經得到了快速的發展,無論是 web、移動端、小程序端、服務器端、桌面應用都被廣泛的使用;
在網頁開發的早期,Brendan Eich 開發 JavaScript 僅僅作爲一種腳本語言,做一些簡單的表單驗證或動畫實現等,那個時候代碼還是很少的:
-
這個時候我們只需要講 JavaScript 代碼寫到 標籤中即可;
-
並沒有必要放到多個文件中來編寫;
<button id="btn">按鈕</button>
<script>
document.getElementById("btn").onclick = function() {
console.log("按鈕被點擊了");
}
</script>
但是隨着前端和 JavaScript 的快速發展,JavaScript 代碼變得越來越複雜了:
-
ajax 的出現,前後端開發分離,意味着後端返回數據後,我們需要通過 JavaScript 進行前端頁面的渲染;
-
SPA 的出現,前端頁面變得更加複雜:包括前端路由、狀態管理等等一系列複雜的需求需要通過 JavaScript 來實現;
-
包括 Node 的實現,JavaScript 編寫複雜的後端程序,沒有模塊化是致命的硬傷;
所以,模塊化已經是 JavaScript 一個非常迫切的需求:
-
但是 JavaScript 本身,直到 ES6(2015)才推出了自己的模塊化方案;
-
在此之前,爲了讓 JavaScript 支持模塊化,湧現出了很多不同的模塊化規範:AMD、CMD、CommonJS 等;
在這個章節,我們將詳細學習 JavaScript 的模塊化,尤其是 CommonJS 和 ES6 的模塊化。
1.2. 沒有模塊化的問題
我們先來簡單體會一下沒有模塊化代碼的問題。
我們知道,對於一個大型的前端項目,通常是多人開發的(即使一個人開發,也會將代碼劃分到多個文件夾中):
- 我們假設有兩個人:小明和小麗同時在開發一個項目,並且會將自己的 JavaScript 代碼放在一個單獨的 js 文件中。
小明開發了 aaa.js 文件,代碼如下(當然真實代碼會複雜的多):
var flag = true;
if (flag) {
console.log("aaa的flag爲true")
}
小麗開發了 bbb.js 文件,代碼如下:
var flag = false;
if (!flag) {
console.log("bbb使用了flag爲false");
}
很明顯出現了一個問題:
-
大家都喜歡使用 flag 來存儲一個 boolean 類型的值;
-
但是一個人賦值了 true,一個人賦值了 false;
-
如果之後都不再使用,那麼也沒有關係;
但是,小明又開發了 ccc.js 文件:
if (flag) {
console.log("使用了aaa的flag");
}
問題來了:小明發現 ccc 中的 flag 值不對
-
對於聰明的你,當然一眼就看出來,是小麗將 flag 賦值爲了 false;
-
但是如果每個文件都有上千甚至更多的代碼,而且有上百個文件,你可以一眼看出來 flag 在哪個地方被修改了嗎?
備註:引用路徑如下:
<script src="./aaa.js"></script>
<script src="./bbb.js"></script>
<script src="./ccc.js"></script>
所以,沒有模塊化對於一個大型項目來說是災難性的。
當然,我們有辦法可以解決上面的問題:立即函數調用表達式(IIFE)
- IIFE (Immediately Invoked Function Expression)
aaa.js
const moduleA = (function () {
var flag = true;
if (flag) {
console.log("aaa的flag爲true")
}
return {
flag: flag
}
})();
bbb.js
const moduleB = (function () {
var flag = false;
if (!flag) {
console.log("bbb使用了flag爲false");
}
})();
ccc.js
const moduleC = (function() {
const flag = moduleA.flag;
if (flag) {
console.log("使用了aaa的flag");
}
})();
命名衝突的問題,有沒有解決呢?解決了。
但是,我們其實帶來了新的問題:
-
第一,我必須記得每一個模塊中返回對象的命名,才能在其他模塊使用過程中正確的使用;
-
第二,代碼寫起來混亂不堪,每個文件中的代碼都需要包裹在一個匿名函數中來編寫;
-
第三,在沒有合適的規範情況下,每個人、每個公司都可能會任意命名、甚至出現模塊名稱相同的情況;
所以,我們會發現,雖然實現了模塊化,但是我們的實現過於簡單,並且是沒有規範的。
-
我們需要制定一定的規範來約束每個人都按照這個規範去編寫模塊化的代碼;
-
這個規範中應該包括核心功能:模塊本身可以導出暴露的屬性,模塊又可以導入自己需要的屬性;
JavaScript 社區爲了解決上面的問題,湧現出一系列好用的規範,接下來我們就學習具有代表性的一些規範。
二. CommonJS 規範
2.1. CommonJS 和 Node
我們需要知道 CommonJS 是一個規範,最初提出來是在瀏覽器意外的地方使用,並且當時被命名爲 ServerJS,後來爲了體現它的廣泛性,修改爲 CommonJS,平時我們也會簡稱爲 CJS。
-
Node 是 CommonJS 在服務器端一個具有代表性的實現;
-
Browserify 是 CommonJS 在瀏覽器中的一種實現;
-
webpack 打包工具具備對 CommonJS 的支持和轉換(後面我會講到);
所以,Node 中對 CommonJS 進行了支持和實現,讓我們在開發 node 的過程中可以方便的進行模塊化開發:
-
在 Node 中每一個 js 文件都是一個單獨的模塊;
-
這個模塊中包括 CommonJS 規範的核心變量:exports、module.exports、require;
-
我們可以使用這些變量來方便的進行模塊化開發;
前面我們提到過模塊化的核心是導出和導入,Node 中對其進行了實現:
-
exports 和 module.exports 可以負責對模塊中的內容進行導出;
-
require 函數可以幫助我們導入其他模塊(自定義模塊、系統模塊、第三方庫模塊)中的內容;
2.2. Node 模塊化開發
我們來看一下兩個文件:
bar.js
const name = 'coderwhy';
const age = 18;
function sayHello(name) {
console.log("Hello " + name);
}
main.js
console.log(name);
console.log(age);
sayHello('kobe');
上面的代碼會報錯:
-
在 node 中每一個文件都是一個獨立的模塊,有自己的作用域;
-
那麼,就意味着別的模塊 main 中不能隨便訪問另外一個模塊 bar 中的內容;
-
bar 需要導出自己想要暴露的變量、函數、對象等等;
-
main 從 bar 中導入自己想要使用的變量、函數、對象等等;
2.2.1. exports 導出
強調:exports 是一個對象,我們可以在這個對象中添加很多個屬性,添加的屬性會導出
bar.js 中導出內容:
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
main.js 中導入內容:
const bar = require('./bar');
上面這行代碼意味着什麼呢?
- 意味着 main 中的 bar 變量等於 exports 對象;
main中的bar = bar中的exports
所以,我可以編寫下面的代碼:
const bar = require('./bar');
const name = bar.name;
const age = bar.age;
const sayHello = bar.sayHello;
console.log(name);
console.log(age);
sayHello('kobe');
爲了進一步論證,bar 和 exports 是同一個對象:
-
所以,bar 對象是 exports 對象的淺拷貝;
-
淺拷貝的本質就是一種引用的賦值而已;
2.2.2. module.exports
但是 Node 中我們經常導出東西的時候,又是通過 module.exports 導出的:
- module.exports 和 exports 有什麼關係或者區別呢?
我們追根溯源,通過維基百科中對 CommonJS 規範的解析:
-
CommonJS 中是沒有 module.exports 的概念的;
-
但是爲了實現模塊的導出,Node 中使用的是 Module 的類,每一個模塊都是 Module 的一個實例,也就是 module;
-
所以在 Node 中真正用於導出的其實根本不是 exports,而是 module.exports;
-
因爲 module 纔是導出的真正實現者;
但是,爲什麼 exports 也可以導出呢?
-
這是因爲 module 對象的 exports 屬性是 exports 對象的一個引用;
-
也就是說
module.exports = exports = main中的bar
;
注意:真正導出的模塊內容的核心其實是 module.exports,只是爲了實現 CommonJS 的規範,剛好 module.exports 對 exports 對象有一個引用而已;
那麼,如果我的代碼這樣修改了:
你能猜到內存中會有怎麼樣的表現嗎?
-
結論:和 exports 對象沒有任何關係了,exports 你隨便玩自己的吧;
-
module.exports 我現在導出一個自己的對象,不帶着你玩了;
-
新的對象取代了 exports 對象的導出,那麼就意味着 require 導入的對象是新的對象;
2.2.3. require 細節
我們現在已經知道,require 是一個函數,可以幫助我們引入一個文件(模塊)中導入的對象。
那麼,require 的查找規則是怎麼樣的呢?
- https://nodejs.org/dist/latest-v14.x/docs/api/modules.html#modules_all_together
這裏我總結比較常見的查找規則:
導入格式如下:require(X)
-
情況一:X 是一個核心模塊,比如 path、http
-
直接返回核心模塊,並且停止查找
-
情況二:X 是以
./
或../
或/
(根目錄)開頭的 -
查找目錄下面的 index 文件
-
1> 查找 X/index.js 文件
-
2> 查找 X/index.json 文件
-
3> 查找 X/index.node 文件
-
- 如果有後綴名,按照後綴名的格式查找對應的文件
-
- 如果沒有後綴名,會按照如下順序:
-
1> 直接查找文件 X
-
2> 查找 X.js 文件
-
3> 查找 X.json 文件
-
4> 查找 X.node 文件
-
第一步:將 X 當做一個文件在對應的目錄下查找;
-
第二步:沒有找到對應的文件,將 X 作爲一個目錄
-
如果沒有找到,那麼報錯:
not found
-
情況三:直接是一個 X(沒有路徑),並且 X 不是一個核心模塊
-
比如
/Users/coderwhy/Desktop/Node/TestCode/04_learn_node/05_javascript-module/02_commonjs/main.js
中編寫require('why')
-
查找順序
-
如果上面的路徑中都沒有找到,那麼報錯:
not found
2.2.4. 模塊加載順序
這裏我們研究一下模塊的加載順序問題。
結論一:模塊在被第一次引入時,模塊中的 js 代碼會被運行一次
aaa.js
const name = 'coderwhy';
console.log("Hello aaa");
setTimeout(() => {
console.log("setTimeout");
}, 1000);
main.js
const aaa = require('./aaa');
aaa.js 中的代碼在引入時會被運行一次
結論二:模塊被多次引入時,會緩存,最終只加載(運行)一次
main.js
const aaa = require('./aaa');
const bbb = require('./bbb');
aaa.js
const ccc = require("./ccc");
bbb.js
const ccc = require("./ccc");
ccc.js
console.log('ccc被加載');
ccc 中的代碼只會運行一次。
爲什麼只會加載運行一次呢?
-
這是因爲每個模塊對象 module 都有一個屬性:loaded。
-
爲 false 表示還沒有加載,爲 true 表示已經加載;
結論三:如果有循環引入,那麼加載順序是什麼?
如果出現下面模塊的引用關係,那麼加載順序是什麼呢?
-
這個其實是一種數據結構:圖結構;
-
圖結構在遍歷的過程中,有深度優先搜索(DFS, depth first search)和廣度優先搜索(BFS, breadth first search);
-
Node 採用的是深度優先算法:main -> aaa -> ccc -> ddd -> eee ->bbb
2.3. Node 的源碼解析
Module 類
Module.prototype.require 函數
Module._load 函數
三. AMD 和 CMD 規範
3.1. CommonJS 規範缺點
CommonJS 加載模塊是同步的:
-
同步的意味着只有等到對應的模塊加載完畢,當前模塊中的內容才能被運行;
-
這個在服務器不會有什麼問題,因爲服務器加載的 js 文件都是本地文件,加載速度非常快;
如果將它應用於瀏覽器呢?
-
瀏覽器加載 js 文件需要先從服務器將文件下載下來,之後在加載運行;
-
那麼採用同步的就意味着後續的 js 代碼都無法正常運行,即使是一些簡單的 DOM 操作;
所以在瀏覽器中,我們通常不使用 CommonJS 規範:
-
當然在 webpack 中使用 CommonJS 是另外一回事;
-
因爲它會將我們的代碼轉成瀏覽器可以直接執行的代碼;
在早期爲了可以在瀏覽器中使用模塊化,通常會採用 AMD 或 CMD:
-
但是目前一方面現代的瀏覽器已經支持 ES Modules,另一方面藉助於 webpack 等工具可以實現對 CommonJS 或者 ES Module 代碼的轉換;
-
AMD 和 CMD 已經使用非常少了,所以這裏我們進行簡單的演練;
3.2. AMD 規範
AMD 主要是應用於瀏覽器的一種模塊化規範:
-
AMD 是 Asynchronous Module Definition(異步模塊定義)的縮寫;
-
它採用的是異步加載模塊;
-
事實上 AMD 的規範還要早於 CommonJS,但是 CommonJS 目前依然在被使用,而 AMD 使用的較少了;
我們提到過,規範只是定義代碼的應該如何去編寫,只有有了具體的實現才能被應用:
- AMD 實現的比較常用的庫是 require.js 和 curl.js;
這裏我們以 require.js 爲例講解:
第一步:下載 require.js
-
下載地址:https://github.com/requirejs/requirejs
-
找到其中的 require.js 文件;
第二步:定義 HTML 的 script 標籤引入 require.js 和定義入口文件:
- data-main 屬性的作用是在加載完 src 的文件後會加載執行該文件
<script src="./lib/require.js" data-main="./index.js"></script>
第三步:編寫如下目錄和代碼
├── index.html
├── index.js
├── lib
│ └── require.js
└── modules
├── bar.js
└── foo.js
index.js
(function() {
require.config({
baseUrl: '',
paths: {
foo: './modules/foo',
bar: './modules/bar'
}
})
// 開始加載執行foo模塊的代碼
require(['foo'], function(foo) {
})
})();
modules/bar.js
- 如果一個模塊不依賴其他,那麼直接使用 define(function) 即可
define(function() {
const name = "coderwhy";
const age = 18;
const sayHello = function(name) {
console.log("Hello " + name);
}
return {
name,
age,
sayHello
}
})
modules/foo.js
define(['bar'], function(bar) {
console.log(bar.name);
console.log(bar.age);
bar.sayHello('kobe');
})
3.3. CMD 規範
CMD 規範也是應用於瀏覽器的一種模塊化規範:
-
CMD 是 Common Module Definition(通用模塊定義)的縮寫;
-
它也採用了異步加載模塊,但是它將 CommonJS 的優點吸收了過來;
-
但是目前 CMD 使用也非常少了;
CMD 也有自己比較優秀的實現方案:
- SeaJS
我們一起看一下 SeaJS 如何使用:
第一步:下載 SeaJS
-
下載地址:https://github.com/seajs/seajs
-
找到 dist 文件夾下的 sea.js
第二步:引入 sea.js 和使用主入口文件
seajs
是指定主入口文件的
<script src="./lib/sea.js"></script>
<script>
seajs.use('./index.js');
</script>
第三步:編寫如下目錄和代碼
├── index.html
├── index.js
├── lib
│ └── sea.js
└── modules
├── bar.js
└── foo.js
index.js
define(function(require, exports, module) {
const foo = require('./modules/foo');
})
bar.js
define(function(require, exports, module) {
const name = 'lilei';
const age = 20;
const sayHello = function(name) {
console.log("你好 " + name);
}
module.exports = {
name,
age,
sayHello
}
})
foo.js
define(function(require, exports, module) {
const bar = require('./bar');
console.log(bar.name);
console.log(bar.age);
bar.sayHello("韓梅梅");
})
四. ES Module
4.1. 認識 ES Module
JavaScript 沒有模塊化一直是它的痛點,所以纔會產生我們前面學習的社區規範:CommonJS、AMD、CMD 等,所以在 ES 推出自己的模塊化系統時,大家也是興奮異常。
ES Module 和 CommonJS 的模塊化有一些不同之處:
-
一方面它使用了 import 和 export 關鍵字;
-
另一方面它採用編譯期靜態類型檢測,並且動態引用的方式;
ES Module 模塊採用 export 和 import 關鍵字來實現模塊化:
-
export 負責將模塊內的內容導出;
-
import 負責從其他模塊導入內容;
瞭解:採用 ES Module 將自動採用嚴格模式:use strict
-
如果你不熟悉嚴格模式可以簡單看一下 MDN 上的解析;
-
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
4.2. ES Module 的使用
4.2.1. 代碼結構組件
這裏我在瀏覽器中演示 ES6 的模塊化開發:
代碼結構如下:
├── index.html
├── main.js
└── modules
└── foo.js
index.html 中引入兩個 js 文件作爲模塊:
<script src="./modules/foo.js" type="module"></script>
<script src="main.js" type="module"></script>
如果直接在瀏覽器中運行代碼,會報如下錯誤:
這個在 MDN 上面有給出解釋:
-
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
-
你需要注意本地測試 — 如果你通過本地加載 Html 文件 (比如一個
file://
路徑的文件), 你將會遇到 CORS 錯誤,因爲 Javascript 模塊安全性需要。 -
你需要通過一個服務器來測試。
我這裏使用的 VSCode,VSCode 中有一個插件:Live Server
- 通過插件運行,可以將我們的代碼運行在一個本地服務中;
4.2.2. export 關鍵字
export 關鍵字將一個模塊中的變量、函數、類等導出;
foo.js 文件中默認代碼如下:
const name = 'coderwhy';
const age = 18;
let message = "my name is why";
function sayHello(name) {
console.log("Hello " + name);
}
我們希望將其他中內容全部導出,它可以有如下的方式:
方式一:在語句聲明的前面直接加上 export 關鍵字
export const name = 'coderwhy';
export const age = 18;
export let message = "my name is why";
export function sayHello(name) {
console.log("Hello " + name);
}
方式二:將所有需要導出的標識符,放到 export 後面的 {}
中
-
注意:這裏的
{}
裏面不是 ES6 的對象字面量的增強寫法,{}
也不是表示一個對象的; -
所以:
export {name: name}
,是錯誤的寫法;
const name = 'coderwhy';
const age = 18;
let message = "my name is why";
function sayHello(name) {
console.log("Hello " + name);
}
export {
name,
age,
message,
sayHello
}
方式三:導出時給標識符
起一個別名
export {
name as fName,
age as fAge,
message as fMessage,
sayHello as fSayHello
}
4.2.3. import 關鍵字
import 關鍵字負責從另外一個模塊中導入內容
導入內容的方式也有多種:
方式一:import {標識符列表} from '模塊'
;
- 注意:這裏的
{}
也不是一個對象,裏面只是存放導入的標識符列表內容;
import { name, age, message, sayHello } from './modules/foo.js';
console.log(name)
console.log(message);
console.log(age);
sayHello("Kobe");
方式二:導入時給標識符起別名
import { name as wName, age as wAge, message as wMessage, sayHello as wSayHello } from './modules/foo.js';
方式三:將模塊功能放到一個模塊功能對象(a module object)上
import * as foo from './modules/foo.js';
console.log(foo.name);
console.log(foo.message);
console.log(foo.age);
foo.sayHello("Kobe");
4.2.4. export 和 import 結合
如果從一個模塊中導入的內容,我們希望再直接導出出去,這個時候可以直接使用 export 來導出。
bar.js 中導出一個 sum 函數:
export const sum = function(num1, num2) {
return num1 + num2;
}
foo.js 中導入,但是隻是做一箇中轉:
export { sum } from './bar.js';
main.js 直接從 foo 中導入:
import { sum } from './modules/foo.js';
console.log(sum(20, 30));
甚至在 foo.js 中導出時,我們可以變化它的名字
export { sum as barSum } from './bar.js';
爲什麼要這樣做呢?
-
在開發和封裝一個功能庫時,通常我們希望將暴露的所有接口放到一個文件中;
-
這樣方便指定統一的接口規範,也方便閱讀;
-
這個時候,我們就可以使用 export 和 import 結合使用;
4.2.4. default 用法
前面我們學習的導出功能都是有名字的導出(named exports):
-
在導出 export 時指定了名字;
-
在導入 import 時需要知道具體的名字;
還有一種導出叫做默認導出(default export)
-
默認導出 export 時可以不需要指定名字;
-
在導入時不需要使用
{}
,並且可以自己來指定名字; -
它也方便我們和現有的 CommonJS 等規範相互操作;
導出格式如下:
export default function sub(num1, num2) {
return num1 - num2;
}
導入格式如下:
import sub from './modules/foo.js';
console.log(sub(20, 30));
注意:在一個模塊中,只能有一個默認導出(default export);
4.2.5. import()
通過 import 加載一個模塊,是不可以在其放到邏輯代碼中的,比如:
if (true) {
import sub from './modules/foo.js';
}
爲什麼會出現這個情況呢?
-
這是因爲 ES Module 在被 JS 引擎解析時,就必須知道它的依賴關係;
-
由於這個時候 js 代碼沒有任何的運行,所以無法在進行類似於 if 判斷中根據代碼的執行情況;
-
甚至下面的這種寫法也是錯誤的:因爲我們必須到運行時能確定 path 的值;
const path = './modules/foo.js';
import sub from path;
但是某些情況下,我們確確實實希望動態的來加載某一個模塊:
-
如果根據不懂的條件,動態來選擇加載模塊的路徑;
-
這個時候我們需要使用
import()
函數來動態加載;
aaa.js 模塊:
export function aaa() {
console.log("aaa被打印");
}
bbb.js 模塊:
export function bbb() {
console.log("bbb被執行");
}
main.js 模塊:
let flag = true;
if (flag) {
import('./modules/aaa.js').then(aaa => {
aaa.aaa();
})
} else {
import('./modules/bbb.js').then(bbb => {
bbb.bbb();
})
}
4.3. ES Module 的原理
4.3.1. ES Module 和 CommonJS 的區別
CommonJS 模塊加載 js 文件的過程是運行時加載的,並且是同步的:
-
運行時加載意味着是 js 引擎在執行 js 代碼的過程中加載 模塊;
-
同步的就意味着一個文件沒有加載結束之前,後面的代碼都不會執行;
console.log("main代碼執行");
const flag = true;
if (flag) {
// 同步加載foo文件,並且執行一次內部的代碼
const foo = require('./foo');
console.log("if語句繼續執行");
}
CommonJS 通過 module.exports 導出的是一個對象:
-
導出的是一個對象意味着可以將這個對象的引用在其他模塊中賦值給其他變量;
-
但是最終他們指向的都是同一個對象,那麼一個變量修改了對象的屬性,所有的地方都會被修改;
ES Module 加載 js 文件的過程是編譯(解析)時加載的,並且是異步的:
-
編譯時(解析)時加載,意味着 import 不能和運行時相關的內容放在一起使用:
-
比如 from 後面的路徑需要動態獲取;
-
比如不能將 import 放到 if 等語句的代碼塊中;
-
所以我們有時候也稱 ES Module 是靜態解析的,而不是動態或者運行時解析的;
-
異步的意味着:JS 引擎在遇到
import
時會去獲取這個 js 文件,但是這個獲取的過程是異步的,並不會阻塞主線程繼續執行; -
也就是說設置了
type=module
的代碼,相當於在 script 標籤上也加上了async
屬性; -
如果我們後面有普通的 script 標籤以及對應的代碼,那麼 ES Module 對應的 js 文件和代碼不會阻塞它們的執行;
<script src="main.js" type="module"></script>
<!-- 這個js文件的代碼不會被阻塞執行 -->
<script src="index.js"></script>
ES Module 通過 export 導出的是變量本身的引用:
-
export 在導出一個變量時,js 引擎會解析這個語法,並且創建模塊環境記錄(module environment record);
-
模塊環境記錄會和變量進行
綁定
(binding),並且這個綁定是實時的; -
而在導入的地方,我們是可以實時的獲取到綁定的最新值的;
所以我們下面的代碼是成立的:
bar.js 文件中修改
let name = 'coderwhy';
setTimeout(() => {
name = "湖人總冠軍";
}, 1000);
setTimeout(() => {
console.log(name);
}, 2000);
export {
name
}
main.js 文件中獲取
import { name } from './modules/bar.js';
console.log(name);
// bar中修改, main中驗證
setTimeout(() => {
console.log(name);
}, 2000);
但是,下面的代碼是不成立的:main.js 中修改
import { name } from './modules/bar.js';
console.log(name);
// main中修改, bar中驗證
setTimeout(() => {
name = 'kobe';
}, 1000);
思考:如果 bar.js 中導出的是一個對象,那麼 main.js 中是否可以修改對象中的屬性呢?
- 答案是可以的,因爲他們指向同一塊內存空間;(自己編寫代碼驗證,這裏不再給出)
4.3.2. Node 中支持 ES Module
在 Current 版本中
在最新的 Current 版本(v14.13.1)中,支持 es module 我們需要進行如下操作:
-
方式一:在 package.json 中配置
type: module
(後續再學習,我們現在還沒有講到 package.json 文件的作用) -
方式二:文件以
.mjs
結尾,表示使用的是 ES Module;
這裏我們暫時選擇以 .mjs
結尾的方式來演練:
bar.mjs
const name = 'coderwhy';
export {
name
}
main.mjs
import { name } from './modules/bar.mjs';
console.log(name);
在 LTS 版本中
在最新的 LST 版本(v12.19.0)中,我們也是可以正常運行的,但是會報一個警告:
4.3.3. ES Module 和 CommonJS 的交互
CommonJS 加載 ES Module
結論:通常情況下,CommonJS 不能加載 ES Module
-
因爲 CommonJS 是同步加載的,但是 ES Module 必須經過靜態分析等,無法在這個時候執行 JavaScript 代碼;
-
但是這個並非絕對的,某些平臺在實現的時候可以對代碼進行鍼對性的解析,也可能會支持;
-
Node 當中是不支持的;
ES Module 加載 CommonJS
結論:多數情況下,ES Module 可以加載 CommonJS
-
ES Module 在加載 CommonJS 時,會將其 module.exports 導出的內容作爲 default 導出方式來使用;
-
這個依然需要看具體的實現,比如 webpack 中是支持的、Node 最新的 Current 版本也是支持的;
-
但是在最新的 LTS 版本中就不支持;
foo.js
const address = 'foo的address';
module.exports = {
address
}
main.js
import foo from './modules/foo.js';
console.log(foo.address);
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/dnqJsbz5oEGqfZjGTfNMmg