JavaScript模塊化規範(CommonJs AMD CMD UMD ES6)

1、 什麼是模塊化?

模塊化是指將一個複雜的程序分解爲多個模塊,方便編碼javascript

2、爲何要使用模塊化?

2.一、 函數寫法

function m1(){
    // xxx
}
function m2(){
    // xxx
}

複製代碼

上面的函數m一、m2就至關於一個模塊,使用的時候,直接調用就能夠了。css

可是這種作法缺點也很明顯:因爲函數是直接掛載在window(全局)對象下,"污染"了全局變量,沒法保證不與其餘模塊發生變量名衝突,並且模塊成員之間看不出直接關係。html

2.二、 對象寫法

既然window對象的可命名屬性名就那麼多,那我再在window(全局)對象上面聲明一個對象,而後把全部的模塊成員都放到這個對象裏面。前端

var module = {
    count: 0,
    function m1(){
        // xxx
    }
    function m2(){
        // xxx
    }
}

複製代碼

上面的函數m1()和m2(),都封裝在module1對象裏。使用的時候,就是調用這個對象的屬性。vue

module.m1();
複製代碼

可是,這樣的寫法會暴露全部模塊成員,內部狀態能夠被外部改寫。好比,外部代碼能夠直接改變內部計數器的值。java

module1._count = 5;
複製代碼

2.三、 當即執行函數

爲了防止內部成員被暴露出去,咱們用當即執行函數能夠實現私有化變量。node

const module2 = (function() {
	let _money = 100
	const m1 = () => {
		console.log(123)
	}
	const m2 = () => {
		console.log(456)
	}
	return {
		f1: m1,
		f2: m2
	}
})()
複製代碼

使用上面的寫法,外部代碼沒法讀取內部的_count變量。react

console.info(module2._count); //undefined
複製代碼

不過雖然這樣function內部的變量就對全局隱藏了,達到是封裝的目的。可是這樣仍是有缺陷的,Module2這個變量仍是暴漏到全局了,隨着模塊的增多,全局變量仍是會愈來愈多。jquery

2.四、使用Script來引用JS模塊

<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script>
<script type="text/javascript" src="d.js"></script>
複製代碼

缺點:git

(1)加載的時候會中止渲染網頁,引入的js文件越多,網頁失去響應的時間越長;

(2)會污染全局變量;

(3)js文件之間存在依賴關係,加載是有順序的,依賴性最大的要放到最後去加載;當項目規模較大時,依賴關係變得錯綜複雜。

(4)要引入的js文件太多,不美觀,代碼難以管理。

2.五、總結

使用函數寫法會致使全局變量污染,並有可能致使命名衝突

使用命名空間會致使內部屬性被暴露,能夠致使內部成員被改寫

使用當即執行函數能夠實現私有化變量,能夠達到必定的防護做用。是早期較好的模塊化方案

使用Script來引用JS模塊會致使文件關係錯綜複雜,難以管理

3、模塊化規範

3.一、CommonJS

2009年,美國程序員Ryan Dahl創造了node.js項目,將javascript語言用於服務器端編程。這標誌"Javascript模塊化編程"正式誕生。由於老實說,在瀏覽器環境下,沒有模塊也不是特別大的問題,畢竟網頁程序的複雜性有限;可是在服務器端,必定要有模塊,與操做系統和其餘應用程序互動,不然根本無法編程。而node.js的模塊系統,就是參照CommonJS規範實現的。

3.1.一、CommonJS特色

  • 全部代碼都運行在模塊做用域,不會污染全局做用域。
  • 模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存
  • 模塊是同步加載的,所以模塊加載的順序,按照其在代碼中出現的順序
  • CommonJS採用同步加載不一樣模塊文件,適用於服務器端的。由於模塊文件都存放在服務器的各個硬盤上,讀取加載時間快,適合服務器端,不適應瀏覽器。 瀏覽器不兼容CommonJs,緣由是瀏覽器缺乏module、exports、require、global四個環境變量。如要使用須要工具轉換。

3.1.二、基本語法

  • 暴露模塊:module.exports = valueexports.xxx = value
  • 引入模塊:require(xxx),若是是第三方模塊,xxx爲模塊名;若是是自定義模塊,xxx爲模塊文件路徑。

此處咱們有個疑問:CommomJS暴露的模塊究竟是什麼?CommonJS規範規定,每一個模塊內部,module變量表明當前模塊,這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,實際上是加載該模塊的module.exports屬性。

// a.js
module.exports = {
    a: 1
}
// or 
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
複製代碼

上面的寫法很好用,可是 module.exports 和 exports 是咋回事?爲啥這幾句代碼就實現模塊化了,讓咱們來看一下基礎的實現

先說 require 吧

var module = require('./a.js')
module.a 
// 這裏其實就是包裝了一層當即執行函數,這樣就不會污染全局變量了,
// 重要的是 module 這裏,module 是 Node 獨有的一個變量
module.exports = {
    a: 1
}
// 基本實現
var module = {
  id: 'xxxx', // 我總得知道怎麼去找到他吧
  exports: {} // exports 就是個空對象
}
// 這個是爲何 exports 和 module.exports 用法類似的緣由
var exports = module.exports 
var load = function (module) {
    // 導出的東西
    var a = 1
    module.exports = a
    return module.exports
};
// 而後當我 require 的時候去找到獨特的
// id,而後將要使用的東西用當即執行函數包裝下,over
複製代碼

再來講說module.exportsexports的區別。

  1. exports是指向的module.exports的引用
  2. module.exports初始值爲一個空對象{},因此exports初始值也是{},可是不能對exports直接賦值,不會有任何效果,,看了上面代碼的同窗確定明白爲何了。
  3. require()返回的是module.exports而不是exports

3.1.三、模塊的加載機制

CommonJS模塊的加載機制是,輸入的是被輸出的值的拷貝。也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。 這點與ES6模塊化有重大差別(下文會介紹),請看下面這個例子:

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
複製代碼

上面代碼輸出內部變量counter和改寫這個變量的內部方法incCounter。

// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3
複製代碼

上面代碼說明,counter輸出之後,lib.js模塊內部的變化就影響不到counter了。這是由於counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能獲得內部變更後的值。

CommonJS規範是 Node 獨有的,若是瀏覽器想使用該規範,就須要用到 Browserify 解析了。

3.1.四、Browserify

Browserify 可讓你使用相似於 node 的 require() 的方式來組織瀏覽器端的 Javascript 代碼,經過 預編譯 讓前端 Javascript 能夠直接使用 Node NPM 安裝的一些庫。 -- 來自百度百科

①下載

  • 全局下載: npm install browserify -g
  • 局部下載: npm install browserify --save-dev

②打包編譯 將須要打包編譯的JS文件經過 運行代碼browserify app.js -o bundle.js 將路徑下的app.js文件編譯output到bundle.js文件中

③頁面使用引入 最後從新在頁面文件中引入bundle.js文件<script type="text/javascript" src="./bundle.js"></script>

3.1.五、總結

  1. CommonJS採用同步加載不一樣模塊文件,適用於服務器端的,不適合在瀏覽器環境中,同步意味着阻塞加載,瀏覽器資源是異步加載的
  2. 代碼沒法直接運行在瀏覽器環境下,必須經過工具轉換成標準的 ES5;

3.二、AMD (Asynchronous Module Definition)

見名知意,就是異步模塊定義。上面已經介紹過,CommonJS是服務器端模塊的規範,主要是爲了JS在後端的表現制定的,不太適合前端。而AMD就是要爲前端JS的表現制定規範。因爲不是JavaScript原生支持,使用AMD規範進行頁面開發須要用到對應的庫函數,也就是require.js(還有個js庫:curl.js)。實際上AMD 是 require.js在推廣過程當中對模塊定義的規範化的產出。 AMD採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。全部依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成以後,這個回調函數纔會運行。

3.2.一、模塊的定義和使用

// 定義一個模塊
define('module', ['dep'], function (dep) {
  return exports;
});

// id  可選參數,用來定義模塊的標識,若是沒有提供該參數,默認腳本文件名(去掉拓展名)

// dependencies 是一個當前模塊用來的模塊名稱數組,(所依賴模塊的數組)

// factory 工廠方法,模塊初始化要執行的函數或對象,若是爲函數,它應該只被執行一次,若是是對象,此對象應該爲模塊的輸出值。


複製代碼

require.js也採用require()語句加載模塊,可是不一樣於CommonJS,它要求二個參數

//導入和使用模塊
require([module], callback);
 
// 第二個參數[module],是一個數組,裏面的成員就是要加載的模塊;

// 第二個參數callback,則是加載成功以後的回調函數

// 等到前面的module加載完成以後,這個回調函數才被調用。
// 加載的模塊會以參數形式傳入該函數,從而在回調函數內部就可使用這些模塊
複製代碼

3.2.二、看個例子

// demo.html
<body>
    //引入所依賴文件和主入口文件
    <script src="./require.js" data-main = './demo.js'></script>
</body>

// modules/m1.js
define(function(){
    var name = 'm1-amd';
    function getName(){
        return name;
    }
    return {getName} //暴露出的模塊
})

// modules/m2.js
//在m2模塊中,引用了m1模塊
define(['m1'],function(m1){
    var msg = 'm2-amd';
    function show(){
        console.log(msg,m1.getName());
    }
    return {show}   //暴露的模塊
})

//demo.js
(function(){
    //配置每一個變量對應的模塊路徑
    require.config({
        paths: {
            m1: './modules/m1',
            m2: './modules/m2',
        }
    })
    require(['m2'],function(m2){
        m2.show(); //結果:m2-amd m1-amd
    })
})()
複製代碼

默認狀況下,require.js假定這加載的模塊與main.js在同一個目錄,而後自動加載。若是不在同一目錄,咱們可使用require.config()方法對模塊的加載行爲進行自定義

在上面的例子中也能夠引用第三方庫,只需在上面代碼的基礎稍做修改:

//demo.js
(function(){
    //配置每一個變量對應的模塊路徑
    require.config({
        paths: {
            m1: './modules/m1',
            m2: './modules/m2',
            jquery:'./jquery-3.3.1'
        }
    })
    require(['m2','jquery'],function(m2,$){
        m2.show(); //結果:m2-amd m1-amd
        $('body').css('backgroundColor','#000');
    })
})()
複製代碼

不過須要注意的是:jquery對模塊化作了各類不一樣的規範,對每一個不一樣模塊都有暴露出的接口名字,對AMD暴露出的接口名字是小寫jquery,所以不能把jquery寫成大寫的jQuery,這樣會報錯

//jquery 3.3.1.js
if(typeof define === 'function' && define.amd){
    define('jquery',[],function(){
        return jQuery;
    })
}
複製代碼

3.2.三、AMD特色:

依賴前置:必須等到全部依賴的模塊加載完成以後纔會執行回調,即便在回調里根本沒用到該模塊。(在定義模塊的時候就要聲明其依賴的模塊),不過目前在AMD2.0也能夠動態加載模塊了

requireJS優缺點

優勢:

一、適合在瀏覽器環境中異步加載模塊

二、能夠並行加載多個模塊

缺點:

一、提升了開發成本,代碼的閱讀和書寫比較困難,模塊定義方式的語義不暢

二、不符合通用的模塊化思惟方式,是一種妥協的實現

3.三、CMD (Common Module Definition)

即通用模塊定義,對應SeaJS,是阿里玉伯團隊首先提出的概念和設計。跟requireJS解決一樣問題,只是運行機制不一樣。

3.3.一、CMD與AMD的不一樣的在於

(1)AMD推崇依賴前置;CMD推崇依賴就近,只有在用到某個模塊的時候再去require:

通俗來講:

AMD在加載完成定義(define)好的模塊就會當即執行,全部執行完成後,遇到require纔會執行主邏輯。(提早加載)

CMD在加載完成定義(define)好的模塊,僅僅是下載不執行,在遇到require纔會執行對應的模塊。(按需加載)

AMD用戶體驗好,由於沒有延遲,CMD性能好,由於只有用戶須要的時候才執行。

CMD爲何會出現,由於對node.js的書寫者友好,由於符合寫法習慣,就像爲什麼vue會受人歡迎的一個道理。

3.3.二、CMD語法

Sea.js 推崇一個模塊一個文件,遵循統一的寫法。

define(id?, deps?, factory)

由於CMD推崇一個文件一個模塊,因此常常就用文件名做爲模塊id CMD推崇依賴就近,因此通常不在define的參數中寫依賴,在factory中寫 factory有三個參數

function(require, exports, module)

  • require 是一個方法,接受 模塊表示 做爲惟一參數,用來獲取其餘模塊提供的接口
  • exports 是一個對象,用來向外提供模塊接口
  • module 是一個對象,上面存儲了與當前模塊相關聯的一些屬性和方法

定義暴露模塊

//定義沒有依賴的模塊
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})

複製代碼
//定義有依賴的模塊
define(function(require, exports, module){
  //引入依賴模塊(同步)
  var module2 = require('./module2')
  //引入依賴模塊(異步)
    require.async('./module3', function (m3) {
    })
  //暴露模塊
  exports.xxx = value
})
複製代碼

引入使用模塊

define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

複製代碼

sea.js 簡單使用教程

①下載sea.js, 並引入

官網: seajs.org/

github : github.com/seajs/seajs

而後將sea.js導入項目: js/libs/sea.js

②建立項目結構

|-modules
    |-m1.js
    |-m2.js
    |-m3.js
    |-m4.js
|-index.html
|-main.js
|-sea.js

複製代碼

③定義sea.js的模塊代碼

// index.html
<body>
    <script src="./sea.js"></script>    //引入依賴文件
    <script>
        seajs.use('./main.js');         //設置主入口文件
    </script>
</body>

// modules/m1.js
define(function(require,exports,module){
    var msg = 'm1';
    function foo(){
        console.log(msg);
    }
    module.exports = {  //暴露的接口
        foo:foo
    }
});

// modules/m2.js
define(function(require,exports,module){
    var msg = 'm2';
    function bar(){
        console.log(msg);
    }
    module.exports = bar;   /暴露的接口
});

// modules/m3.js
define(function(require,exports,module){
    var msg = 'm3';
    function foo(){
        console.log(msg);
    }
    exports.m3 = { foo:foo} /暴露的接口
});

// modules/m4.js
define(function(require,exports,module){
    var msg = 'm4';
    // 同步引入
    var m2 = require('./m2');
    m2();
    // 異步引入
    require.async('./m3',function(m3){
        m3.m3.foo();
    });
    function fun(){
        console.log(msg);
    }
    exports.m4 = fun;   /暴露的接口
})


//main.js
define(function(require,exports,module){
    var m1 = require('./modules/m1');
    m1.foo();
    var m4 = require('./modules/m4');
    m4.m4();
})

複製代碼

最後獲得結果以下

3.3.三、CMD優缺點

優勢: 一樣實現了瀏覽器端的模塊化加載。 能夠按需加載,依賴就近。

缺點: 依賴SPM打包,模塊的加載邏輯偏重

3.四、ES6 Module

ES6 模塊的設計思想是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時肯定這些東西。好比,CommonJS 模塊就是對象,輸入時必須查找對象屬性。

3.4.1 語法

在 ES6 中,使用export關鍵字來導出模塊,使用import關鍵字引用模塊。可是瀏覽器尚未徹底兼容,須要使用babel轉換成ES5的require。

// 導出
export function hello() { };
export default {
  // ...
};
// 導入
import { readFile } from 'fs';
import React from 'react';

複製代碼

使用import導入模塊時,須要知道要加載的變量名或函數名。

在ES6中還提供了export default,爲模塊指定默認輸出.對應導入模塊import時,不須要使用大括號。

//math.js
var num = 0;
var add = function (a, b) {
  return a + b;
};
export { num, add };

//導入
import { num, add } from './math';
function test(ele) {
  ele.textContent = add(1 + num);
}

複製代碼

3.4.二、ES6與CommonJS的區別

CommonJS

  • 對於基本數據類型,屬於複製。即會被模塊緩存。同時,在另外一個模塊能夠對該模塊輸出的變量從新賦值。

  • 對於複雜數據類型,屬於淺拷貝。因爲兩個模塊引用的對象指向同一個內存空間,所以對該模塊的值作修改時會影響另外一個模塊。

  • 當使用require命令加載某個模塊時,就會運行整個模塊的代碼。

  • 當使用require命令加載同一個模塊時,不會再執行該模塊,而是取到緩存之中的值。也就是說,CommonJS模塊不管加載多少次,都只會在第一次加載時運行一次,之後再加載,就返回第一次運行的結果,除非手動清除系統緩存。

  • 循環加載時,屬於加載時執行。即腳本代碼在require的時候,就會所有執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。

ES6模塊

  • ES6模塊中的值屬於【動態只讀引用】。

  • 對於只讀來講,即不容許修改引入變量的值,import的變量是隻讀的,不管是基本數據類型仍是複雜數據類型。當模塊遇到import命令時,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。

  • 對於動態來講,原始值發生變化,import加載的值也會發生變化。不管是基本數據類型仍是複雜數據類型。

  • 循環加載時,ES6模塊是動態引用。只要兩個模塊之間存在某個引用,代碼就可以執行。

3.4.三、優缺點

優勢:

一、容易進行靜態分析

二、面向將來的 EcmaScript 標準

缺點:

一、瀏覽器尚未徹底兼容,必須經過工具轉換成標準的 ES5 後才能正常運行。

二、全新的命令字,新版的 Node.js才支持

4、總結


  • CommonJS規範主要用於服務端編程,加載模塊是同步的,這並不適合在瀏覽器環境,由於同步意味着阻塞加載,瀏覽器資源是異步加載的,所以有了AMD CMD解決方案。

  • AMD規範在瀏覽器環境中異步加載模塊,並且能夠並行加載多個模塊。不過,AMD規範開發成本高,代碼的閱讀和書寫比較困難,模塊定義方式的語義不暢。

  • CMD規範與AMD規範很類似,都用於瀏覽器編程,依賴就近,延遲執行,能夠很容易在Node.js中運行。不過,依賴SPM 打包,模塊的加載邏輯偏重

  • ES6 在語言標準的層面上,實現了模塊功能,並且實現得至關簡單,徹底能夠取代 CommonJS 和 AMD 規範,成爲瀏覽器和服務器通用的模塊解決方案。

相關文章
相關標籤/搜索