前端模塊化IIFE,commonjs,AMD,UMD,ES6 Module規範超詳細講解

為什麼前端需要模塊化

在沒有模塊化的時候,多個腳本引入頁面,會造成諸多問題,比如:

  • 多人協同開發的時候,系統中可能會引入很多js腳本,這些js會定義諸多全局變量,這時候很容易出現變量名覆蓋的問題
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script type="text/javascript">
        var info = "這是功能A";
    </script>
    <script type="text/javascript">
        var info = "這是功能B";
    </script>
    <script>
        console.log(info); // 這是功能B
    </script>
</body>
</html>

上面的例子中可以看到 第一個js中定義的變量info的值被第二個js中的變量所覆蓋

  • 當腳本之間存在依賴關係的時候,單純的引用script看不出js之間的依賴,可讀性很差
<html>
<!-- 此處省略head -->
<body>
    <script type="text/javascript">
        function getMessage(){
            return "這是一條message"
        }
    </script>
    <script type="text/javascript">
        function showMessage(){
            console.log(getMessage());
        }
    </script>
    <script>
        showMessage(); // 這是一條message
    </script>
</body>
</html>

如果第一個腳本沒有引入,那麼執行就會拋錯,也就是說第二個腳本是依賴第一個腳本的,但是這個依賴關係這裡看不出來

什麼是模塊

模塊我理解為就是一個局部作用域,這個局部作用域內部定義了一些局部變量和方法,然後對外提供接口供外部調用,比如:

var moduleA = {
    name : "A"
}
var moduleB = {
    name : "B"
}

console.log(moduleA.name); // A

這裡就可以看成是定義了兩個最簡單的模塊,我們可以通過模塊去訪問各自的變量

是什麼IIFE

IIFE(Immediately Ivoked Function Expression),即立即執行函數表達式,所謂立即執行,就是聲明一個函數,聲明完了立即執行

var IIFE = function(){
    // ...
}
IIFE();

這樣是立即執行但是肯定會有一個問題,函數名衝突了怎麼辦?所以有了我們最常見的寫法,聲明一個自執行匿名函數

(function(){
    // ...
})()

如果看過jquery的一些插件的源碼的話經常能看到這樣的代碼

(function($){
    // ...
})(jQuery)

這裡其實就是表明 這個模塊依賴了jquery

舉個栗子

定義模塊A和模塊B,模塊B依賴模塊A

  • js文件
// 模塊A moduleA.js
(function(window){
    var name = "module A";
    // 對外暴露對象moduleA
    window.moduleA = {
        getName(){
            return name;
        }
    }
})(window)

// 模塊B moduleB.js
(function(window, moduleA){
    // 對外暴露對象moduleB
    window.moduleB = {
        showFirstModuleName(){
            console.log(moduleA.getName());
        }
    }
})(window, moduleA)

// main.js
(function(moduleB){
    console.log(moduleB.showFirstModuleName());
})(moduleB)
  • html文件中
<html>
<!-- 此處省略head -->
<body>
    <script type="text/javascript" type="./moduleA.js"></script>
    <script type="text/javascript" type="./moduleB.js"></script>
    <script type="text/javascript" type="./main.js"></script>
</body>
</html>

上述例子展示了如何用IIFE來定義模塊,這樣寫有幾個缺點

  • 定義了3個模塊,那麼就引入了3個js腳本,那如果有更多模塊呢,那就意味着很頁面加載時會像服務器發起多次http請求,這是不好的
  • html中script的標籤順序是固定的,因為模塊main依賴moduleB,moduleB依賴moduleA,所以moduleA必須先聲明,這樣在moduleB的IIFE執行時候才能正常,不然會拋處ReferenceError

模塊化標準

Commonjs

nodejs採用的模塊化標準,commonjs使用方法require來引入模塊,這裡require()接收的參數是模塊名或者是模塊文件的路徑,如果是模塊名的話,require會到node_modules中去找對應名稱的模塊來加載

const _ = require("lodash");

這裡就引入了一個名為lodash的模塊,那麼一個模塊應該如何對外提供接口呢?
commonjs提供兩種方式對外暴露接口

// 第一種module.exports
const name = "張三";
module.exports = {
    getName(){
        return name
    }
}

// 第二種
const name = "張三"
exports.getName = function(){
    return name;
}

其實本質上,模塊對外暴露的就是exports這個對象,module.exports =這種寫法,相當於直接給exports對象賦值,而export. name這種寫法其實就是給exports對象上添加了一個名為”name”的方法

特徵

  • 在node運行時執行
  • require是對值的拷貝
// moduleA.js
let count = 1;

// 異步讓count++
setTimeout(()=>{
    count++;
});

exports.count = count;

// main.js
const {count} = require("./moduleA.js");

// 同步打印count
console.log(count); // 打印值為1

// 異步打印count
setTimeout(()=>{
    console.log(count); // 打印值為1
});

可見改變了moduleA中的count,並不影響main.js中引入的值

  • 不做特殊處理(webpack打包)commonjs只能運行在node環境,瀏覽器環境不能直接使用,window上沒有定義require這個方法,所以解釋腳本的時候就會拋處ReferenceError
  • commonjs是同步加載模塊,在node環境中require引入一個模塊的時候,這個過程是同步的,必須等模塊加載完才能繼續後續操作

IIFE中的例子用commonjs實現

上述IIFE中的例子,用commonjs來實現就看起來就更清晰:

// 模塊A moduleA.js
const name = "module A"
module.exports = {
    getName(){
        return name;
    }
}

// 模塊B moduleB.js
const {getName} = require("./moduleA.js"); // 引入moduleA

exports.showFirstModuleName = function(){
    console.log(getName());
}

// main.js
const moduleB = require("./moduleB.js");

moduleB.showFirstModuleName(); // module A

上文中講commonjs的特性的時候提到過,不能直接在瀏覽器中運行,所以我們需要先使用打包用具(webpack等工具,以後的文章中會寫)把js打包處理成瀏覽器能直接運行的bundle.js,在引入到html中

<html>
<!-- 此處省略head -->
<body>
    <script type="text/javascript" type="./dist/bundle.js"></script>
</body>
</html>

或者直接在用node運行main:

-> node main.js

AMD和RequireJS

全稱Asynchronous Module Definition異步模塊定義,與commonjs不同AMD是完全針對瀏覽器的模塊化定義,AMD加載模塊是異步的

如何定義一個模塊

AMD規範中定義模塊用到方法define,還是以之前的例子來舉例,先來定義一個沒有依賴的模塊moduleA

// 定義一個moduleA.js
define(function(){
    var name = "module A"
    return {
        getName(){
            return name
        }
    }
})

這裡define只接受了一個回調函數作為參數,這個回調是不是與IIFE有點相似,再來定義一個依賴moduleA的moduleB

// 定義一個moduleB.js
define(["moduleA"], function(moduleA){
    return {
        showFirstModuleName(){
            console.log(moduleA.getName());
        }
    }
});

這裡define的第一個參數是一個數組,數組裏面放的是當前定義的模塊所依賴的模塊的名字,而後面回調函數接收的參數就是對應的模塊了,也許看到這裡你會想問,為什麼這裡只寫一個模塊名「moduleA」就能找到對應的moduleA.js的文件了呢?後面會講

如何在入口文件引入模塊

我們已經實現了moduleA.js和moduleB.js接下來要實現入口main.js,AMD的標準中,引入模塊需要用到方法require,看到這你可能會有疑問,前面說commonjs的時候,不是說了window對象上沒定義require嗎?這裡就不得不提到一個庫,那就是RequireJS

RequireJS is a JavaScript file and module loader.

官網介紹RequireJS是一個js文件和模塊的加載器,提供了加載和定義模塊的api,當在頁面中引入了RequireJS之後,我們便能夠在全局調用define和require,下面來實現main.js

// 實現main.js
require(["moduleB"], function(moduleB){
    moduleB.showFirstModuleName();
});

三個js文件都寫好了,我們該如何引入到頁面中呢?查看RequireJS官網已經給出了答案

<html>
<!-- 此處省略head -->
<body>
    <!--引入requirejs並且在這裡指定入口文件的地址-->
    <script data-main="js/main.js" src="js/require.js"></script>
</body>
</html>

要通過script引入requirejs,然後需要為標籤加一個屬性data-main來指定入口文件

使用RequireJS需要的配置

前面介紹用define來定義一個模塊的時候,直接傳「模塊名」似乎就能找到對應的文件,這一塊是在哪實現的呢?其實在使用RequireJS之前還需要為它做一個配置

// main.js
require.config({
    paths : {
        // key為模塊名稱, value為模塊的路徑
        "moduleA" : "./moduleA",
        "moduleB" : "./moduleB"
    }
});

require(["moduleB"], function(moduleB){
    moduleB.showFirstModuleName();
});

這個配置中的屬性paths應該說是一目了然,看了就能明白,為什麼引入的時候只寫模塊名就能找到對應路徑了吧,不過這裡有一項要注意的是,路徑後面不能跟.js文件後綴名,更多的配置項請參考RequireJS官網

ES6 module

es6提出了新的模塊化方案,這個方案應該也是現在最流行的。通過關鍵字export value來暴露模塊,通過import moduleName from path來引入模塊,是不是看起來很簡單?但是其實這裡還有很多細節

如何運行

  • 瀏覽器端是不能直接運行的,需要先用babel將es6語法轉譯成es5(把import轉譯成了require),然後再使用打包工具打包,最後在頁面中引入
  • node端在某個版本後有辦法直接運行了(抱歉沒查是哪個版本),首先js文件的後綴名都要改成.mjs,然後再命令行直接運行node –experimental-modules main.mjs

多次暴露

模塊可以多次調用export來暴露任何值

// moduleA.mjs
// 暴露一個變量
export let name = "張三"

// 暴露一個方法
export function getName(){
    return name;
}

export function setName(newName){
    name = newName;
}

// main.mjs
import {name, getName, setName} from "./moduleA";

console.log(name); // 張三
setName("李四");
console.log(getName()); // 李四

這裡import後面必須跟結構賦值如果寫成下面這樣,會輸出undefined

import moduleA from "./moduleA"

console.log(moduleA); // undefined;在node環境下運行會報錯

那如果模塊分別暴露的方法有很多怎麼辦呢,這時候結構賦值不是要寫很多個方法?其實還可以這樣引入

import * as moduleA from "./moduleA";

console.log(moduleA.name); // 張三
moduleA.setName("李四");
console.log(moduleA.getName()); // 李四

默認暴露

es6還提供了一種暴露方法叫默認暴露,默認暴露即export default value這裡的value可以是任何值,為什麼上面舉得import的反例,引入結果會是undefined呢,再看一個例子

// moduleA.mjs
export default {
    name : 張三,
    setName(newName){
        this.name = newName;
    },
    getName(){
        return this.name;
    }
}

// main.mjs
import moduleA from "./moduleA"

console.log(moduleA);  // { name: '張三', setName: [Function: setName], getName: [Function: getName] }

這裡其實就能看出來,直接引入給moduleA賦值的其實是export default value後面的value

UMD

UMD全稱為Universal Module Definition,也就是通用模塊定義,為什麼叫通用呢,我們怎麼描述一個模塊是通用的呢?舉個例子,假如現在我的項目使用的是amd模塊規範,那麼現在我引入了一個用commonjs規範寫的模塊,能正常運行嗎?肯定不行的,而UMD就是解決了這個問題。

特點

umd所謂的通用,就是兼容了commonjs和amd規範,這意味着無論是在commonjs規範的項目中,還是amd規範的項目中,都可以直接引用umd規範的模塊使用(牛逼!)

原理

原理其實就是在模塊中去判斷全局是否存在exports和define,如果存在exports,那麼以commonjs的方式暴露模塊,如果存在define那麼以amd的方式暴露模塊

(function(window, factory){
    if(typeof exports === "objects"){
        // commonjs
        module.exports = factory();
    }else if(typeof define === "function"){
        // amd
        define(factory);
    }else{
        window.moduleA = factory();
    }
})(window, function(){
    // 返回module
    let modlueA = {
        name : "張三",
        setName(newName){
            thie.name = newName;
        },
        getName(){
            return this.name;
        }
    }
    return modlueA;s
})