1. 沒有模塊化的時代
在JS沒有模塊化標准的時代,如果存在以下依賴關系:
main.js -> b.js -> a.js
那么我們必須把js文件的順序按照模塊的依賴關系順序放到頁面中(簡單的舉例,不考慮循環依賴等復雜情況)
<!-- NoModule.html -->
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<script src="./a.js"></script>
<script src="./b.js"></script>
<script src="./main.js"></script>
</head>
<body></body>
我們需要提前加載好所有的依賴。
//main.js
(function(){
moduleB.logb();
})()
//b.js
var moduleB = (function () {
function logb() {
moduleA.loga();
console.log("logb");
}
return { logb: logb }
})()
//a.js
var moduleA = (function () {
function loga() {
console.log("loga");
}
return { loga: loga }
})()
//輸出結果
//loga
//logb
這種方式相當簡單粗暴啊,當然造成的問題也很多:依賴關系無法顯式維護,全局命名空間污染沖突等等
2. AMD
首先:AMD是一種規范,全稱Asynchronous Module Definition 異步模塊定義
其次:RequireJS(2.3.6)是AMD的一個實現,我們可以使用RequireJS來實際看看這種規范到底怎么回事
依賴關系:main.js -> b.js -> a.js
我們來看看js文件的在頁面中的結構:
<!-- AMD.html -->
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<script src="./require.js"></script>
<script src="./main.js"></script>
</head>
<body></body>
然后是各個文件的代碼:
//main.js
console.log("load main.js");
require(['./b.js'], function (b) {
console.log("call b.logb()");
b.logb();
return {};
})
console.log("end main.js");
//b.js
define(['./a.js'], function (a) {
console.log("load b.js");
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
function logb() {
a.loga();
//注意,這里暫停了5秒
var startTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(startTime);
sleep(5000);
var endTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(endTime);
console.log("logb");
}
return {
logb: logb
};
})
//a.js
define([], function () {
console.log("load a.js")
function loga() {
console.log("loga");
}
return {
loga: loga
};
})
從上面可以看出來,我們初始頁面並不需要引入依賴的模塊js文件。Chrome中打開AMD.html,我們可以觀察到網絡時序圖如下,可以明顯的發現b.js和a.js是在main.js之后被請求的。
此時再看看我們的頁面,發現多了2個script標簽把b.js和a.js給引入進來了。
<!-- AMD.html -->
<html>
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<script src="./require.js"></script>
<script src="./main.js"></script>
<script type="text/javascript" charset="utf-8" async="" data-requirecontext="_" data-requiremodule="b.js"
src="b.js"></script>
<script type="text/javascript" charset="utf-8" async="" data-requirecontext="_" data-requiremodule="a.js"
src="a.js"></script>
</head>
<body></body>
</html>
這就是RequireJS幫我們做的事情了,根據我們指定的依賴,在代碼運行時動態的將依賴的模塊js文件加載到運行環境中。
我們再來看看輸出:
可以很明顯的發現,依賴模塊的加載沒有阻塞后面代碼的執行,並且模塊會在使用前加載好。
而且模塊加載是異步的。
3. CMD
首先:CMD是一種規范,全稱Common Module Definition 通用模塊定義
其次:Sea.js(3.0.0)是CMD的一個實現,我們可以使用Sea.js來實際看看這種規范到底怎么回事
<!-- CMD.html -->
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<script src="./sea.js"></script>
<script>
seajs.use("./main.js");
</script>
</head>
<body></body>
//main.js
console.log("load main.js");
define(function (require, exports, module) {
console.log("call b.logb()");
var b = require('./b.js');
b.logb();
});
console.log("end main.js");
//b.js
console.log("load b.js");
define(function (require, exports, module) {
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
function logb() {
var a = require('./a.js');
a.loga();
//注意,這里暫停了5秒
var startTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(startTime);
sleep(5000);
var endTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(endTime);
console.log("logb");
}
exports.logb = logb;
})
//a.js
console.log("load a.js");
define(function (require, exports, module) {
function loga() {
console.log("loga");
}
exports.loga = loga;
})
同樣的,sea.js會幫我們把需要的依賴模塊動態的加載進來,這里就不截圖了。
同樣的,我們先看輸出結果:
有沒有發現,雖然寫法上依賴就近,但實際上依賴的模塊還是被前置加載了。
最新版本中模塊加載也是異步的了。
4. CommonJS
NodeJS運行環境下的模塊規范
//main.js
console.log("load main.js");
const a = require('./a.js');
const b = require('./b.js');
a.loga();
b.logb();
console.log("end main.js");
//a.js
console.log("load a.js");
function loga() {
console.log("loga");
}
module.exports.loga = loga;
//b.js
console.log("load b.js");
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
function logb() {
//注意,這里暫停了5秒
var startTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(startTime);
sleep(5000);
var endTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(endTime);
console.log("logb");
}
exports.logb = logb;
不同於最新的requireJS和sea.js,CommonJS在node環境中是同步IO,會阻塞后面的代碼執行。
5. ES6 模塊
ES6也有自己的模塊化方案,現在我們即使不使用AMD或者CMD的js實現庫,也能在瀏覽器中直接使用模塊化的方案了。瀏覽器的支持率可以參考: https://caniuse.com/?search=import
<!-- ES6.html -->
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<script type="module" src="./main.js"></script>
<!-- <script src="./main2.js"></script> -->
</head>
<body>ES6.html</body>
ES6支持二種方式的模塊使用,第一種是在script上使用type=module
//main.js
console.log("load main.js");
import { loga } from './a.js';
import logb from './b.js';
loga();
logb();
console.log("end main.js");
//a.js
console.log("load a.js");
export function loga() {
console.log("loga");
}
//b.js
console.log("load b.js");
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
function logb() {
//注意,這里暫停了5秒
var startTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(startTime);
sleep(5000);
var endTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(endTime);
console.log("logb");
}
export default { logb };
輸出結果:
可以發現依賴模塊還是會被提前加載,再看看第二種方式:
<!-- ES6.html -->
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<!-- <script type="module" src="./main.js"></script> -->
<script src="./main2.js"></script>
</head>
<body>ES6.html</body>
console.log("load main.js");
import('./a.js').then(a => {
a.loga();
})
import('./b.js').then(b => {
console.log(b.default());
})
console.log("end main.js");
結果如下:
可以發現,模塊是異步加載進來的。
6. Webpack中的模塊化
可能有人有疑問,我們在Webpack中好像既可以使用require和module.exports的CommonJS語法,也可以使用export和import的ES6語法。那Webpack又是怎么處理的?
而且,前面列出的幾個模塊化方案中基本都是一個js文件作為一個模塊,但是好像Webpack沒有輸出那么多的文件啊?
其實Webpack有自己的模塊化實現,兼容了這二種標准,而且還有一個編譯的過程將多文件bundle到一起。詳細的可以參考:https://segmentfault.com/a/1190000010349749
其核心還是模塊化設計的幾個要點:
- 模塊加載
- 模塊隔離
- 模塊緩存控制
- 模塊依賴維護
總結
其實從個人觀點來看,前端的模塊化經歷了:
- 野蠻發展階段:每個團隊和公司有自己的方案,好苦逼
- 到AMD/CMD階段:行業領頭人推廣,大家圍觀
- 再到原生ES6支持階段:建立瀏覽器標准,大家圍觀
- 和編譯支持階段:在前端越來越復雜,引入預編譯模式,大家膜拜
這么幾個以上的階段后,現階段基本比較穩定在預編譯模式,結合預編譯工具的其他功能和帶來的便利,前端模塊化不再是一個主要關注的技術點。取而代之的是更加關注:代碼分割、按需加載、Tree Shaking、模塊合並、模塊緩存等等問題。