ES6 系列之模塊加載方案

前言

本篇我們重點介紹以下四種模塊加載規範:

  1. AMD
  2. CMD
  3. CommonJS
  4. ES6 模塊

最後再延伸講下 Babel 的編譯和 webpack 的打包原理。

require.js

在瞭解 AMD 規範之前,我們先來看看 require.js 的使用方式。

項目目錄爲:

* project/
    * index.html
    * vender/
        * main.js
        * require.js
        * add.js
        * square.js
        * multiply.js

index.html 的內容如下:

<!DOCTYPE html>
<html>
    <head>
        <title>require.js</title>
    </head>
    <body>
        <h1>Content</h1>
        <script data-main="vender/main" src="vender/require.js"></script>
    </body>
</html>

data-main="vender/main" 表示主模塊是 vender 下的 main.js

main.js 的配置如下:

// main.js
require(['./add', './square'], function(addModule, squareModule) {
    console.log(addModule.add(1, 1))
    console.log(squareModule.square(3))
});

require 的第一個參數表示依賴的模塊的路徑,第二個參數表示此模塊的內容。

由此可以看出,主模塊依賴 add 模塊square 模塊

我們看下 add 模塊add.js 的內容:

// add.js
define(function() {
    console.log('加載了 add 模塊');
    var add = function(x, y) { 
        return x + y;
    };

    return {      
        add: add
    };
});

requirejs 爲全局添加了 define 函數,你只要按照這種約定的方式書寫這個模塊即可。

那如果依賴的模塊又依賴了其他模塊呢?

我們來看看主模塊依賴的 square 模塊square 模塊的作用是求出一個數字的平方,比如輸入 3 就返回 9,該模塊依賴一個乘法模塊,該乘法模塊即 multiply.js 的代碼如下:

// multiply.js
define(function() {
    console.log('加載了 multiply 模塊')
    var multiply = function(x, y) { 
        return x * y;
    };

    return {      
        multiply: multiply
    };
});

square 模塊就要用到 multiply 模塊,其實寫法跟 main.js 添加依賴模塊一樣:

// square.js
define(['./multiply'], function(multiplyModule) {
    console.log('加載了 square 模塊')
    return {      
        square: function(num) {
            return multiplyModule.multiply(num, num)
        }
    };
});

require.js 會自動分析依賴關係,將需要加載的模塊正確加載。

requirejs 項目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/requirejs

而如果我們在瀏覽器中打開 index.html,打印的順序爲:

加載了 add 模塊
加載了 multiply 模塊
加載了 square 模塊
2
9

AMD

在上節,我們說了這樣一句話:

requirejs 爲全局添加了 define 函數,你只要按照這種約定的方式書寫這個模塊即可。

那這個約定的書寫方式是指什麼呢?

指的便是 The Asynchronous Module Definition (AMD) 規範。

所以其實 AMD 是 RequireJS 在推廣過程中對模塊定義的規範化產出。

你去看 AMD 規範) 的內容,其主要內容就是定義了 define 函數該如何書寫,只要你按照這個規範書寫模塊和依賴,require.js 就能正確的進行解析。

sea.js

在國內,經常與 AMD 被一起提起的還有 CMD,CMD 又是什麼呢?我們從 sea.js 的使用開始說起。

文件目錄與 requirejs 項目目錄相同:

* project/
    * index.html
    * vender/
        * main.js
        * require.js
        * add.js
        * square.js
        * multiply.js

index.html 的內容如下:

<!DOCTYPE html>
<html>
<head>
    <title>sea.js</title>
</head>
<body>
    <h1>Content</h1>
    <script src="vender/sea.js"></script>
    <script>
    // 在頁面中加載主模塊
    seajs.use("./vender/main");
    </script>
</body>

</html>

main.js 的內容如下:

// main.js
define(function(require, exports, module) {
    var addModule = require('./add');
    console.log(addModule.add(1, 1))

    var squareModule = require('./square');
    console.log(squareModule.square(3))
});

add.js 的內容如下:

// add.js
define(function(require, exports, module) {
    console.log('加載了 add 模塊')
    var add = function(x, y) { 
        return x + y;
    };
    module.exports = {      
        add: add
    };
});

square.js 的內容如下:

define(function(require, exports, module) {
    console.log('加載了 square 模塊')
    var multiplyModule = require('./multiply');
    module.exports = {      
        square: function(num) {
            return multiplyModule.multiply(num, num)
        }
    };

});

multiply.js 的內容如下:

define(function(require, exports, module) {
    console.log('加載了 multiply 模塊')
    var multiply = function(x, y) { 
        return x * y;
    };
    module.exports = {      
        multiply: multiply
    };
});

跟第一個例子是同樣的依賴結構,即 main 依賴 add 和 square,square 又依賴 multiply。

seajs 項目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/seajs

而如果我們在瀏覽器中打開 index.html,打印的順序爲:

加載了 add 模塊
2
加載了 square 模塊
加載了 multiply 模塊
9

CMD

與 AMD 一樣,CMD 其實就是 SeaJS 在推廣過程中對模塊定義的規範化產出。

你去看 CMD 規範的內容,主要內容就是描述該如何定義模塊,如何引入模塊,如何導出模塊,只要你按照這個規範書寫代碼,sea.js 就能正確的進行解析。

AMD 與 CMD 的區別

從 sea.js 和 require.js 的例子可以看出:

1.CMD 推崇依賴就近,AMD 推崇依賴前置。看兩個項目中的 main.js:

// require.js 例子中的 main.js
// 依賴必須一開始就寫好
require(['./add', './square'], function(addModule, squareModule) {
    console.log(addModule.add(1, 1))
    console.log(squareModule.square(3))
});
// sea.js 例子中的 main.js
define(function(require, exports, module) {
    var addModule = require('./add');
    console.log(addModule.add(1, 1))

    // 依賴可以就近書寫
    var squareModule = require('./square');
    console.log(squareModule.square(3))
});

2.對於依賴的模塊,AMD 是提前執行,CMD 是延遲執行。看兩個項目中的打印順序:

// require.js
加載了 add 模塊
加載了 multiply 模塊
加載了 square 模塊
2
9
// sea.js
加載了 add 模塊
2
加載了 square 模塊
加載了 multiply 模塊
9

AMD 是將需要使用的模塊先加載完再執行代碼,而 CMD 是在 require 的時候纔去加載模塊文件,加載完再接着執行。

感謝

感謝 require.js 和 sea.js 在推動 JavaScript 模塊化發展方面做出的貢獻。

CommonJS

AMD 和 CMD 都是用於瀏覽器端的模塊規範,而在服務器端比如 node,採用的則是 CommonJS 規範。

導出模塊的方式:

var add = function(x, y) { 
    return x + y;
};

module.exports.add = add;

引入模塊的方式:

var add = require('./add.js');
console.log(add.add(1, 1));

我們將之前的例子改成 CommonJS 規範:

// main.js
var add = require('./add.js');
console.log(add.add(1, 1))

var square = require('./square.js');
console.log(square.square(3));
// add.js
console.log('加載了 add 模塊')

var add = function(x, y) { 
    return x + y;
};

module.exports.add = add;
// multiply.js
console.log('加載了 multiply 模塊')

var multiply = function(x, y) { 
    return x * y;
};

module.exports.multiply = multiply;
// square.js
console.log('加載了 square 模塊')

var multiply = require('./multiply.js');

var square = function(num) { 
    return multiply.multiply(num, num);
};

module.exports.square = square;

CommonJS 項目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/commonJS

如果我們執行 node main.js,打印的順序爲:

加載了 add 模塊
2
加載了 square 模塊
加載了 multiply 模塊
9

跟 sea.js 的執行結果一致,也是在 require 的時候纔去加載模塊文件,加載完再接着執行。

CommonJS 與 AMD

引用阮一峯老師的《JavaScript 標準參考教程(alpha)》:

CommonJS 規範加載模塊是同步的,也就是說,只有加載完成,才能執行後面的操作。

AMD規範則是非同步加載模塊,允許指定回調函數。

由於 Node.js 主要用於服務器編程,模塊文件一般都已經存在於本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以 CommonJS 規範比較適用。

但是,如果是瀏覽器環境,要從服務器端加載模塊,這時就必須採用非同步模式,因此瀏覽器端一般採用 AMD 規範。

ES6

ECMAScript2015 規定了新的模塊加載方案。

導出模塊的方式:

var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

引入模塊的方式:

import {firstName, lastName, year} from './profile';

我們再將上面的例子改成 ES6 規範:

目錄結構與 requirejs 和 seajs 目錄結構一致。

<!DOCTYPE html>
<html>
    <head>
        <title>ES6</title>
    </head>
    <body>
        <h1>Content</h1>
        <script src="vender/main.js" type="module"></script>
    </body>
</html>

注意!瀏覽器加載 ES6 模塊,也使用 <script> 標籤,但是要加入 type="module" 屬性。

// main.js
import {add} from './add.js';
console.log(add(1, 1))

import {square} from './square.js';
console.log(square(3));
// add.js
console.log('加載了 add 模塊')

var add = function(x, y) {
    return x + y;
};

export {add}
// multiply.js
console.log('加載了 multiply 模塊')

var multiply = function(x, y) { 
    return x * y;
};

export {multiply}
// square.js
console.log('加載了 square 模塊')

import {multiply} from './multiply.js';

var square = function(num) { 
    return multiply(num, num);
};

export {square}

ES6-Module 項目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/ES6

值得注意的,在 Chrome 中,如果直接打開,會報跨域錯誤,必須開啓服務器,保證文件同源纔可以有效果。

爲了驗證這個效果你可以:

cnpm install http-server -g

然後進入該目錄,執行

http-server

在瀏覽器打開 http://localhost:8080/ 即可查看效果。

打印的順序爲:

加載了 add 模塊
加載了 multiply 模塊
加載了 square 模塊
2
9

跟 require.js 的執行結果是一致的,也就是將需要使用的模塊先加載完再執行代碼。

ES6 與 CommonJS

引用阮一峯老師的 《ECMAScript 6 入門》

它們有兩個重大差異。

  1. CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
  2. CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

第二個差異可以從兩個項目的打印結果看出,導致這種差別的原因是:

因爲 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

重點解釋第一個差異。

CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。

舉個例子:

// 輸出模塊 counter.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
    counter: counter,
    incCounter: incCounter,
};
// 引入模塊 main.js
var mod = require('./counter');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

counter.js 模塊加載以後,它的內部變化就影響不到輸出的 mod.counter 了。這是因爲 mod.counter 是一個原始類型的值,會被緩存。

但是如果修改 counter 爲一個引用類型的話:

// 輸出模塊 counter.js
var counter = {
    value: 3
};

function incCounter() {
    counter.value++;
}
module.exports = {
    counter: counter,
    incCounter: incCounter,
};
// 引入模塊 main.js
var mod = require('./counter.js');

console.log(mod.counter.value); // 3
mod.incCounter();
console.log(mod.counter.value); // 4

value 是會發生改變的。不過也可以說這是 "值的拷貝",只是對於引用類型而言,值指的其實是引用。

而如果我們將這個例子改成 ES6:

// counter.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './counter';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

這是因爲

ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令 import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。換句話說,ES6 的 import 有點像 Unix 系統的“符號連接”,原始值變了,import 加載的值也會跟着變。因此,ES6 模塊是動態引用,並且不會緩存值,模塊裏面的變量綁定其所在的模塊。

Babel

鑑於瀏覽器支持度的問題,如果要使用 ES6 的語法,一般都會藉助 Babel,可對於 import 和 export 而言,只借助 Babel 就可以嗎?

讓我們看看 Babel 是怎麼編譯 import 和 export 語法的。

// ES6
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};
// Babel 編譯後
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

exports.firstName = firstName;
exports.lastName = lastName;
exports.year = year;

是不是感覺有那麼一點奇怪?編譯後的語法更像是 CommonJS 規範,再看 import 的編譯結果:

// ES6
import {firstName, lastName, year} from './profile';
// Babel 編譯後
'use strict';

var _profile = require('./profile');

你會發現 Babel 只是把 ES6 模塊語法轉爲 CommonJS 模塊語法,然而瀏覽器是不支持這種模塊語法的,所以直接跑在瀏覽器會報錯的,如果想要在瀏覽器中運行,還是需要使用打包工具將代碼打包。

webpack

Babel 將 ES6 模塊轉爲 CommonJS 後, webpack 又是怎麼做的打包的呢?它該如何將這些文件打包在一起,從而能保證正確的處理依賴,以及能在瀏覽器中運行呢?

首先爲什麼瀏覽器中不支持 CommonJS 語法呢?

這是因爲瀏覽器環境中並沒有 module、 exports、 require 等環境變量。

換句話說,webpack 打包後的文件之所以在瀏覽器中能運行,就是靠模擬了這些變量的行爲。

那怎麼模擬呢?

我們以 CommonJS 項目中的 square.js 爲例,它依賴了 multiply 模塊:

console.log('加載了 square 模塊')

var multiply = require('./multiply.js');


var square = function(num) { 
    return multiply.multiply(num, num);
};

module.exports.square = square;

webpack 會將其包裹一層,注入這些變量:

function(module, exports, require) {
    console.log('加載了 square 模塊');

    var multiply = require("./multiply");
    module.exports = {
        square: function(num) {
            return multiply.multiply(num, num);
        }
    };
}

那 webpack 又會將 CommonJS 項目的代碼打包成什麼樣呢?我寫了一個精簡的例子,你可以直接複製到瀏覽器中查看效果:

// 自執行函數
(function(modules) {

    // 用於儲存已經加載過的模塊
    var installedModules = {};

    function require(moduleName) {

        if (installedModules[moduleName]) {
            return installedModules[moduleName].exports;
        }

        var module = installedModules[moduleName] = {
            exports: {}
        };

        modules[moduleName](module, module.exports, require);

        return module.exports;
    }

    // 加載主模塊
    return require("main");

})({
    "main": function(module, exports, require) {

        var addModule = require("./add");
        console.log(addModule.add(1, 1))

        var squareModule = require("./square");
        console.log(squareModule.square(3));

    },
    "./add": function(module, exports, require) {
        console.log('加載了 add 模塊');

        module.exports = {
            add: function(x, y) {
                return x + y;
            }
        };
    },
    "./square": function(module, exports, require) {
        console.log('加載了 square 模塊');

        var multiply = require("./multiply");
        module.exports = {
            square: function(num) {
                return multiply.multiply(num, num);
            }
        };
    },

    "./multiply": function(module, exports, require) {
        console.log('加載了 multiply 模塊');

        module.exports = {
            multiply: function(x, y) {
                return x * y;
            }
        };
    }
})

最終的執行結果爲:

加載了 add 模塊
2
加載了 square 模塊
加載了 multiply 模塊
9

參考

ES6 系列

ES6 系列目錄地址:https://github.com/mqyqingfeng/Blog

ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級作用域、標籤模板、箭頭函數、Symbol、Set、Map 以及 Promise 的模擬實現、模塊加載方案、異步處理等內容。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啓發,歡迎 star,對作者也是一種鼓勵。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章