點燈坊

失くすものさえない今が強くなるチャンスよ

Module 進化史:CommonJS、AMD 與 ES Module

Sam Xiao's Avatar 2019-11-27

ECMAScript 之前很難寫大程式,主要是因為沒有 Module 概念,常常一個檔案寫兩三千行程式,且大量使用 Global Variable 造成 Side Effect 很難維護。早期會使用 Module Pattern 解決,稍後更有 CommonJS 與 AMD 試圖制定 Module 標準,一直到 ES Module 後,ECMAScript 模組化總算塵埃落定,是重要里程碑。

Version

macOS Catalina 10.15.1
Node 13.2.0
ECMAScript 2015

Why Module ?

在 ES5 時代,scope 只有兩種概念:global 與 function,而沒有如 C# 的 namespace 或 Java 的 package,因此很難將程式碼加以模組化,造成 ECMAScript 很難寫大程式。

Module 須提供兩大功能:

  • 將 data 或 function 封裝在 module 內
  • 將 interface 暴露在 module 外

ES5 在語言層級並沒有提供以上支援。

Module Pattern

MouseCounterModule.js

let MouseCounterModule = function() {
  let numClicks = 0
   
  let handleClick = console.log(++numClicks)
    
  return {
    countClick: () => document.addEventListener('click', handleClick)
  }  
}()

當語言不支援時,第一個會想用的就是 design pattern 自救。

ECMAScript 什麼都是用 function,module 也用 function 就不意外了。

  • 將 data 或 function 封裝在 module 內:使用了 closure + IIFE
  • 將 interface 暴露在 module 外:return 全新 object

main.js

<script type="text/javascript" src="MouseCounterModule.js"/>
<script type="texty/javascript">
  MouseCounterModule.counterClick()  
</script>

使用 HTML 載入 module,此時 ECMAScript 的載入順序就很重要,需要自行控制。

AMD

AMD
Asynchronous Module Defintion
針對 browser 所設計的 module 解決方案,使用 asynchronous 方式載入 module

MouseCounterModule.js

define('MouseCounterModule', ['jQuery'], $ => {
  let numClicks = 0
    
  let handleClick = () => console.log(++numClicks)
    
  return {
    countClick: () =>
      $(document).on('click', handleClick)
  }
})

define() 為 AMD 所提供 function:

  • 第一個 argument:定義 module 的 ID 作為識別
  • 第二個 argument:array,傳入其他 module 的 ID
  • 第三個 argument:用來建立 module 的 function

除了 define() 外,該寫的 module function 還是要寫。

  • 將 data 或 function 封裝在 module 內:在 function 內使用 closure 封裝
  • 將 interface 暴露在 module 外:return 全新 object

不必再使用 IIFE,define() 會幫你執行。

main.js

require(['MouseCounterModule'], mouseCounterModule =>
  mouseCounterModule.countClick()
)

require() 為 AMD 所提供 function:

  • 第一個 argument:相依的外部 module ID
  • 第二個 argument:使用 module 的 function

AMD 有以下特色:

  • 自動解析 module 的 dependency,不用在乎 module 載入順序
  • Module 以 asynchronous 載入,不會 blocking 影響使用者體驗
  • 允許一個檔案有多個 module,也就是多個 define()

也因為 AMD 的 asynchronous 特性,特別適合在 browser 使用。

CommonJS

CommonJS
為一般性 ECMAScript 環境所設計的解決方案,Node.js 使用

MouseCounterModule.js

let $ = require('jQuery')

let numClicks = 0

let handleClick = () => console.log(++numClicks)

module.exports = {
  countClick: () => $(document).on('click', handleClick)
}

require() 為 CommonJS 所提供的 function,負責載入 module。

  • 將 data 或 function 封裝在 Module 內:numClickshandleClick() 看似 global,但事實上其 scope 只有 module level,不用特別使用 function 與 closure 寫法就能達成封裝 data 與 function

  • 將 interface 暴露在 module 外:將全新 object 指定給 module.exports 即可,不需特別 return

main.js

let MouseCounterModule = require('MouseCounterModule.js')

MouseCounterModule.counterClick()

使用 require() 載入 module 後即可使用,也不用搭配 callback。

CommonJS 有以下特色:

  • Data 與 function 不需再使用 closure,雖然看起來像 global,但 CommonJS 會封裝在 module 內
  • 使用 module.exports 為公開 interface
  • 一個檔案就是一個 module
  • 語法比 AMD 優雅

但 CommonJS 也有幾個缺點:

  • require() 為 synchronous,因此適合在 server 端使用
  • Browser 並未提供 moduleexports,因此還要透過 Browserify 作轉換

ES Module

由於 JavaScript 社群存在這兩大 module 標準,TC39 決定融合 AMD 與 CommonJS 的優點制定出 ES Module,至此 JavaScript 有了正式的 Module 規格。

  • 學習 CommonJS,一個檔案就是一個 Module
  • 學習 CommonJS 簡單優雅的語法
  • 學習 AMD 以 asynchronous 載入 module

MouseCounterModule.js

import $ from 'jquery'

let numClicks = 0

let handleClick = () => console.log(++numClicks);

export default {
  countClick: () => $(document).on('click', handleClick)
}

import 為 ES6 所提供的 keyword,負責載入 module,可以 synchronous 也可 asynchronous。

export 亦為 ES6 所提供的 keyword,負責暴露 interface 於 module 外。

  • 將 data 或 function 封裝在 module 內:numClickshandleClick() 看似 global,但事實上其 scope 只有 module level,不用特別使用 function 與 closure 寫法就能達成封裝 data 與 function,這點與 CommonJS 一樣

  • 將 interface 暴露在 module 外:將全新 object 透過 export 即可,不需特別 return

main.js

import MouseCounterModule from 'MouseCounterModule.js';

MouseCounterModule.counterClick();

使用 import 載入 module 後即可使用,也不用搭配 callback,這點與 CommonJS 一樣。

ES Module 有以下特色:

  • 提供 exportimport 兩個 keyword 就解決
  • 自動解析 module 的 dependency,不用在乎 module 載入順序
  • 語法比 CommonJS 優雅

Conclusion

  • CommonJS 雖然主要用在 Node 後端,但現在前端透過 Browserify 或 Webpack 也可使用 CommonJS
  • Node 目前也能使用 ES module,唯支援尚未完整,現階段建議使用 babel-node 將 ES module 轉成 CommonJS module
  • 若要開發前後端都能使用的 package,建議使用 ES module 語法透過 babel-node 轉成 CommonJS module,如此前後端都能使用

Reference

John Resig, Secret of the JavaScript Ninja, 2nd
MDN, export
MDN, import