1. 程式人生 > >JavaScript模組化CommonJS/AMD/CMD/UMD/ES6Module的區別

JavaScript模組化CommonJS/AMD/CMD/UMD/ES6Module的區別

JS-模組化程序

隨著js技術的不斷髮展,途中會遇到各種問題,比如模組化。

那什麼是模組化呢,他們的目的是什麼?

定義:如何把一段程式碼封裝成一個有用的單元,以及如何註冊此模組的能力、輸出的值
依賴引用:如何引用其它程式碼單元

到目前為止,大概分為以下幾個里程碑式節點。

原始的開發方式 ---> CommonJS ---> AMD ---> CMD ---> UMD ---> ES6Module

原始的開發方式

最開始的時候,JS自身是沒有模組機制的。專案有多個js檔案。

// a.js
function foo() {}
// b.js
function bar() {}
// c.js
foo()

HTML載入

<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>

原始的開發方式,隨著專案的複雜度,程式碼量越來越多,所需要載入的檔案也越來越多,這個時候,就要考慮幾個問題了:

  1. 命名問題:所有檔案的方法都是掛載到global上,會汙染全域性環境,並且需要考慮命名衝突問題。
  2. 依賴問題:script是順序載入的,如果各檔案之間有依賴,那我們得考慮載入.js檔案的書寫順序。
  3. 網路問題。如果檔案過多,所需請求次數會增多,增加載入時間。

CommonJS && node.js

CommonJS規範 CommonJS使用例子
CommonJS規範,主要運行於伺服器端,同步載入模組,而載入的檔案資源大多數在本地伺服器,所以執行速度或時間沒問題。Node.js很好的實現了該規範。
該規範指出,一個單獨的檔案就是一個模組。
模組功能主要的幾個命令:requiremodule.exportsrequire命令用於輸入其他模組提供的功能,module.exports命令用於規範模組的對外介面,輸出的是一個值的拷貝,輸出之後就不能改變了,會快取起來。

// moduleA.js
var name = 'weiqinl'
function foo() {}

module.exports = exports = {
name,
foo
}

// moduleB.js
var ma = require('./moduleA') // 可以省略字尾.js
exports.bar = function() {
ma.name === 'weiqinl' // true
ma.foo() // 執行foo方法
}

// moduleC.js
var mb = require('./moduleB')
mb.bar()

通過例子,我們可以看出require(moduleId)來載入其他模組的內容,其返回值就是其引用的外部模組所暴露的API,之後再通過module.exports或者exports來為當前模組的方法和變數提供輸出介面。

最後通過node來執行模組。

AMD && Require.js

AMD規範文件 AMD使用例子
AMD(Asynchronous Module Definition - 非同步載入模組定義)規範,制定了定義模組的規則,一個單獨的檔案就是一個模組,模組和模組的依賴可以被非同步載入。主要運行於瀏覽器端,這和瀏覽器的非同步載入模組的環境剛好適應,它不會影響後面語句的執行。該規範是在RequireJs的推廣過程中逐漸完善的。

模組功能主要的幾個命令:definerequirereturndefine.amddefine是全域性函式,用來定義模組,define(id?, dependencies?, factory)require命令用於輸入其他模組提供的功能,return命令用於規範模組的對外介面,define.amd屬性是一個物件,此屬性的存在來表明函式遵循AMD規範。

// moduleA.js
define(['jQuery','lodash'], function($, _) {
var name = 'weiqinl',
function foo() {}
return {
name,
foo
}
})

// index.js
require(['moduleA'], function(a) {
a.name === 'weiqinl' // true
a.foo() // 執行A模組中的foo函式
// do sth...
})

// index.html
<script src="js/require.js" data-main="js/index"></script>

在這裡,我們使用define來定義模組,return來輸出介面, require來載入模組,這是AMD官方推薦用法。當然也可以使用其他相容性的寫法,比如對 Simplified CommonJS Wrapper 格式的支援,但背後還是原始AMD的執行邏輯。
AMD的執行邏輯是:提前載入,提前執行。在Requirejs中,申明依賴模組時,會第一時間載入並執行模組內的程式碼,使後面的回撥函式能在所需的環境中執行。
為了更好地優化請求,同時推出了打包工具r.js,使所需載入的檔案數減少。

CMD && Sea.js

CMD規範文件 CMD使用例子
CMD(Common Module Definition - 通用模組定義)規範主要是Sea.js推廣中形成的,一個檔案就是一個模組,可以像Node.js一般書寫模組程式碼。主要在瀏覽器中執行,當然也可以在Node.js中執行。

// moduleA.js
// 定義模組
define(function(require, exports, module) {
var func = function() {
var a = require('./a') // 到此才會載入a模組
a.func()
if(false) {
var b = require('./b') // 到此才會載入b模組
b.func()
}
}
// do sth...
exports.func = func;
})

// index.js
// 載入使用模組
seajs.use('moduleA.js', function(ma) {
var ma = math.func()
})

// HTML,需要在頁面中引入sea.js檔案。
<script src="./js/sea.js"></script>
<script src="./js/index.js"></script>

這裡define是一個全域性函式,用來定義模組,並通過exports向外提供介面。之後,如果要使用某模組,可以通過require來獲取該模組提供的介面。最後使用某個元件的時候,通過seajs.use()來呼叫。

  1. 通過exports暴露介面。這意味著不需要名稱空間了,更不需要全域性變數。
  2. 通過require引入依賴。這可以讓依賴內建,我們只需要關心當前模組的依賴。關注度分離

CMD推崇依賴就近,延遲執行。在上面例子中,通過require引入的模組,只有當程式執行到此處的時候,模組才會自動載入執行。

同時推出了spm(static package manager)的打包方式,聽說支付寶的專案在使用。

UMD && webpack

UMD文件
UMD(Universal Module Definition - 通用模組定義)模式,該模式主要用來解決CommonJS模式和AMD模式程式碼不能通用的問題,並同時還支援老式的全域性變數規範。

// 使用Node, AMD 或 browser globals 模式建立模組
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD模式. 註冊為一個匿名函式
define(['b'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node等類CommonJS的環境
module.exports = factory(require('b'));
} else {
// 瀏覽器全域性變數 (root is window)
root.returnExports = factory(root.b);
}
}(typeof self !== 'undefined' ? self : this, function (b) {
// 以某種方式使用 b

//返回一個值來定義模組匯出。(即可以返回物件,也可以返回函式)
return {};
}));
  1. 判斷define為函式,並且是否存在define.amd,來判斷是否為AMD規範,
  2. 判斷module是否為一個物件,並且是否存在module.exports來判斷是否為CommonJS規範
  3. 如果以上兩種都沒有,設定為原始的程式碼規範。

這種模式,通常會在webpack打包的時候用到。output.libraryTarget將模組以哪種規範的檔案輸出。

ES6 Module && ES6

ES6模組載入文件 ES6 Module使用例子
在ECMAScript 2015版本出來之後,確定了一種新的模組載入方式,我們稱之為ES6 Module。它和前幾種方式有區別和相同點。

  1. 它因為是標準,所以未來很多瀏覽器會支援,可以很方便的在瀏覽器中使用。
  2. 它同時相容在node環境下執行。
  3. 模組的匯入匯出,通過importexport來確定。
  4. 可以和Commonjs模組混合使用。
  5. CommonJS輸出的是一個值的拷貝。ES6模組輸出的是值的引用,載入的時候會做靜態優化。
  6. CommonJS模組是執行時載入確定輸出介面,ES6模組是編譯時確定輸出介面。

ES6模組功能主要由兩個命令構成:importexportimport命令用於輸入其他模組提供的功能。export命令用於規範模組的對外介面。

export的幾種用法

// 輸出變數
export var name = 'weiqinl'
export var year = '2018'

// 輸出一個物件(推薦)
var name = 'weiqinl'
var year = '2018'
export { name, year}


// 輸出函式或類
export function add(a, b) {
return a + b;
}

// export default 命令
export default function() {
console.log('foo')
}

import匯入其他模組

// 正常命令
import { name, year } from './module.js' //字尾.js不能省略

// 如果遇到export default命令匯出的模組
import ed from './export-default.js'

模組編輯好之後,它可以以多種形式載入。

1. 瀏覽器載入

瀏覽器載入ES6模組,使用<script>標籤,但是要加入type="module"屬性
外鏈js檔案

<script type="module" src="index.js"></script>

也可以內嵌在網頁中

<script type="module">
import utils from './utils.js';
// other code
</script>

對於載入外部模組,需要注意:

  • 程式碼是在模組作用域之中執行,而不是在全域性作用域執行。模組內部的頂層變數,外部不可見。
  • 模組指令碼自動採用嚴格模式,不管有沒有宣告use strict
  • 模組之中,可以使用import命令載入其他模組(.js字尾不可省略,需要提供絕對 URL 或相對 URL),也可以使用export命令輸出對外介面。
  • 模組之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模組頂層使用this關鍵字,是無意義的。
  • 同一個模組如果載入多次,將只執行一次。

2. Node載入

Node要求 ES6 模組採用.mjs字尾檔名。也就是說,只要指令碼檔案裡面使用import或者export命令,就必須採用.mjs字尾名。
這個功能還在試驗階段。安裝Node V8.5.0或以上版本,要用--experimental-modules引數才能開啟該功能。

$ node --experimental-modules my-app.mjs

Nodeimport命令只支援非同步載入本地模組(file:協議),不支援載入遠端模組。

總結

以上即是,我對js模組化概念,最主要的還是他們輸入輸出的區別,理論結合實踐完成的簡單梳理。通過閱讀前輩的文章或者各種Issue,也大概瞭解了JS的歷史程序。以史為鑑,可以知興替。此時不斷成為歷史,歷史終將定論。

參考

  1. 各模組化使用的例子
  2. Require.js
  3. Sea.js
  4. UMD
  5. ES6 Module
  6. JavaScript模組化七日談

[完]