模組系統

JavaScript語言長期以來並未內建支援模組系統,社群上發展了兩套知名的模組系統,但它們並不相容:

  • CommonJS Modules

  • Asynchronous Module Definition (AMD)

ES6中加入了模組系統的支援,它採用了CommonJS與AMD的優點,成了未來JavaScript語言中重要的特性。

模組系統是什麼?

當程式碼愈寫愈多,應用程式的規模愈來愈大時,我們需要一個用於組織與管理程式碼的方式,這個需求相當明確,或許不只是應用程式發展到一定程度才會考慮這些,而是應該在開發程式之前的規劃就需要考量進來。

JavaScript語言是一個沒有命名空間設計的程式語言,也沒有支援類似的組織與程式碼分離的設計。有些人認為使用物件定義的字面文字,可以定義出物件的方法與屬性,但如果你看過"物件"、"this"與"原型物件導向"的章節內容,就知道物件中並沒有區分私有、公開成員或方法的特性,這個組織法頂多只是把方法或屬性整理集中而已。

而在很早之前(2003)在社群上發展出一個稱之為模組樣式(module pattern),以及之後的變型如 暴露模組樣式(Revealing Module Pattern),就是第一期的程式碼組織方式。模組樣式實作相當簡單,有許多早期開始發展的函式庫或框架採用這個樣式,甚至到今天也可以看到它的使用身影。一個簡單的範例如下(以下範例來自jQuery):

// The module pattern
var feature = (function() {

    // Private variables and functions
    var privateThing = "secret";
    var publicThing = "not secret";

    var changePrivateThing = function() {
        privateThing = "super secret";
    };

    var sayPrivateThing = function() {
        console.log( privateThing );
        changePrivateThing();
    };

    // Public API
    return {
        publicThing: publicThing,
        sayPrivateThing: sayPrivateThing
    };
})();

feature.publicThing; // "not secret"

// Logs "secret" and changes the value of privateThing
feature.sayPrivateThing();

它使用了IIFE函式的特性,區分出作用域,不過它並沒有辦法徹底解決問題,它在小型的應用程式可以用得很好,但在複雜的程式中仍然有很大的問題,例如以下的問題:

  • 沒辦法在程式中作模組載入

  • 模組之間的相依性不易管理

  • 異步載入模組

  • 除錯與測試都不容易

  • 在大型專案中不易管理

模組樣式似乎是一個暫時性的解決方案,但不得不說它的確是上一代很重要的程式碼組織方式,第二代的模組系統,是在2009年之後的CommonJS與AMD計劃,它們實作出真正完整的模組系統,CommonJS是專門設計給伺服器端的Node.js使用的,而AMD的目標對象則是瀏覽器端。當然它們兩者的設計有所不同,也不相容,使用時也可能需要搭配載入工具來一併使用,不過這個階段的模組系統已經是較前一代完善許多,在相依性與模組輸出與輸入,都有相對的解決方式,程式碼的管理與組織方便了許多。

CommonJS與AMD並不會在這裡討論,我們的重點是是ES6中的模組系統,ES6中加入了模組系統的支援,它採用了CommonJS與AMD的優點,是一個語言內建的模組系統,而且它可以使用於瀏覽器與伺服器端,這是一個重大的新特性,可以讓你的開發日子更輕鬆許多。

模組如何開始使用

ES6的模組系統使用上相當簡單,大致上只有三個重點:

  • ES6的模組程式碼會自動變成strict-mode(嚴格模式),不論你有沒有使用"use strict"在程式碼中。

  • ES6的模組是一個檔案一個模組

  • ES6模組使用export(輸出)與import(輸入)語句來進行模組輸出與輸入。輸出通常位於檔案最後,輸入位於最前面。

模組輸出與輸入

有寫成模組的程式碼檔案,才能讓其他程式碼檔案進入輸入。模組輸出可以使用export關鍵字,在想要輸出(也就是變為公開部份)加在前面,物件、類別、函式與原始資料(變數與常數)都可以輸出,例如以下的範例:

多個輸出名稱

export const aString = 'test'

export function aFunction(){
  console.log('function test')
}

export const aObject = {a: 1}

export class aClass {
  constructor(name, age){
    this.name = name
    this.age = age
  }
}

上面稱之為多個輸出名稱的情況,有兩種方式可以進行輸入,一種是每個要輸入的名稱都需要定在在花括號({})之中,例如以下的範例:

import {aString, aObject, aFunction, aClass} from './lib.js'

console.log(aString)
console.log(aObject)

另一種是使用萬用字元(*),代表要輸入所有的輸出定義的值,不過你需要加上一個模組名稱,例如下面程式碼中的myModule,這是為了防止命名空間的衝突之用的,之後的程式碼中都需要用這個模組名稱來存取輸出模組中的值,這個作法不常使用:

import  * as myModule from './lib.js'

console.log(myModule.aString)
console.log(myModule.aObject)

myModule.aFunction()
const newObj = new myModule.aClass('Inori', 16)
console.log(newObj)

單一輸出名稱

這個要輸出成為模組的程式碼檔案中,只會有一個輸出的變數/常數、函式、類別或物件,通常會加上default關鍵詞。如果要使用有回傳值的函式,通常也是用單一輸出的方式。例如以下的範例:

function aFunction(param){
  return param * param
}

export default aFunction

對單一輸出的模組就不需要用花括號,這代表只輸入以default值定義的輸出語句:

import aFunction from './lib2.js'

console.log(aFunction(5))

這是最特別的,可以在輸入時改變輸入值的名稱,這樣可以讓作輸入檔案中,確保不會有名稱衝突的情況:

import square from './lib2.js'

console.log(square(5))

合法的輸出語法

export var x = 42;                      // export a named variable
export function foo() {};               // export a named function

export default 42;                      // export the default export
export default function foo() {};       // export the default export as a function

export { encrypt };                     // export an existing variable
export { decrypt as dec };              // export a variable as a new name
export { encrypt as en } from 'crypto'; // export an export from another module
export * from 'crypto';                 // export all exports from another module

合法的輸入語法

import 'jquery';                        // import a module without any import bindings
import $ from 'jquery';                 // import the default export of a module
import { $ } from 'jquery';             // import a named export of a module
import { $ as jQuery } from 'jquery';   // import a named export to a different name
import * as crypto from 'crypto';    // import an entire module instance object

參考資料

Last updated