前端模塊化


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

其核心還是模塊化設計的幾個要點:

  • 模塊加載
  • 模塊隔離
  • 模塊緩存控制
  • 模塊依賴維護

總結

其實從個人觀點來看,前端的模塊化經歷了:

  1. 野蠻發展階段:每個團隊和公司有自己的方案,好苦逼
  2. 到AMD/CMD階段:行業領頭人推廣,大家圍觀
  3. 再到原生ES6支持階段:建立瀏覽器標准,大家圍觀
  4. 和編譯支持階段:在前端越來越復雜,引入預編譯模式,大家膜拜

這么幾個以上的階段后,現階段基本比較穩定在預編譯模式,結合預編譯工具的其他功能和帶來的便利,前端模塊化不再是一個主要關注的技術點。取而代之的是更加關注:代碼分割、按需加載、Tree Shaking、模塊合並、模塊緩存等等問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM